鼬~~哩賀,我是寫程式的山姆老弟,昨天跟大家一起看了 Rails 的 Rack,今天來看 RailsGuide 的 Caching 篇,看 Rails 是怎麼處理 暫存(Cache
)的,夠夠~
Cache 可以用在各種場景,像是同樣或類似的 request,不需要每一次都重新計算、重新去資料庫拿一樣的資料、重新把一樣的資料放到一樣的頁面上,這樣的情況就很適合直接暫存起來,下次遇到一樣的 request,就直接從暫存中拿出來即可,省了 CPU 成本、資料庫成本,還加快了反應時間
*There are only two hard things in Computer Science: cache invalidation and naming things
actionpack-page_caching
這個 gemconfig.action_controller.perform_caching = true
or $ rails dev:cache
before_action :authenticate_user
)before_action
) 正常運作actionpack-action_caching
這個 gemconfig.action_controller.perform_caching = true
or $ rails dev:cache
ps. Page Caching
和 Action Caching
從 Rails 4 開始,就被拿掉了,變成需要額外安裝的 gem
Cache Partial
使用方式: e.g. 產生 Product scaffold 的時候,會有 _product.html.erb
的 partial
方法一:cache block
<% @products.each do |product| %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
方法二:cache_if
, cache_unless
<% cache_if admin?, product do %>
<%= render product %>
<% end %>
<% cache_unless admin?, product do %>
<%= render product %>
<% end %>
過期時機:partial 內的 HTML 內容改變時
Cache 巢狀(nest) fragments,也就是 partial 包 partial,可能會有很多層
使用方法:
Product 包 Games,Cache Product 的同時
<% cache product do %>
<%= render product.games %>
<% end %>
也 Cache 每一個 Game
<% cache game do %>
<%= render game %>
<% end %>
這時如果 Game 改變,Product 並不會知道需要更新 Cache,這時候需要在 dependency 加上 touch,讓 Game 改變的時候,也去更新 Product 的 updated_at,這樣 Product 的 Cache 也會被更新
class Product < ApplicationRecord
has_many :games
end
class Game < ApplicationRecord
belongs_to :product, touch: true
end
方法一:不管 respond_to 是 HTML 或 Javascript,都可以吃得到 cache
render(partial: 'hotels/hotel', collection: @hotels, cached: true)
方法二:強制指定,不管 respond_to 是什麼 format,就是直接回傳 html 格式的
render(partial: 'hotels/hotel.html.erb', collection: @hotels, cached: true)
這段我看不太懂,大家看看就好 ?
隱性(Implicit) dependencies:
ActionView::Digestor
會自動判斷以下要 render 的是一樣的
render partial: 'comments/comment'
render 'comments/comment'
render "comments/comment"
render('comments/comment')
顯性(Explicit) dependencies
有時候沒辦法直接指定 partial
<%= render_sortable_todolists @project.todolists %>
這時候就要手動加這個特殊的註解格式 (?
<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>
外部(External) dependencies
這邊直接放棄,我看不懂QQ
<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>
方法一:帶入值
Rails.cache.fetch('some_key', 'your_value')
方法二:帶入 Block,會取 Block 最後 return 的 value 出來暫存(以這個例子,暫存就不會每呼叫一次就發一次 API)
class Product < ApplicationRecord
def competing_price
Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
Competitor::API.find_price(id)
end
end
end
其他詳細使用方法,可以看下面的 Cache Stores
章節
不要 Cache ActiveRecord instance
,改存 ActiveRecord 的 id
# super_admins is an expensive SQL query, so don't run it too often
Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do
User.super_admins.to_a # 這樣不好
end
# super_admins is an expensive SQL query, so don't run it too often
ids = Rails.cache.fetch("super_admin_user_ids", expires_in: 12.hours) do
User.super_admins.pluck(:id) # 這樣好
end
User.where(id: ids).to_a # 事後再 SQL 拿出來
重複的 SQL query 會被 cache
設定方法:不需特別設定
使用方法:
class ProductsController < ApplicationController
def index
# Run a find query
@products = Product.all
# ...
# Run the same query again
@products = Product.all
end
end
Caching 的時機:起始點是從 Controller action 開始,當 action 結束,cache 就會被清除
Rails 提供 Cache Stores
這個暫存的「統一介面」,讓我們可以抽換掉最底層的暫存機制,只需要用統一的介面就可以使用暫存,跟 ActiveStorage
、ActiveJob
的概念很像,Rails 就是提供同一介面,可抽換底層
設定方法:
可選擇的暫存方式有:
memory_store
:暫存在記憶體裡,不能跨 process 取用
# config/application.rb or config/environemnts/*.rb
config.cache_store = :memory_store, { size: 64.megabytes }
file_store
:暫存在檔案裡
# config/application.rb or config/environemnts/*.rb
config.cache_store = :file_store, "/path/to/cache/directory"
mem_cache_store
:使用 MemCached 暫存,預設使用 dalli
gem,還可以透過 MEMCACHE_SERVERS
環境變數來指定 Cache Server 的 host,如果都沒有指定,那就預設會找 127.0.0.1
的 11211
port
# config/application.rb or config/environemnts/*.rb
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
redis_cache_store
:使用 Redis 暫存,使用 redis
gem 或 hiredis
gem
# config/application.rb or config/environemnts/*.rb
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
# Production 的設定會長得像這樣
cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,
connect_timeout: 30, # Defaults to 20 seconds
read_timeout: 0.2, # Defaults to 1 second
write_timeout: 0.2, # Defaults to 1 second
reconnect_attempts: 1, # Defaults to 0
error_handler: -> (method:, returning:, exception:) {
# Report errors to Sentry as warnings
Raven.capture_exception exception, level: 'warning',
tags: { method: method, returning: returning }
}
}
null_store
:只有在每個 request 之前暫存,request 結束後就會清除,適合用d evelopment 和 test 環境
# config/application.rb or config/environemnts/*.rb
config.cache_store = :null_store
其他 custom store:只要繼承 ActiveSupport::Cache::Store
即可
# config/application.rb or config/environemnts/*.rb
config.cache_store = MyCacheStore.new
使用方法:
Rails.cache.read(key)
Rails.cache.write(key, value)
Rails.cache.delete(key)
Rails.cache.exist?(key)
Rails.cache.fetch(key, value)
= write + readCache 真的是一門很深的學問,這篇看下來,會好奇前面的 Page Caching
, Action Caching
和 Fragment Caching
是不是過時了?因為現在的 Rails 前端,如果改接 React 或 Vue,這樣的 Caching 機制就失效了
不過 Rails 就是提供一些工具,給開發者自己去決定怎麼使用,我們還是可以針對一些重大的效能瓶頸去使用 low-level caching,那今天就先這樣囉,我們明天見~