天天看點

Ruby on Rails實戰之一:巧妙解決頁面片段緩存陷阱

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版本,先提供附件下載下傳吧,等我弄好後再正式釋出出來,希望對大家有幫助。

繼續閱讀