rails缓存机制的几个问题
by Robin Lu on Apr.28, 2007, about cache, performance, rubyonrails
ruby on rails提供了一些内建的cache机制,我们比较多的用到了其中的fragment cache。在实际使用过程中,发现了一些问题,如果你不注意,performance和正确性可能都会受到影响。
cache的重复读取问题
这是从Agile Web Development with Rails中抄下来的一段代码:
1 2 3 4 5 6 7 | def list @dynamic_content = Time.now.to_s unless read_fragment(:action => 'list') logger.info("Creating fragment") @articles = Article.find_recent end end |
相信大家也都是用判断read_fragment返回值的方法来看cache是否存在。但你有没有想过,read_fragment这个函数的真正含义是读取cache。在view里,helper cache又会把cache的内容读一遍。假设你的cache用的是FileStore,下面是cache.rb中read的实现:
1 2 3 | def read(name, options = nil) #:nodoc: File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil end |
两次调用read_fragment就是两次独立的文件IO,而且每次都全文读入。第一次读完全是浪费。
解决方案:
一个是在controller里将read_fragment返回的内容保存在一个instance variable里,再到view里显示。这个做法的缺点是无法在view里使用现成的helper cache了,需要重写一个类似的helper,判断相关的instance variable而不是再去read。
另一个方法是自己写一个check_fragment,用File.exist?去判断,效率要高得多。
cache判断的原子性问题
还是上面那段代码:
1 2 3 4 5 6 7 | def list @dynamic_content = Time.now.to_s unless read_fragment(:action => 'list') logger.info("Creating fragment") @articles = Article.find_recent end end |
假设controller在read_fragment的时候,cache存在,但就在运行出了这个unless之后,render view之前,cache被清除了,这完全有可能发生。就在render view的时候,helper cache发现cache不存在,看看下面的代码(仍然来自Agile Web Development with Rails),会发生什么?
1 2 3 4 5 6 7 | < % cache do %> < !- Here's the content we cache -> <ul> < % for article in @articles -%> <li><p>< %= h(article.body) %></p></li> < % end -%> </ul> < % end %> < !- End of the cached content -> |
@articles是nil,你的程序抛出异常了!
解决方法:
没有什么万灵药,要根据具体的情况来分析。最简单的就是在view里判断@articles是否存在,总比抛异常好。还有一个方法就是把@articles的初始化放到view的cache do里面,如果嫌view里代码太多,就放一个helper里。
可怕的expire_fragment
其实只有一半是可怕的。
如果用FileStore,expire_fragment做的其实就是删除文件。假如你指定了是哪个cache,没问题。但expire_fragment还支持正则表达式,假如你指定的是一个正则表达式,它会干什么?看action_controller/caching.rb里的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def delete_matched(matcher, options) #:nodoc: search_dir(@cache_path) do |f| if f =~ matcher begin File.delete(f) rescue Object => e # If there's no cache, then there's nothing to complain about end end end end ... def search_dir(dir, &callback) Dir.foreach(dir) do |d| next if d == "." || d == ".." name = File.join(dir, d) if File.directory?(name) search_dir(name, &callback) else callback.call name end end end |
遍历所有cache目录下的文件,所有。然后一个一个去和正则表达式匹配。如果你不幸有上千个cache fragment,知道有多慢么?自己试试吧。
解决方案:
首先不是一个解决方案,是一个忠告:不要传给expire_fragment一个正则表达式。
其次,你可能可以写一个expire_fragment_from(root_path, reg),让它从一个指定的路径下做这样的遍历。然后优化cache fragment的目录结构,尽量缩小作用域。
Ruby On Rails是一个很好的框架,但仍然有要改进的地方。如果你在使用rails cache上有什么心得,欢迎交流。
6 Comments for this entry
1 Trackback or Pingback for this entry
-
» 昨日收集 - 电子商务的六种模式 | 94smart’s Blog
April 29th, 2007 on 12:17 am[...] rails缓存机制的几个问题 ?石锅拌饭 | 互联网 Mac & 软件开发 [...]
April 28th, 2007 on 7:01 pm
谢谢分享,robin 是一个非常细致入微的程序员,这是优秀程序员的标致吧
April 30th, 2007 on 11:02 am
我的做法是, 在 controller 裡建一個 late finder (也有人叫 lazy data fetcher), 把實際從Database讀資料的工作延後到 rendering view 時才做, 這樣就不需要在 controller 裡判斷 cache 是否存在, 也不需要擔心上文提到的問題.
November 23rd, 2007 on 4:24 pm
研究果真很透彻,有些问题我也遇见过!谢谢分享啦
March 25th, 2008 on 7:10 pm
关于那个 read 的 问题 , 好像社区里面 不推荐在 controller 里面 判断 , 直接在 helper 里面写个方法 ,然后:
‘fragment_url’ , :object => helper_method %>
March 26th, 2008 on 11:57 am
楼上的方法不错
October 24th, 2008 on 2:10 pm
‘fragment_url’ , :object => helper_method %>
‘fragment_url’是指什么啊,能说清楚点吗?意思是也在view中判断,如果没有再查找