Rails Rails 里的 cache

ane · 2017年08月18日 · 最后由 happyming9527 回复于 2018年05月15日 · 5460 次阅读

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)

Rails 自带的 cache 有四种

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 的具体 cache 方案

1. rails 框架自动进行 SQL 缓存

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 语句的抽象二叉树,而不是所谓的查询结果。

2.ActiveRecord 层缓存

在 rails 的 model 层中可以手动缓存某些业务结果到对应的缓存存储系统里。

Rails.cache.fetch(key, expires_in: 1.hour) do 
....
end

3. ActionController 层

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 压缩

4. ActionView 层缓存

随着互联网应用的逐渐发展,页面复杂程度加剧,后端无法做到整页面的缓存,只能分割成片段。片段缓存的概念逐渐强化。 虽然调用的是 Helper 里的 cache,本质还是 ActionController 层的缓存

楼主,你好像没理解 race_condition_ttl 的意思。当前进程,给 cache 延长了时间。但是当前进程,也在继续执行后面的代码(而其他进程再执行的时候,发现有新的缓存就直接返回了),接着就要执行耗时的 block,然后重新写缓存。这是为了避免所有进程都同时执行 block,同时进行耗时操作。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号