[Rails]キャッシュについて
はじめに
Railsにはキャッシュを管理するための機能がいくつ組み込まれています。
初期設定と例を使ってキャッシュの導入についてまとめてみました。
環境
Rails 7.0.7
ruby 3.2.1
キャッシュとは
「キャッシュ(caching)」とは、リクエスト・レスポンスのサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。
Railsのキャッシュ
- フラグメントキャッシュ (Fragment Caching):
フラグメントキャッシュは、ビューファイル内の特定の部分をキャッシュするための機能です。例えば、特定のビュー部分(部分テンプレート)が頻繁に変更されない場合、それをキャッシュして再利用することで、ページのレンダリング時間を短縮できます。cache
ヘルパーメソッドを使用してキャッシュを定義します。
<% @products.each do |product| %>
<% cache product do %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
<% end %>
- ページキャッシュ (Page Caching):
ページキャッシュは完全なページをHTMLファイルとして保存し、再リクエスト時にそれを提供することで高速化を実現します。caches_page
メソッドを使用して特定のコントローラーアクションのページキャッシュを有効にできます。
class ProductsController < ApplicationController
caches_page :show
end
ページキャッシュ機能は、Rails 4で本体から削除されてgem化されました。
actionpack-page_caching
gemを参照してください。
- アクションキャッシュ
ページキャッシュは、
before_filter
のあるアクション(認証の必要なページなど)には適用できません。アクションキャッシュは、このような場合に使います。アクションキャッシュの動作は、ページキャッシュと似ていますが、WebサーバーへのリクエストがRailsスタックに到達したときに、
before_filter
を実行してからキャッシュを配信する点が異なります。これによって、認証などの制限をかけながらキャッシュを配信できるようになります。
アクションキャッシュ機能は、Rails 4から削除されました。詳しくは
actionpack-action_caching
gemを参照してください。
- 低レベルキャッシュ API:
Railsは低レベルなキャッシュAPIも提供しており、Railsキャッシュストアを直接操作できます。
サポートされているキャッシュストアには、メモリ、Memcached、Redisなどがあります。
商品の平均価格を一時間キャッシュされ、リクエストごとに計算されることなく、データベースへの負荷を減らし、パフォーマンスを向上させることができます。
ただし、キャッシュが見つからない場合、ブロック内のコードが実行され、その結果がキャッシュに保存されます。
class Product < ApplicationRecord
def self.average_price
Rails.cache.fetch("products/average_price", expires_in: 1.hour) do
average(:price)
end
end
end
キャッシュタイプ | メリット | デメリット | 使用シーン |
---|---|---|---|
フラグメントキャッシュ | - 特定のビュー部分の高速化 - 変更頻度の低いコンテンツをキャッシュ可能 |
- キャッシュキーの管理が必要 - キャッシュの有効期限の管理が必要 |
- 個々のビュー部分のレンダリングがコストが高い場合 - 頻繁に変更されないコンテンツを表示する場合 |
ページキャッシュ | - 完全なHTMLページの高速化 - シンプルな設定 |
- 動的なコンテンツには適さない - ユーザー固有の情報を含むページには適さない |
- 変更頻度の低いページや静的なコンテンツを表示する場合 - ログインなどの承認が不要なページ |
低レベルキャッシュ | - カスタムのキャッシュロジックを実装可能 | - キャッシュの設定と管理が複雑 - キャッシュストアの選択が必要 |
- より高度なキャッシュ制御が必要な場合 - キャッシュの有効期限をカスタマイズしたい場合 |
アクションキャッシュ | - 特定のコントローラーアクションの高速化 | - 承認が必要なコンテンツには適さない - キャッシュのキーの設計が重要 |
- 特定のコントローラーアクションの出力をキャッシュしたい場合 - ユーザーに依存しないコンテンツの高速化が必要な場合 |
ページ内の一部のコンテンツをキャッシュするために使用されるフラグメントキャッシュについてもう少しみていきます。
cache
ヘルパーメソッド
cache(name = {}, options = {}, &block)
特定の部分テンプレートをコレクション内の各要素に対して個別にキャッシュします。
各要素の変更は他の要素には影響しません。
<%= render partial: 'projects/project', collection: @projects, cached: true %>
指定された変数(project
)に関連付けられたビュー全体をキャッシュします。変数が変更されるたびにキャッシュが無効になり、全体のビューが再描画される必要があります。
<% cache [ project, current_user ] do %>
<%= render @projects %>
<% end %>
生成されたキャッシュ:DBからレコードを取得するより、キャッシュキーを通してコレクションを取得しています。
views/template/action:7a1156131a6928cb0026877f8b749ac9/projects/123
^template path ^template tree digest ^class ^id
パーシャルに変更があるう場合キャッシュが無効になります。
skip_digest: true
はディジェスト(ファイルの内容に基づいたキャッシュの更新のトリガー)を無効にするオプションです
<% cache project, skip_digest: true do %>
また、キャッシュの有効期限を指定することもできます。
<% @products.each do |product| %>
<% cache product, expires_in: 1.hour do %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
<% end %>
ロシアンドールキャッシュ
フラグメントキャッシュの中に更にフラグメントキャッシュをネストしたい時に使います。
つまり、ビュー全体がキャッシュされ、ビュー内の特定の要素もまたキャッシュされることになります。これにより、要素ごとに独立したキャッシュが行われ、要素の更新時に必要な部分のみ再キャッシュできます。
カテゴリ、サブカテゴリ、その中の商品をそれぞれキャッシュした場合、他のカテゴリーのキャッシュに影響を与えないで一つのサブカテゴリに更新を行うことができます。
<% @categories.each do |category| %>
<% cache category do %>
<h2><%= category.name %></h2>
<% category.subcategories.each do |subcategory| %>
<% cache subcategory do %>
<h3><%= subcategory.name %></h3>
<% subcategory.products.each do |product| %>
<% cache product do %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Railsはcreated_at
, column_at
カラムのデータを使用しキャッシュを管理します。
touch: true
商品が更新されたら、商品が所属するカテゴリ(Category
)も自動的に更新される場合touch: true
オプションをつけて親オブジェクトのキャッシュも無効にします。。
touch: true
は、Active Recordモデルにおいて、関連付けられたオブジェクトが更新された際に、親オブジェクト(belongs_to
で関連付けられたオブジェクト)も自動的に更新(タッチ)するためのオプションです。
関連付けの設定に追加します:
class Product < ApplicationRecord
belongs_to :category, touch: true
end
class Category < ApplicationRecord
has_many :products
end
キャッシュストア
キャッシュストアは、アプリケーションがデータを一時的に保存し、後で高速にアクセスできるようにするための仕組みやデータストレージのことです。
Railsで使われる主要なキャッシュストア4種類を紹介します。
-
メモリストア(Memory Store):
メモリストアは、データをメモリ内にキャッシュする方法です。Railsではconfig.cache_store
を設定して使用できます。
# config/environments/development.rb
config.cache_store = :memory_store
データベースからのクエリ結果をメモリにキャッシュします:
# クエリ結果をキャッシュ
@products = Rails.cache.fetch("all_products", expires_in: 1.hour) do
Product.all
end
-
ディスクストア(Disk Store):
ディスクストアは、データをディスク上のファイルにキャッシュする方法です。ファイルは永続的で、再起動後もデータが保持されます。
# config/environments/development.rb
config.cache_store = :file_store, "/path/to/cache/directory"
画像のリサイズ済みバージョンをディスクにキャッシュすることができます。
# 画像のリサイズ済みバージョンをキャッシュ
resized_image = Rails.cache.fetch("resized_image_#{image.id}", expires_in: 1.day) do
image.resize_to_fit(200, 200)
end
-
Redis:
Redisは、高速で分散キャッシュを提供するインメモリデータベースです。RailsではRedisをconfig.cache_store
に設定して使用できます。
# config/environments/development.rb
config.cache_store = :redis_cache_store, { url: "redis://localhost:6379/0" }
例えば、ユーザーセッション情報をRedisにキャッシュすることができます。セッションデータは高速にアクセスできます。
# ユーザーセッション情報をRedisにキャッシュ
session_data = Rails.cache.fetch("user_session_#{self.id}", expires_in: 1.hour) do
user_id: self.id,
username: self.username
...
end
-
Memcached:
Memcachedも高速な分散キャッシュストアで、Railsで使用することができます。config.cache_store
に設定します。
# config/environments/development.rb
config.cache_store = :mem_cache_store, "localhost"
例えば、あるページのHTMLレスポンスをMemcachedにキャッシュして、負荷を軽減することができます。
# あるページのHTMLレスポンスをMemcachedにキャッシュ
cached_html = Rails.cache.fetch("page_#{page_id}", expires_in: 1.hour) do
render_to_string(template: "pages/page")
end
キャッシュ前後のパフォーマンスを比較する
キャッシュされる前のTodos一覧のレコードを取得してビューを描画するまでの時間を計測してみます。
benchmark.real
は計測結果の実際の時間を取得し、それをミリ秒に変換しています。
realは実行時間(CPU時間ではなく実際の時間)を表します。
class TodosController < ApplicationController
def index
benchmark = Benchmark.measure do
@todos = Todo.all
render
end
logger.info "Benchmark results: #{benchmark.real * 1000}ms."
end
end
ログの出力:
16:05:20 web.1 | Benchmark results: 182.16400011442602ms.
16:05:20 web.1 | Completed 200 OK in 184ms (Views: 173.6ms | ActiveRecord: 8.6ms | Allocation
キャッシュを有効にする
開発環境ではデフォルトでキャッシュがOFF
になってます。
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
キャッシュをON
にする:
bin/rails dev:cache
Development mode is now being cached.
tmp/
ディレクトリ内にこれらのファイルがあることを確認します。
キャッシュディレクトリ
デフォルトでは#{root}/tmp/cache/
内に作成されます。
ディレクトリを変えたい場合設定することができます。
config.cache_store = :file_store, "/path/to/cache/directory"
パーシャルをキャッシュする
<% cache @todos do %>
<%= render @todos %>
<% end %>
ページリロードしキャッシュされたことを確認します。
フラグメントキャッシュからtodos
一覧を取得しています。
スピードもDBから取得するより早いですね。
16:28:26 web.1 | Write fragment views/todos/index:19cbc8c49d7957d857632bf2d21d5760/todos/query-725ae7453acd1711aadcb2397fc190b9-100-20230911063354506083 (0.1ms)
16:28:26 web.1 | Rendered todos/index.html.erb within layouts/application (Duration: 132.4ms | Allocations: 109570)
16:30:32 web.1 | Benchmark results: 24.107000092044473ms.
16:30:32 web.1 | Completed 200 OK in 28ms (Views: 23.6ms | ActiveRecord: 0.6ms | Allocations: 16963)
コレクションをキャッシュする
<%= render partial: "todos/todo", collection: @todos %>
キャッシュ前のロードタイム:
16:33:31 web.1 | Benchmark results: 154.8949999269098ms.
16:33:31 web.1 | Completed 200 OK in 158ms (Views: 148.5ms | ActiveRecord: 6.4ms | Allocations: 115129)
cached: true
を追加します。
<%= render partial: "todos/todo",
collection: @todos,
+ cached: true %>
キャッシュからコレクションをレンダリングして表示していることを確認します。
16:37:39 web.1 | Rendered collection of todos/_todo.html.erb [100 / 100 cache hits] (Duration: 3.8ms | Allocations: 3235)
16:37:39 web.1 | Benchmark results: 23.6200001090765ms.
16:37:39 web.1 | Completed 200 OK in 27ms (Views: 22.8ms | ActiveRecord: 0.9ms | Allocations: 21173)
終わりに
キャッシュを導入することでパーフォマンスの向上を実感しました!
これからもキャッシュを使っていきたいと思います。
Discussion