caching-strategies
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCaching Strategies for Rails 8
Rails 8 缓存策略
Overview
概述
Rails provides multiple caching layers:
- HTTP caching: ETags and for 304 Not Modified
fresh_when - Fragment caching: Cache view partials
- Russian doll caching: Nested cache fragments with
touch: true - Low-level caching: Cache arbitrary data with
Rails.cache.fetch - Collection caching: Efficient cached rendering of collections
- Solid Cache: Database-backed caching (Rails 8 default, no Redis)
Rails提供多层缓存机制:
- HTTP缓存:使用ETags和返回304 Not Modified
fresh_when - 片段缓存:缓存视图局部模板
- 俄罗斯套娃缓存:通过实现的嵌套缓存片段
touch: true - 底层缓存:使用缓存任意数据
Rails.cache.fetch - 集合缓存:高效缓存渲染集合数据
- Solid Cache:基于数据库的缓存(Rails 8默认选项,无需Redis)
Cache Store Options
缓存存储选项
| Store | Use Case |
|---|---|
| Development |
| Production (Rails 8 default) |
| Production (if Redis available) |
| Testing |
ruby
undefined| 存储类型 | 使用场景 |
|---|---|
| 开发环境 |
| 生产环境(Rails 8默认) |
| 生产环境(若有Redis可用) |
| 测试环境 |
ruby
undefinedconfig/environments/production.rb
config/environments/production.rb
config.cache_store = :solid_cache_store
config.cache_store = :solid_cache_store
config/environments/development.rb
config/environments/development.rb
config.cache_store = :memory_store
Enable caching in development:
```bash
bin/rails dev:cacheconfig.cache_store = :memory_store
在开发环境中启用缓存:
```bash
bin/rails dev:cacheHTTP Caching (ETags / fresh_when)
HTTP缓存(ETags / fresh_when)
Use conditional GET to send 304 Not Modified when content has not changed.
ruby
class EventsController < ApplicationController
def show
@event = current_account.events.find(params[:id])
fresh_when @event
end
def index
@events = current_account.events.recent
fresh_when @events
end
end使用条件GET请求,当内容未变更时返回304 Not Modified。
ruby
class EventsController < ApplicationController
def show
@event = current_account.events.find(params[:id])
fresh_when @event
end
def index
@events = current_account.events.recent
fresh_when @events
end
endComposite ETags
复合ETag
ruby
def show
@event = current_account.events.find(params[:id])
fresh_when [@event, Current.user]
endruby
def show
@event = current_account.events.find(params[:id])
fresh_when [@event, Current.user]
endWith stale? for JSON
结合stale?处理JSON请求
ruby
class Api::EventsController < Api::BaseController
def show
@event = current_account.events.find(params[:id])
if stale?(@event)
render json: @event
end
end
endruby
class Api::EventsController < Api::BaseController
def show
@event = current_account.events.find(params[:id])
if stale?(@event)
render json: @event
end
end
endFragment Caching
片段缓存
erb
<%# app/views/events/_event.html.erb %>
<% cache event do %>
<article class="event-card">
<h3><%= event.name %></h3>
<p><%= event.description %></p>
<time><%= l(event.event_date, format: :long) %></time>
</article>
<% end %>erb
<%# app/views/events/_event.html.erb %>
<% cache event do %>
<article class="event-card">
<h3><%= event.name %></h3>
<p><%= event.description %></p>
<time><%= l(event.event_date, format: :long) %></time>
</article>
<% end %>Custom Cache Keys
自定义缓存键
erb
<% cache [event, "v2"] do %>
...
<% end %>
<% cache [event, current_user] do %>
...
<% end %>erb
<% cache [event, "v2"] do %>
...
<% end %>
<% cache [event, current_user] do %>
...
<% end %>Russian Doll Caching
俄罗斯套娃缓存
Nested caches with automatic invalidation through :
touch: trueruby
undefined通过实现自动失效的嵌套缓存:
touch: trueruby
undefinedapp/models/comment.rb
app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :event, touch: true
end
```erb
<% cache @event do %>
<h1><%= @event.name %></h1>
<% @event.comments.each do |comment| %>
<% cache comment do %>
<%= render comment %>
<% end %>
<% end %>
<% end %>When a comment is updated, cascades up through timestamps, invalidating all parent caches automatically.
touch: trueupdated_atclass Comment < ApplicationRecord
belongs_to :event, touch: true
end
```erb
<% cache @event do %>
<h1><%= @event.name %></h1>
<% @event.comments.each do |comment| %>
<% cache comment do %>
<%= render comment %>
<% end %>
<% end %>
<% end %>当评论更新时,会通过时间戳向上级联,自动失效所有父级缓存。
touch: trueupdated_atCollection Caching
集合缓存
erb
<%# Caches each item individually, multi-read from cache store %>
<%= render partial: "events/event", collection: @events, cached: true %>erb
<%# 单独缓存每个条目,从缓存存储中批量读取 %>
<%= render partial: "events/event", collection: @events, cached: true %>Low-Level Caching
底层缓存
ruby
Rails.cache.fetch("stats/#{Date.current}", expires_in: 1.hour) do
{ total_events: Event.count, total_revenue: Order.sum(:total_cents) }
endruby
Rails.cache.fetch("stats/#{Date.current}", expires_in: 1.hour) do
{ total_events: Event.count, total_revenue: Order.sum(:total_cents) }
endIn Models
在模型中使用
ruby
class Board < ApplicationRecord
def statistics
Rails.cache.fetch([self, "statistics"], expires_in: 1.hour) do
{
total_cards: cards.count,
completed_cards: cards.joins(:closure).count,
total_comments: cards.joins(:comments).count
}
end
end
endruby
class Board < ApplicationRecord
def statistics
Rails.cache.fetch([self, "statistics"], expires_in: 1.hour) do
{
total_cards: cards.count,
completed_cards: cards.joins(:closure).count,
total_comments: cards.joins(:comments).count
}
end
end
endWith Race Condition Protection
竞争条件保护
ruby
Rails.cache.fetch([self, "stats"], expires_in: 1.hour, race_condition_ttl: 10.seconds) do
expensive_operation
endruby
Rails.cache.fetch([self, "stats"], expires_in: 1.hour, race_condition_ttl: 10.seconds) do
expensive_operation
endCache Invalidation
缓存失效
Key-Based (Automatic)
基于键的自动失效
Cache keys include , so updates automatically expire old entries.
updated_at缓存键包含,因此数据更新会自动过期旧缓存条目。
updated_atTouch Cascade
触摸级联
ruby
class Card < ApplicationRecord
belongs_to :board, touch: true # Updates board.updated_at
end
class Comment < ApplicationRecord
belongs_to :card, touch: true # Updates card.updated_at -> board.updated_at
endruby
class Card < ApplicationRecord
belongs_to :board, touch: true # 更新board.updated_at
end
class Comment < ApplicationRecord
belongs_to :card, touch: true # 更新card.updated_at -> board.updated_at
endManual Invalidation
手动失效
ruby
class Event < ApplicationRecord
after_commit :invalidate_caches
private
def invalidate_caches
Rails.cache.delete([self, "statistics"])
Rails.cache.delete("featured_events")
end
endruby
class Event < ApplicationRecord
after_commit :invalidate_caches
private
def invalidate_caches
Rails.cache.delete([self, "statistics"])
Rails.cache.delete("featured_events")
end
endSweeper Pattern
清扫器模式
ruby
class CacheSweeper
def self.clear_board_caches(board)
Rails.cache.delete([board, "statistics"])
Rails.cache.delete([board, "card_distribution"])
end
endruby
class CacheSweeper
def self.clear_board_caches(board)
Rails.cache.delete([board, "statistics"])
Rails.cache.delete([board, "card_distribution"])
end
endCounter Caching
计数器缓存
ruby
undefinedruby
undefinedMigration
迁移文件
add_column :events, :vendors_count, :integer, default: 0, null: false
add_column :events, :vendors_count, :integer, default: 0, null: false
Model
模型
class Vendor < ApplicationRecord
belongs_to :event, counter_cache: true
end
class Vendor < ApplicationRecord
belongs_to :event, counter_cache: true
end
Usage (no query needed)
使用方式(无需查询数据库)
event.vendors_count
undefinedevent.vendors_count
undefinedCache Warming
缓存预热
ruby
class CacheWarmerJob < ApplicationJob
queue_as :low
def perform(account)
account.boards.find_each do |board|
board.statistics
board.card_distribution
end
end
endruby
class CacheWarmerJob < ApplicationJob
queue_as :low
def perform(account)
account.boards.find_each do |board|
board.statistics
board.card_distribution
end
end
endTesting Caching
缓存测试
ruby
undefinedruby
undefinedtest/test_helper.rb (enable caching for specific tests)
test/test_helper.rb(为特定测试启用缓存)
class ActiveSupport::TestCase
def with_caching(&block)
caching = ActionController::Base.perform_caching
ActionController::Base.perform_caching = true
Rails.cache.clear
yield
ensure
ActionController::Base.perform_caching = caching
end
end
undefinedclass ActiveSupport::TestCase
def with_caching(&block)
caching = ActionController::Base.perform_caching
ActionController::Base.perform_caching = true
Rails.cache.clear
yield
ensure
ActionController::Base.perform_caching = caching
end
end
undefinedTesting Touch Cascade
测试触摸级联
ruby
undefinedruby
undefinedtest/models/card_test.rb
test/models/card_test.rb
require "test_helper"
class CardCachingTest < ActiveSupport::TestCase
test "touching card updates board updated_at" do
board = boards(:one)
card = cards(:one)
assert_changes -> { board.reload.updated_at } do
card.touch
endend
end
undefinedrequire "test_helper"
class CardCachingTest < ActiveSupport::TestCase
test "触摸卡片会更新看板的updated_at" do
board = boards(:one)
card = cards(:one)
assert_changes -> { board.reload.updated_at } do
card.touch
endend
end
undefinedTesting HTTP Caching
测试HTTP缓存
ruby
undefinedruby
undefinedtest/controllers/boards_controller_test.rb
test/controllers/boards_controller_test.rb
require "test_helper"
class BoardsControllerCachingTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:one)
@board = boards(:one)
end
test "returns 304 when board unchanged" do
get board_url(@board)
assert_response :success
etag = response.headers["ETag"]
get board_url(@board), headers: { "If-None-Match" => etag }
assert_response :not_modifiedend
test "returns 200 when board updated" do
get board_url(@board)
etag = response.headers["ETag"]
@board.touch
get board_url(@board), headers: { "If-None-Match" => etag }
assert_response :successend
end
undefinedrequire "test_helper"
class BoardsControllerCachingTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:one)
@board = boards(:one)
end
test "看板未变更时返回304" do
get board_url(@board)
assert_response :success
etag = response.headers["ETag"]
get board_url(@board), headers: { "If-None-Match" => etag }
assert_response :not_modifiedend
test "看板更新后返回200" do
get board_url(@board)
etag = response.headers["ETag"]
@board.touch
get board_url(@board), headers: { "If-None-Match" => etag }
assert_response :successend
end
undefinedTesting Cache Invalidation
测试缓存失效
ruby
undefinedruby
undefinedtest/models/board_test.rb
test/models/board_test.rb
require "test_helper"
class BoardCacheInvalidationTest < ActiveSupport::TestCase
test "statistics cache is cleared after card update" do
board = boards(:one)
card = cards(:one)
board.statistics # Warm cache
card.update!(title: "New title")
assert_nil Rails.cache.read([board, "statistics"])end
end
undefinedrequire "test_helper"
class BoardCacheInvalidationTest < ActiveSupport::TestCase
test "卡片更新后统计缓存被清除" do
board = boards(:one)
card = cards(:one)
board.statistics # 预热缓存
card.update!(title: "New title")
assert_nil Rails.cache.read([board, "statistics"])end
end
undefinedMemoization
记忆化
ruby
class EventPresenter < BasePresenter
def vendor_count
@vendor_count ||= event.vendors.count
end
endruby
class EventPresenter < BasePresenter
def vendor_count
@vendor_count ||= event.vendors.count
end
endChecklist
检查清单
- Cache store configured for environment
- on show/index actions
fresh_when - on belongs_to for Russian doll
touch: true - Collection caching with
cached: true - Low-level caching for expensive queries
- Cache invalidation strategy defined
- Counter caches for counts
- Cache warming jobs for cold starts
- All tests GREEN
- 已为对应环境配置缓存存储
- 已在show/index动作中使用
fresh_when - 已在belongs_to关联中添加以支持俄罗斯套娃缓存
touch: true - 已使用实现集合缓存
cached: true - 已为复杂查询添加底层缓存
- 已定义缓存失效策略
- 已为统计计数添加计数器缓存
- 已配置缓存预热任务应对冷启动
- 所有测试均通过