Rails 的 cache 统一入口是 Rails.cache。通常会在 environments 里进行配置,配置方式为:config.cache_store = :null_store.
在 Rails 的 bootstrap 中,将 config.cache_store 赋值给 Rails.cache,从而让 Rails.cache 变成一个全局统一的入口。
Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store)
autoload :FileStore, "active_support/cache/file_store"
autoload :MemoryStore, "active_support/cache/memory_store”
autoload :MemCacheStore, "active_support/cache/mem_cache_store"
autoload :NullStore, "active_support/cache/null_store”
ActiveSupport::Cache.lookup_store 的参数如果是 nil
If no arguments are passed to this method, then a new ActiveSupport::Cache::MemoryStore object will be returned.
所以默认的存储系统是 MemoryStore。ActiveSupport::Cache.lookup_store 接受自定义的存储对象。
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
# => returns MyOwnCacheStore.new
ActiveSupport::Cache::Store 是四个存储类型的基类,提供了统一的 API: +fetch+, +write+, +read+, +exist?+, and +delete+.
需要特别强调 fetch 的:race_condition_ttl 参数,延长了过期数据的过期时间,避免死锁发生,但我觉得也是鸡肋而已。
# Writes the value to the cache, with the key.
#
# Options are passed to the underlying cache implementation.
def write(name, value, options = nil)
options = merged_options(options)
instrument(:write, name, options) do
entry = Entry.new(value, options)
write_entry(normalize_key(name, options), entry, options)
end
end
每次缓存的数据,都被包装成了 Entry 对象。这样就可以在过期的情况下,从 Time.now 开始,再续命 race_condition_ttl 时间
if entry && entry.expired?
race_ttl = options[:race_condition_ttl].to_i
if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
# When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
# for a brief period while the entry is being recalculated.
entry.expires_at = Time.now + race_ttl
write_entry(key, entry, expires_in: race_ttl * 2)
else
delete_entry(key, options)
end
entry = nil
end
write_entry(key, entry, expires_in: race_ttl * 2) 看了看 write_entry 方法,似乎对 expires_in 没做任何处理,所以,估计还是只是续了 race_condition_ttl 一倍的命
FileStore 有 ActiveSupport::Cache::Strategy::LocalCache 配合本地使用缓存
rails 在内存中缓存了每次 sql 查询的结果,那么在同一次经过 ActionPack 时,相同的 sql 会命中缓存,而提高性能 ActiveRecord::Core 中的方法。
def cached_find_by_statement(key, &block) # :nodoc:
cache = @find_by_statement_cache[connection.prepared_statements]
cache[key] || cache.synchronize {
cache[key] ||= StatementCache.create(connection, &block)
}
end
就 find 查询而言,缓存的是一个关于 sql 语句的抽象二叉树,而不是所谓的查询结果。
在 rails 的 model 层中可以手动缓存某些业务结果到对应的缓存存储系统里。
Rails.cache.fetch(key, expires_in: 1.hour) do
....
end
action 缓存:rails4 中移除了 action 的缓存,需要 gem(actionpack-action_caching) 才能实现。缓存了 action response 的 html 结果,但可以进入 action-pack 进行 authenticaion 等判断。其内部实现主要是借助 fragment cache 和各种 callback 实现的。 如
before_action :authentication, only: :show
cache_action :show, expires_in: 1.hour
采取的是讲缓存的方法设置为 around_action,每次执行 action 的时候,就会 around 这个缓存操作。
def write_fragment(key, content, options = nil)
return content unless cache_configured?
key = fragment_cache_key(key)
instrument_fragment_cache :write_fragment, key do
content = content.to_str
cache_store.write(key, content, options)
end
content
end
而这个 cache_store,就是在 AbstractController::Caching 中定义的
config.cache_store = ActiveSupport::Cache.lookup_store(store),也就是 Rails.cache
page 缓存:rails4 中移除了 page 的缓存,需要 gem(actionpack-page_caching) 才能实现。缓存了 action response 的 html,无需进入 action-pack,直接可以返回结果,速度是最快的。
cache_page :show, expires_in: 1.hour
def write(content, path, gzip)
FileUtils.makedirs(File.dirname(path))
File.open(path, "wb+") { |f| f.write(content) }
if gzip
Zlib::GzipWriter.open(path + ".gz", gzip) { |f| f.write(content) }
end
end
page 缓存就干脆存入文件里,如果可以的话 gzip 压缩
随着互联网应用的逐渐发展,页面复杂程度加剧,后端无法做到整页面的缓存,只能分割成片段。片段缓存的概念逐渐强化。 虽然调用的是 Helper 里的 cache,本质还是 ActionController 层的缓存