开源 > identity_cache
Ruby

identity_cache

IdentityCache 是一个可插入 ActiveRecord 的 blob 级别缓存解决方案。不要使用 #find,使用 #fetch!

IdentityCache

Build Status

IdentityCache 是一个选择性加入的 ActiveRecord 读取缓存,在生产环境中被使用,并从 Shopify 中提取出来。IdentityCache 允许你在模型级别指定如何缓存模型对象,并添加了许多方便的方法来通过缓存访问这些对象。Memcached 被用作后端缓存存储,只有当在 Memcached 中找不到对象的副本时才会访问数据库。

IdentityCache 跟踪已缓存索引的对象,并使用 after_commit 钩子在对象更改时使其过期,以及任何上层对象。

安装

将以下行添加到你的应用程序的 Gemfile 中

gem 'identity_cache'
gem 'cityhash'        # optional, for faster hashing (C-Ruby only)

然后执行

$ bundle

将以下内容添加到你的 environment/production.rb 中

config.identity_cache_store = :mem_cache_store, Memcached::Rails.new(:servers => ["mem1.server.com"])

使用此代码添加一个初始化器

IdentityCache.cache_backend = ActiveSupport::Cache.lookup_store(*Rails.configuration.identity_cache_store)

用法

基本用法

class Product < ActiveRecord::Base
  include IdentityCache

  has_many :images

  cache_has_many :images, :embed => true
end

# Fetch the product by its id, the primary index.
@product = Product.fetch(id)

# Fetch the images for the Product. Images are embedded so the product fetch would have already loaded them.
@images = @product.fetch_images

注意:你必须将 IdentityCache 模块包含到你想要使用它的类中。

二级索引

IdentityCache 允许你通过 id 以外的字段查找记录。你可以拥有多个这些索引,并使用任何其他字段组合。

class Product < ActiveRecord::Base
  include IdentityCache
  cache_index :handle, :unique => true
  cache_index :vendor, :product_type
end

# Fetch the product from the cache by the index.
# If the object isn't in the cache it is pulled from the db and stored in the cache.
product = Product.fetch_by_handle(handle)

products = Product.fetch_by_vendor_and_product_type(vendor, product_type)

这让你能够以你想要的方式自由地使用你的对象,并且不会妨碍你。这确实会在 Memcached 中保留一个独立的缓存副本,因此你可能需要注意正在添加的不同缓存的数量。

从缓存读取

IdentityCache 基于你标记了缓存索引的类,根据这些索引添加 fetch_* 方法。下面的示例将向类添加一个 fetch_by_domain 方法。

class Shop < ActiveRecord::Base
  include IdentityCache
  cache_index :domain
end

关联缓存也遵循此规则,并根据为这些关联添加的索引添加 fetch_* 方法。

class Product < ActiveRecord::Base
  include IdentityCache
  has_many  :images
  has_one   :featured_image

  cache_has_many :images
  cache_has_one :featured_image
end

@product.fetch_featured_image
@product.fetch_images

嵌入关联

IdentityCache 可以轻松地将对象嵌入到父级的缓存条目中。这意味着加载父对象也会加载关联并将其与父对象一起添加到缓存中。后续的缓存请求将在一次提取中加载父对象及其关联。如果还要能够单独缓存对象,这可能意味着缓存中的一些重复,因此应谨慎使用。这适用于 cache_has_manycache_has_one 方法。

class Product < ActiveRecord::Base
  include IdentityCache

  has_many :images
  cache_has_many :images, :embed => true
end

@product = Product.fetch(id)
@product.fetch_images

使用此代码,在缓存未命中时,将从数据库加载产品及其关联的图像。所有这些数据都将存储到产品的单个缓存键中。以后的请求将加载整个数据块;@product.fetch_images 将不需要访问数据库,因为图像是从缓存中与产品一起加载的。

缓存多态关联

IdentityCache 尽可能地尝试找出关联的双方,以便在从缓存重建对象时可以设置它们。在某些情况下,这很难确定,因此你可以告诉 IdentityCache 关联应该是什么。这种情况最常见于嵌入多态关联时。 cache_has_manycache_has_one 上的 inverse_name 选项允许你指定关联的反向名称。

class Metafield < ActiveRecord::Base
  include IdentityCache
  belongs_to :owner, :polymorphic => true
  cache_belongs_to :owner
end

class Product < ActiveRecord::Base
  include IdentityCache
  has_many :metafields, :as => 'owner'
  cache_has_many :metafields, :inverse_name => :owner
end

:inverse_name => :owner 选项告诉 IdentityCache 另一侧的关联的名称,以便在从缓存加载元字段时可以正确设置关联。

缓存属性

对于你可能不需要缓存整个对象的情况,只需要缓存记录中的一个属性,可以使用 cache_attribute。这将通过指定的键缓存单个属性。

class Redirect < ActiveRecord::Base
  cache_attribute :target, :by => [:shop_id, :path]
end

Redirect.fetch_target_by_shop_id_and_path(shop_id, path)

这将从缓存中读取属性,或者查询数据库获取属性并将其存储在缓存中。

添加到 ActiveRecord::Base 的方法

cache_index

选项:[:unique] 允许你声明索引是唯一的(只有一个对象存储在索引处)还是不唯一的,这允许存在多个匹配索引键的对象。默认值为 false。

示例:cache_index :handle

cache_has_many

选项:[:embed] 当为 true 时,指定在缓存时应将关联与父级一起包含。这意味着当从缓存加载父级时,将已加载关联的对象,而无需自行获取它们。当为 :ids 时,只有关联记录的 id 将在缓存时与父级一起包含。

[:inverse_name] 指定关联使用的父对象名称。当关联在父对象和子对象之间通常命名不同时,这对多态关联很有用。

示例:cache_has_many :metafields, :inverse_name => :owner, :embed => true

cache_has_one

选项:[:embed] 当为 true 时,指定在缓存时应将关联与父级一起包含。这意味着当从缓存加载父级时,将已加载关联的对象,而无需自行获取它们。当前未实现其他值。

[:inverse_name] 指定关联使用的父对象名称。当关联在父对象和子对象之间通常命名不同时,这对多态关联很有用。

示例:cache_has_one :configuration, :embed => true

cache_belongs_to

示例:cache_belongs_to :shop

cache_attribute

选项:[:by] 指定你想要通过什么键来缓存属性。默认为 :id。

示例:cache_attribute :target, :by => [:shop_id, :path]

记忆化缓存代理

可以为一个代码块记忆化缓存读取和写入操作,以从内存中提供重复的身份缓存请求。可以通过在你的 ApplicationController 中添加此环绕过滤器来为 http 请求执行此操作。

class ApplicationController < ActionController::Base
  around_filter :identity_cache_memoization

  def identity_cache_memoization
    IdentityCache.cache.with_memoization{ yield }
  end
end

版本控制

缓存键默认包含一个版本号,该版本号在 IdentityCache::CACHE_VERSION 中指定。每当缓存值的存储格式被修改时,此版本号就会更新。如果修改了缓存值格式,你必须运行 rake update_serialization_format 才能通过单元测试,并将修改后的 test/fixtures/serialized_record 文件包含在你的拉取请求中。

注意事项

一个警告。某些版本的 Rails 会默默地捕获 after_commit 钩子中的所有异常。如果缓存过期 after_commit 之前的 after_commit 失败,则缓存将不会过期,并且你将留下过时的数据。

由于所有内容都是从 Memcached 进行编组和解组的,因此更改 Ruby 或 Rails 版本可能意味着你的对象无法从 Memcached 解组。有很多方法可以解决这个问题,例如在升级时命名空间键,或者捕获编组加载错误并将其视为缓存未命中。如果你正在使用 IdentityCache 并升级 Ruby 或 Rails,请注意这一点。

IdentityCache 也是通过深思熟虑的设计来 *选择性加入* 的。这意味着 IdentityCache 不会干扰普通 Rails 关联的工作方式,并且将其包含在模型中不会更改该模型的任何客户端,直到你将它们切换为使用 fetch 而不是 find。这是因为 IdentityCache 永远不可能 100% 一致。进程死亡、异常发生和网络故障发生,这意味着某些数据库事务可能提交,但相应的 memcached DEL 操作可能不会成功。这意味着你需要仔细考虑何时使用 fetch 以及何时使用 find。例如,在 Shopify,我们从不在涉及到金钱流动的路径上使用任何 fetch,因为 IdentityCache 可能完全错误,并且我们希望向人们收取正确的金额。但是,我们在绝对正确性不是最重要的事情的性能关键路径上使用 fetcher,这正是 IdentityCache 的用途。

备注