Статьи

Кеширование модели Rails с помощью Redis

redisrails

Кэширование на уровне модели часто игнорируется даже опытными разработчиками. Во многом это из-за неправильного представления о том, что при кэшировании представлений вам не нужно кэшировать на более низких уровнях. Хотя это правда, что большая часть узкого места в мире Rails лежит на слое View, это не всегда так.

Кэши более низкого уровня очень гибки и могут работать в любом месте приложения. В этом уроке я покажу, как кэшировать ваши модели с помощью Redis.

Как работает кеширование?

Традиционно доступ к диску был дорогим. Попытка частого доступа к данным с диска отрицательно скажется на производительности. Чтобы противостоять этому, мы можем реализовать слой кэширования между вашим приложением и сервером базы данных.

Сначала слой кэширования не содержит никаких данных. Когда он получает запрос данных, он вызывает базу данных и сохраняет результат в памяти (кеш). Все последующие запросы будут обслуживаться из уровня кэша, поэтому исключается ненужное обратное обращение к серверу базы данных, что повышает производительность.

Почему Редис?

Redis — это хранилище значений ключей в памяти. Это невероятно быстро, а поиск данных происходит практически мгновенно. Redis поддерживает расширенные структуры данных, такие как списки, хэши, наборы и может сохраняться на диске.

Хотя большинство разработчиков предпочитают Memcache с Dalli для своих нужд кэширования, я считаю, что Redis очень прост в настройке и прост в администрировании. Кроме того, если вы используете resque или Sidekiq для управления фоновыми заданиями, возможно, у вас уже установлен Redis. Для тех, кому интересно знать, когда следует использовать Redis, эта дискуссия является хорошим началом.

Предпосылки

Я предполагаю, что у вас есть Rails и работает. В этом примере используется Rails 4.2.rc1, haml для визуализации представлений и MongoDB в качестве базы данных, но фрагменты этого руководства должны быть совместимы с любой версией Rails.

Вам также нужно установить и запустить Redis, прежде чем мы начнем. Перейдите в каталог приложения и выполните следующие команды:

$ wget http://download.redis.io/releases/redis-2.8.18.tar.gz $ tar xzf redis-2.8.18.tar.gz $ cd redis-2.8.18 $ make 

Выполнение команды займет некоторое время. После завершения просто запустите сервер Redis:

 $ cd redis-2.8.18/src $ ./redis-server 

Чтобы измерить улучшение производительности, мы будем использовать гем «rack-mini-profiler». Этот драгоценный камень поможет нам измерить улучшение производительности прямо из представлений.

Начиная

Для этого примера давайте создадим вымышленный онлайн-магазин для чтения историй. В этом магазине есть книги на разных категориях и языках. Давайте сначала создадим модели:

 # app/models/category.rb class Category include Mongoid::Document include Mongoid::Timestamps include Mongoid::Paranoia include CommonMeta end # app/models/language.rb class Language include Mongoid: :Document include Mongoid::Timestamps include Mongoid::Paranoia include CommonMeta end # app/models/concerns/common_meta.rb module CommonMeta extend ActiveSupport::Concern included do field :name, :type => String field :desc, :type => String field :page_title, :type => String end end 

Я включил исходный файл здесь . Просто скопируйте и вставьте это в ваш seed.rb и запустите задачу rake seed, чтобы сохранить данные в нашей базе данных.

 rake db:seed 

Теперь давайте создадим простую страницу со списком категорий, которая отображает все доступные категории с описанием и тегами.

 # app/controllers/category_controller.rb class CategoryController < ApplicationController include CategoryHelper def index @categories = Category.all end end # app/helpers/category_helper.rb module CategoryHelper def fetch_categories @categories = Category.all end end # app/views/category/index.html.haml %h1 Category Listing %ul#categories - @categories.each do |cat| %li %h3 = cat.name %p = cat.desc # config.routes.rb Rails.application.routes.draw do resources :languages resources :category end 

Когда вы запустите браузер и укажете на /category , вы увидите мини-профилировщик, который измеряет время выполнения каждого действия, выполняемого в бэкэнде. Это должно дать вам четкое представление о том, какие части вашего приложения работают медленно и как их оптимизировать. Эта страница выполнила две команды SQL, и на выполнение запроса ушло около 5 мс.

Хотя поначалу 5 мс могут показаться тривиальными, особенно если рендеринг требует больше времени, в производственном приложении обычно выполняется несколько запросов к базе данных, поэтому они могут значительно замедлить работу приложения.

screen1

Поскольку модели метаданных вряд ли изменятся, часто имеет смысл избегать ненужных обращений к базе данных. Вот где начинается кэширование на более низком уровне.

Инициализировать Redis

Существует Ruby-клиент для Redis, который помогает нам легко подключаться к экземпляру Redis:

 gem 'redis' gem 'redis-namespace' gem 'redis-rails' gem 'redis-rack-cache' 

После того, как эти гемы установлены, проинструктируйте Rails использовать Redis в качестве хранилища кэша:

 # config/application.rb #........... config.cache_store = :redis_store, 'redis://localhost:6379/0/cache', { expires_in: 90.minutes } #......... 

Гем redis-namespace позволяет нам создать красивую оболочку для Redis:

 # config/initializers/redis.rb $redis = Redis::Namespace.new("site_point", :redis => Redis.new) 

Все функциональные возможности Redis теперь доступны во всем приложении через глобальную переменную $ redis. Вот пример того, как получить доступ к значениям на сервере redis (запустить консоль Rails):

 $redis.set("test_key", "Hello World!") 

Эта команда создаст новый ключ с именем «test_key» в Redis со значением «Hello World». Чтобы получить это значение, просто выполните:

 $redis.get("test_key") 

Теперь, когда у нас есть основы, давайте начнем с переписывания наших вспомогательных методов:

 # app/helpers/category_helper.rb module CategoryHelper def fetch_categories categories = $redis.get("categories") if categories.nil? categories = Category.all.to_json $redis.set("categories", categories) end @categories = JSON.load categories end end 

При первом выполнении этого кода в памяти / кеше ничего не будет. Итак, мы просим Rails извлечь его из базы данных и затем отправить его в redis. Обратите внимание на вызов to_json ? При записи объектов в Redis у нас есть несколько вариантов. Один из вариантов — перебирать каждое свойство объекта, а затем сохранять их как хеш, но это медленно. Самый простой способ — сохранить их в виде строки в кодировке JSON. Для декодирования просто используйте JSON.load .

Однако, это приходит с непреднамеренным побочным эффектом. Когда мы получаем значения, простая запись объекта не будет работать. Нам нужно обновить представления, чтобы использовать синтаксис хеша для отображения категорий:

 # app/views/category/index.html.haml %h1 Category Listing %ul#categories - @categories.each do |cat| %li %h3 = cat["name"] %p = cat["desc"] 

Запустите браузер снова и посмотрите, есть ли разница в производительности. В первый раз мы все еще обращаемся к базе данных, но при последующих перезагрузках база данных вообще не используется. Все будущие запросы будут загружены из кеша. Это большая экономия для простого изменения :).

screen2

Управление кешем

Я только что заметил опечатку в одной из категорий. Давайте сначала исправим это:

 $ rails c c = Category.find_by :name => "Famly and Frends" c.name = "Family and Friends" c.save 

Перезагрузите и посмотрите, появляется ли это обновление в представлении:

SCREEN3

Нет, это изменение не отражено в наших взглядах. Это потому, что мы обошли доступ к базе данных и все значения подаются из кеша. Увы, кеш теперь устарел, и обновленные данные не будут доступны до перезапуска Redis. Это прерыватель сделки для любого приложения. Мы можем преодолеть эту проблему путем периодического истечения срока действия кэша:

 # app/helpers/category_helper.rb module CategoryHelper def fetch_categories categories = $redis.get("categories") if categories.nil? categories = Category.all.to_json $redis.set("categories", categories) # Expire the cache, every 3 hours $redis.expire("categories",3.hour.to_i) end @categories = JSON.load categories end end 

Срок действия кэша истекает каждые 3 часа. Хотя это работает для большинства сценариев, данные в кэше теперь будут отставать от базы данных. Это, вероятно, не будет работать для вас. Если вы предпочитаете сохранять кеш свежим, мы можем использовать after_save вызов after_save :

 # app/models/category.rb class Category #........... after_save :clear_cache def clear_cache $redis.del "categories" end #........... end 

Каждый раз, когда модель обновляется, мы поручаем Rails очистить кеш. Это гарантирует, что кэш всегда актуален. Ура!

Вероятно, вы должны использовать что-то вроде cache_observers в производстве, но для краткости мы будем придерживаться after_save здесь. В случае, если вы не уверены, какой подход может работать лучше для вас, эта дискуссия может пролить некоторый свет.

Вывод

Кэширование нижнего уровня очень просто и при правильном использовании очень полезно. Он может мгновенно повысить производительность вашей системы с минимальными усилиями. Все фрагменты кода в этой статье доступны на Github.

Надеюсь, понравилось читать это. Пожалуйста, поделитесь своими мыслями в комментариях.