分享 关于缓存的一些思路。。。

zhangjinzhu · 2012年11月27日 · 最后由 hooopo 回复于 2012年11月28日 · 4348 次阅读

缓存的问题:

1, 项目中写缓存往往比较简单,但是清缓存比较麻烦,往往是清缓存条件一大堆,要加各种 sweeper, observer 来清... 2, 在 views 中,通过 page/action/fragment 来缓存,可是这些缓存方法对 API json 类的请求有一定的局限性

那么怎么缓存比较好呢?我的想法是: 1, 缓存不是用来清的,需要自动过期. 例如:product A 的缓存的 key 需要和 product A 的更新时间相关,那么在这个产品 A 更新后,下次取缓存的时候, 会去查找和更新时间绑定的缓存,也就是说 A 之前的缓存已经自动过期了,这次取会自动生成新的缓存。

2, View 中的 cache 不是最重要的,最应该缓存的地方是取数据层,然后再把这些缓存数据 render。 做为一个 Light Views/Controllers, Heavy Models 的程序来说,最应该缓存的是那些 model 的比较重量级方法。

假设下面的例子:

class Product def heavy_instance_method # ............. end

def self.heavy_class_method end end

Product.find(1).heavy_instance_method Product.heavy_class_method

假设上面的这两个方法都是比较耗时间,那么这两个方法可能就是一个被缓存的好例子. 如果让你去缓存这个方法,怎么做呢?最简单的就是:

class Product def heavy_instance_method Rails.cache.fetch("Product-heavy_instance_method-#{self.updated_at}") do .... end end end

这里用了这个实例的 updated_at 做为 key 来帮助这个方法自动过期。。。虽然简单,可是挺恶心的.... 另外如果这是个类方法,或者里面的缓存如果再和其它类有相互依赖的话,就会麻烦的多....

为了解决上面的问题,我写了个 Gem, 先看一下怎么用:

1, 将这个 gem 加到你的 Gemfile 中

gem 'qor_cache'

2, 定义你的配置文件

# config/qor/cache.rb scope :product do cache_method :heavy_instance_method cache_class_method :heavy_class_method end

3, 搞定了。。。。。。简单么?

先从配置文件看一下,那么这个 gem 做了什么呢?简单的讲:

scope :product 就是意味着 block 里面的操作是对 Product 这个 model 的操作 cache_method :heavy_instance_method 就是说要缓存 Product 的实例方法 heavy_instance_method cache_class_method :heavy_class_method 就是说要缓存 Product 的类方法 heavy_class_method

工作原理是什么呢?

1, 对实例方法的缓存:

alias 以前的 heavy_instance_method 方法 重新生成一个 heavy_instance_method 然后在这个新的 heavy_instance_method 中,调用 Rails.cache, 以类名,方法名,更新时间为 key 进行缓存

2, 对类方法的缓存:

alias 以前的 heavy_class_method 方法 重新生成一个新的 heavy_class_method 类方法 然后在这个新的类方法中,调用 Rails.cache, 以类名,方法名,这个类的 cache_key 为 key 进行缓存 (PS: 这个类的 cache_key 是一个随机值,会在这个类发生任意保存,删除后更新为新随机值)

上面只是这个 gem 的一部分小功能,更多欢迎查看 README, 源代码。。。参考 https://github.com/qor/qor_cache 代码刚刚完成,木有经过再加工、再优化,大家先凑合着看吧... XD

本来想再多写点,把所有的功能全部描述一遍,可是实在写不下去了,不知道怎么表述,文字功底太差了,所以点到为止,欢迎探讨。。。真佩服那些一写就能写几万字的神人。。。XD

对 Product.find(1) 进行缓存会不会更好点呢?过期可以使用 after_commit 来做,毕竟瓶颈还是 db 嘛。

不用清空,设置过期时间就可以了,设置过去的时间,就过期了

我最近在用 @flyerhzmsimple_cacheable

定义方法是在对应的 Model 里面申明,比如 post.rb

class Post < ActiveRecord::Base
  include Cacheable

  belongs_to :user
  has_many :comments, :as => :commentable

  model_cache do
    with_key                          # post.find_cached(1)
    with_association :user, :comments # post.cached_user, post.cached_comments
  end
end

调用的时候

Post.first.cached_user

感觉你这样把 Cache 的配置项独立到外部文件里面以后不好管理,尤其是在 config 目录下面,为什么不在 app/qor_cache 里面呢?

@hooopo 同学说任何需要修改 AR 写法的 cache 都是耍流氓 https://github.com/csdn-dev/second_level_cache

你所谓的重方法本身就应该把结果缓存到实例变量中,这无关乎你是否利用外部的缓存策略。 外部的策略,可以选择直接将对象丢到 memcached 加上过期时间,由外部解决减少耦合。

两者完全是不同层面的东西,混合在一起的话,灵活度和可维护性都极低

@vkill 其实你的程序中,类似这种 find Product.find(1),占用的时间应该是比较少数的,而占用时间最多的是 Product.on_selling, Product.find(1).related_products , Product.find(1).hot_comments 这些方法,qor_cache 要解决的主要问题也是缓存这些执行时间比较久的方法。

例如下面这个 我随手写的一个方法:

class Product def recommend_products order_ids = OrderItem.where(:product_id => id).map(&:order_id).uniq product_ids = OrderItem.where(:order_id => order_ids).map(&:product_id) Product.where(:id => product_ids) end end

这个方法是用来根据一个产品推荐其它产品,他依赖的规则就是其它购买这个产品用户购买的其它产品,这个方法的结果不只是根据 Product 相关,还根据 OrderItem 相关

如果用 qor_cache 的话,你可以这样写

cache_key :orders do [Order, OrderItem] end

cache_key :products do Product end

scope :product do cache_method :recommend_products, 'orders', 'products' end

上面的配置文件的意思就是说,我把 recommend_products 这个实例方法给缓存起来了,但是,如果 Product 有任何更新,Order, OrderItem 有任何更新,我都要重新取 recommend_products 的值

#3 楼 @huacnlee #4 楼 @ywencn

这样子的话,我要改好多的现有代码,例如把 post.find(1) 改成 post.find_cached(1) ,有个缺点就是,如果我深度依赖这个库的话,可能就被他吊死了,例如 cache_money 不支持 rails 3

而像我这样子的话,我可以随时删掉这个库,一切还是都正常工作的

并且,重写方法有一个好处就是在开发中,我只要修改这个 model,所有的缓存都会失效了,因为 rails 会重新加载你更新后的方法

#5 楼 @hozaka 如果把结果放实例方法的话,每个请求还会要重新计算的么。。。。其实上面那个例子还好点

我们实际项目中,有个更重的方法,就是取出所有在当前季节下有库存的产品分类,这个方法横跨四,五张表取数据,计算一下都要近 1s,这样子只能做持久性的缓存比较适合

2, 在 views 中,通过 page/action/fragment 来缓存,可是这些缓存方法对 API json 类的请求有一定的局限性

jbuilder 可以和 Rails 的片段缓存一起使用。

#4 楼 @ywencn #7 楼 @zhangjinzhu

恩 和 7 楼观点一样:

  1. 如果缓存需要依赖方法名称,就会被使用的缓存插件吊死,想更换/去掉缓存插件越来越困难。

  2. 引入了一种新的 DSL,使用成本增加

  3. 外部 Gem/Plugin/Engine 无法利用这种缓存

#9 楼 @hooopo 嗯,不过即使 json 可以 fragment 缓存,如果你要缓存 Product.hot_products 的话,view 的缓存如果 html 一份,json 一份。。。还是有点麻烦的。。。。。。而直接缓存 Product.hot_products 的值的话,简单一些

同意 #7 楼 @hooopo 说的,用了 find_cached 后想去掉缓存插件就异常麻烦了

对方法做缓存这个思路就不错,实际使用的时候非常有用,尤其是对有复杂逻辑的方法,缓存结果就是性能的提升,但是不管你是什么牛逼的缓存,缓存更新和失效的策略都是一件麻烦的事,只能按自己的需求来。

#14 楼 @yzhrain 嗯,是的。。。。缓存更新,失效是个麻烦的事情,不过 qor_cache 就可以帮助你简化这部分,可以参看下 6 楼的那个例子

这个 Product 的实例方法和 Order 相关的情况下,怎么缓存方法,失效缓存

#8 楼 @zhangjinzhu 你没看明白我意思,这是两个不同层面的缓存,用不同的策略解决,才能解决你的问题:

  1. 针对方法自身的结果,缓存在实例变量里,在这个层面上保障同一个实例不会重复查询
  2. 针对业务,把这个实例缓存到 memcached 中,设定过期时间,完成跨请求的缓存

这两者不应该搅在一起

#16 楼 @hozaka 你这种是 local cache + remote cache 的思路。不过在楼主的场景(耗时的逻辑)不会在一次请求里重复调用。只用 remote cache 是没有问题的。

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