Rails ActionCacheを導入する
Webアプリケーションのパフォーマンス改善として、RailsのActionCacheを導入しました。
Railsのキャッシュに関する概要はRailsGuidesが一番わかりやすかったです。
「キャッシュ(caching)」とは、リクエスト・レスポンスのサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。
キャッシュの手法は 3つ
- ページキャッシュ (ページ単位のキャッシュ)
- アクションキャッシュ (アクション単位のキャッシュ)
- フラグメントキャッシュ (部品単位のキャッシュ)
今回紹介するのは「アクションキャッシュ」です。
アクションキャッシュの機能はRails3までは標準で機能として存在していましたが、Rails4からはGemとして切り出されて独立して存在しています。
Rails4以降で開発している場合はGemfileに記述しinstallします。
gem "actionpack-action_caching"
bundle install
class BooksController caches_action :index def index @book = Book.find(1) end end
これでキャッシュを導入できます。 初回アクセス時はindexメソッドが実行されますが、2回目以降は初回アクセス時にレンダリングされた結果が返ってきます。
キャッシュがなかった場合
- キャッシュの存在をチェックする
- indexアクションを実行
- キャッシュを生成
キャッシュがあった場合
- キャッシュの存在チェック
- キャッシュを表示
キャッシュはアクションよりも先に実行されるため、コールバックで値を取得しておく必要がある。
class BooksController before_action :set_book, only: :index caches_action :index def index end private def set_book @book = Book.find(params[:id]) end end
表示される内容が変更される場合には少し工夫が必要です。
内容が更新されたにもかかわらず更新前に保存されたキャッシュが効いてしまい、最新の内容を表示することができない。という状況を想定します
その場合には、キャッシュする情報に更新情報を渡すことによって解決します。
class BooksController caches_action :index, cache_path: ->(c) { build_path(c, @book) } def index end private def build_path(controller, model: nil) cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller).path cache_path = "#{base_path}:#{model.update_at.try(:strftime, "%Y%m%d-%H%M%S")}" if model.present? cache_path end end
@bookの値が更新されupdated_atが変更されていればcache_pathは新しくなりキャッシュ自体も変更され最新のキャッシュが呼び出されるようになるため、内容が変更されていてもキャッシュも更新されるます。
開発環境でキャッシュを有効にし、機能しているか確認する場合は以下を config/environments/*.rb に記述
config.action_controller.perform_caching = true
キャッシュを有効にし、キャッシュを効かせたいメソッドを呼び出せばキャッシュが効いていることをログで確認することができる。
その際に生成されるキャッシュファイルは、tmp/caches以下に生成されていきます。
キャッシュをexpireするオプションがあります。
expires_in: 30.minutes
とすることで30ごとにexpireされます。
class BooksController before_action :set_book, only: :index caches_action :index, cache_path: ->(c) { build_path(c, @book) }, expires_in: 30.minutes def index end private def set_book end def build_path(controller, model: nil) cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller).path cache_path = "#{base_path}:#{model.update_at.try(:strftime, "%Y%m%d-%H%M%S")}" if model.present? cache_path end end
このままでも動作しますが、挙動としてファイルキャッシュになります。(tmp/cache以下にキャッシュ用のファイルが生成される)
複数サーバーでアプリケーションを運用する場合は現状のままだと各アプリケーションでキャッシュが行われてしまいAP1でキャッシュされているが、AP2ではキャッシュされていないのでファイルを生成する。など無駄な挙動が増えパフォーマンスに統一性が持てません。 この問題の解決としてキャッシュサーバとしてredisを用いて複数サーバー運用でも良い感じに挙動する仕組みに変更します。
Railsからredisを使うためredis-railsを導入します。
gem 'redis-rails'
bundle install
config.cache_store = :redis_store, "redis://localhost:3001/0/cache" # redisの参照先はそれぞれの環境に合わせて変更する 上記は例
caches_action側のexpires_in:
でexpireの期間を定義しているためapplication.rb側への定義は不要です。
なかなか早くなるので良いですね。