Rails的片段緩存fragment cache有一個很隐蔽的陷阱。在Rubyconf China 2009會上,牛人Robin Lu向大家詳細講解了這個陷阱(他演講的ppt在http://www.scribd.com/doc/15705678/Ruby-on-Rails-Pitfall?autodown=pdf 可以看到),最後也提出了3種解決方案,不過我覺得這3種方案都不太令人滿意,下面分享一下我的解決方案。
首先,有必要簡單介紹一下Rails的這個片段緩存陷阱。大家先看一下代碼:
#code in the controller
class BlogController < ApplicationController
def list
unless read_fragment("/blog/list/articles")
@articles = Article.find(:all)
end
end
end
#code in the view
<% cache("/blog/list/articles") do %>
<ul>
<% for article in @articles do %>
<li> <p> <%=h(article.body)%> </p></li>
<% end %>
</ul>
<% end %>
這是很多人使用片段緩存的做法,大概是受了那本經典教程影響的緣故。在并發量不大的情況下,這段代碼不會産生問題,但是在并發量一大很容易會crash,提示資訊是@articles未指派。其原因在于,在action中檢查緩存和view中使用緩存中間存在時間間隔。設想一下:一個過程在action中found cache,于是未擷取@articles,但是執行到view時,緩存被另外的程序清空了,這個時候使用@articles就會報異常。
Robin提到的三種方法是:
1.處理異常,提示使用者重新整理頁面。這種方法對使用者體驗而言,不友好。
2.在view處理的地方擷取@articles。這種方法讓view中充斥代碼,不優雅。
3.更新緩存内容而不是清空緩存。這種方法需要額外處理,不爽。
我的解決方法其實非常簡單 ,改寫cache方法和fragment_exists?方法,在action中使用fragment_exists?檢查緩存,如果找到,就把讀到的内容置到執行個體變量中,在view的cache方法中,使用該執行個體變量。 多說無益,看一下代碼
就明白了。
插件smart_fragment_cache 中的核心代碼
module ActionController #:nodoc:
module Caching
module Fragments
#override fragment_exist?
def fragment_exists?(name, options=nil)
@internal_smart_caches ||= {}
key = fragment_cache_key(name)
@internal_smart_caches[key] = read_fragment(name, options)
end
#cache_miss?
def fragment_miss?(name, options=nil)
!fragment_exists?(name,options)
end
#override fragment_for
def fragment_for(buffer, name = {}, options = nil, &block) #:nodoc:
if perform_caching
if cache = smart_read_fragment(name, options)
buffer.concat(cache)
else
pos = buffer.length
block.call
write_fragment(name, buffer[pos..-1], options)
end
else
block.call
end
end
#smart_read_fragment
def smart_read_fragment(name, options=nil)
key = fragment_cache_key(name)
(@internal_smart_caches and @internal_smart_caches[key]
) or read_fragment(name, options)
end
end
end
end
使用插件後,剛才的代碼改寫成
#code in the controller
class BlogController < ApplicationController
def list
unless fragment_exists?("/blog/list/articles")
@articles = Article.find(:all)
end
end
end
#code in the view
<% cache("/blog/list/articles") do %>
<ul>
<% for article in @articles do %>
<li> <p> <%=h(article.body)%> </p></li>
<% end %>
</ul>
<% end %>
代碼隻有一處細小變化,action中 read_fragment 變為 fragment_exists?。使用這個方案,有三個好處:
1.檢查和使用緩存資料隻發生一次,高并發下不會觸發異常,更安全;
2.隻執行一次擷取緩存動作,更高效;
3.不需要額外處理,同時保持了view中代碼清潔,更優雅。
插件 smart_fragment_cache剛剛寫成,目前隻針對 Rails 2.3.2版本,先提供附件下載下傳吧,等我弄好後再正式釋出出來,希望對大家有幫助。