石锅拌饭

rails缓存机制的几个问题

by on Apr.28, 2007, about , ,

ruby on rails提供了一些内建的cache机制,我们比较多的用到了其中的fragment cache。在实际使用过程中,发现了一些问题,如果你不注意,performance和正确性可能都会受到影响。

cache的重复读取问题

这是从Agile Web Development with Rails中抄下来的一段代码:

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的实现:

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判断的原子性问题

还是上面那段代码:

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),会发生什么?

< % cache do %> < !- Here's the content we cache -> 
  
    < % for article in @articles -%>
  • < %= h(article.body) %>

  • < % end -%>
< % 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里的实现:

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

  • Caiwangqin

    谢谢分享,robin 是一个非常细致入微的程序员,这是优秀程序员的标致吧 🙂

  • Kevin

    我的做法是, 在 controller 裡建一個 late finder (也有人叫 lazy data fetcher), 把實際從Database讀資料的工作延後到 rendering view 時才做, 這樣就不需要在 controller 裡判斷 cache 是否存在, 也不需要擔心上文提到的問題.

  • confach

    研究果真很透彻,有些问题我也遇见过!谢谢分享啦

  • bujiande

    关于那个 read 的 问题 , 好像社区里面 不推荐在 controller 里面 判断 , 直接在 helper 里面写个方法 ,然后:

    ‘fragment_url’ , :object => helper_method %>

  • Robin Lu

    楼上的方法不错

  • cricy

    ‘fragment_url’ , :object => helper_method %>

    ‘fragment_url’是指什么啊,能说清楚点吗?意思是也在view中判断,如果没有再查找

1 Trackback or Pingback for this entry

Search

Archives

Browse by tags

agile apple blog book design ecto extension firefox git google hack ichm iphone keyword life mac madfox movie nonsense opensource plugin pm ruby rubyonrails sns software startup wordpress work 财帮子