caching-strategies

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Caching Strategies for Rails 8

Rails 8 缓存策略

Overview

概述

Rails provides multiple caching layers:
  • HTTP caching: ETags and
    fresh_when
    for 304 Not Modified
  • 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和
    fresh_when
    返回304 Not Modified
  • 片段缓存:缓存视图局部模板
  • 俄罗斯套娃缓存:通过
    touch: true
    实现的嵌套缓存片段
  • 底层缓存:使用
    Rails.cache.fetch
    缓存任意数据
  • 集合缓存:高效缓存渲染集合数据
  • Solid Cache:基于数据库的缓存(Rails 8默认选项,无需Redis)

Cache Store Options

缓存存储选项

StoreUse Case
:memory_store
Development
:solid_cache_store
Production (Rails 8 default)
:redis_cache_store
Production (if Redis available)
:null_store
Testing
ruby
undefined
存储类型使用场景
:memory_store
开发环境
:solid_cache_store
生产环境(Rails 8默认)
:redis_cache_store
生产环境(若有Redis可用)
:null_store
测试环境
ruby
undefined

config/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:cache
config.cache_store = :memory_store

在开发环境中启用缓存:
```bash
bin/rails dev:cache

HTTP 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
end

Composite ETags

复合ETag

ruby
def show
  @event = current_account.events.find(params[:id])
  fresh_when [@event, Current.user]
end
ruby
def show
  @event = current_account.events.find(params[:id])
  fresh_when [@event, Current.user]
end

With 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
end
ruby
class Api::EventsController < Api::BaseController
  def show
    @event = current_account.events.find(params[:id])
    if stale?(@event)
      render json: @event
    end
  end
end

Fragment 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: true
:
ruby
undefined
通过
touch: true
实现自动失效的嵌套缓存:
ruby
undefined

app/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,
touch: true
cascades up through
updated_at
timestamps, invalidating all parent caches automatically.
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 %>
当评论更新时,
touch: true
会通过
updated_at
时间戳向上级联,自动失效所有父级缓存。

Collection 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) }
end
ruby
Rails.cache.fetch("stats/#{Date.current}", expires_in: 1.hour) do
  { total_events: Event.count, total_revenue: Order.sum(:total_cents) }
end

In 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
end
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
end

With Race Condition Protection

竞争条件保护

ruby
Rails.cache.fetch([self, "stats"], expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  expensive_operation
end
ruby
Rails.cache.fetch([self, "stats"], expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  expensive_operation
end

Cache Invalidation

缓存失效

Key-Based (Automatic)

基于键的自动失效

Cache keys include
updated_at
, so updates automatically expire old entries.
缓存键包含
updated_at
,因此数据更新会自动过期旧缓存条目。

Touch 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
end
ruby
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
end

Manual Invalidation

手动失效

ruby
class Event < ApplicationRecord
  after_commit :invalidate_caches

  private

  def invalidate_caches
    Rails.cache.delete([self, "statistics"])
    Rails.cache.delete("featured_events")
  end
end
ruby
class Event < ApplicationRecord
  after_commit :invalidate_caches

  private

  def invalidate_caches
    Rails.cache.delete([self, "statistics"])
    Rails.cache.delete("featured_events")
  end
end

Sweeper Pattern

清扫器模式

ruby
class CacheSweeper
  def self.clear_board_caches(board)
    Rails.cache.delete([board, "statistics"])
    Rails.cache.delete([board, "card_distribution"])
  end
end
ruby
class CacheSweeper
  def self.clear_board_caches(board)
    Rails.cache.delete([board, "statistics"])
    Rails.cache.delete([board, "card_distribution"])
  end
end

Counter Caching

计数器缓存

ruby
undefined
ruby
undefined

Migration

迁移文件

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
undefined
event.vendors_count
undefined

Cache Warming

缓存预热

ruby
class CacheWarmerJob < ApplicationJob
  queue_as :low

  def perform(account)
    account.boards.find_each do |board|
      board.statistics
      board.card_distribution
    end
  end
end
ruby
class CacheWarmerJob < ApplicationJob
  queue_as :low

  def perform(account)
    account.boards.find_each do |board|
      board.statistics
      board.card_distribution
    end
  end
end

Testing Caching

缓存测试

ruby
undefined
ruby
undefined

test/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
undefined
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
undefined

Testing Touch Cascade

测试触摸级联

ruby
undefined
ruby
undefined

test/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
end
end end
undefined
require "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
end
end end
undefined

Testing HTTP Caching

测试HTTP缓存

ruby
undefined
ruby
undefined

test/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_modified
end
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 :success
end end
undefined
require "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_modified
end
test "看板更新后返回200" do get board_url(@board) etag = response.headers["ETag"]
@board.touch

get board_url(@board), headers: { "If-None-Match" => etag }
assert_response :success
end end
undefined

Testing Cache Invalidation

测试缓存失效

ruby
undefined
ruby
undefined

test/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
undefined
require "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
undefined

Memoization

记忆化

ruby
class EventPresenter < BasePresenter
  def vendor_count
    @vendor_count ||= event.vendors.count
  end
end
ruby
class EventPresenter < BasePresenter
  def vendor_count
    @vendor_count ||= event.vendors.count
  end
end

Checklist

检查清单

  • Cache store configured for environment
  • fresh_when
    on show/index actions
  • touch: true
    on belongs_to for Russian doll
  • 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
    实现集合缓存
  • 已为复杂查询添加底层缓存
  • 已定义缓存失效策略
  • 已为统计计数添加计数器缓存
  • 已配置缓存预热任务应对冷启动
  • 所有测试均通过