memcached と Rails のフラグメントとオブジェクトのキャッシュを組み合わせる最良の方法
-
07-07-2019 - |
質問
最新の投稿を表示するページの一部があり、30 分後に期限切れになるとします。ここではRailsを使用しています。
<% cache("recent_posts", :expires_in => 30.minutes) do %>
...
<% end %>
フラグメントが存在する場合、最新の投稿を取得するためにデータベース検索を行う必要がないことは明らかなので、そのオーバーヘッドも回避できるはずです。
私が今やっていることは、コントローラーで次のようなもので、機能しているようです。
unless Rails.cache.exist? "views/recent_posts"
@posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC")
end
これが最善の方法でしょうか?安全ですか?
私が理解できないことの 1 つは、なぜキーが「」なのかということです。recent_posts
" フラグメントの場合と "views/recent_posts
後で確認するときに、これを見て思いついたのですが、 memcached -vv
それが何を使っていたのかを見るために。また、「」を手動で入力することの重複も好きではありません。recent_posts
」ということは一か所にまとめておいた方が良いでしょう。
アイデアは?
解決
Evan Weaverのインターロックプラグインはこの問題を解決します。
さらにきめ細かな制御など、異なる動作が必要な場合は、このようなものを自分で簡単に実装することもできます。基本的な考え方は、ビューがそのデータを必要とする場合にのみ実際に実行されるブロックにコントローラーコードをラップすることです:
# in FooController#show
@foo_finder = lambda{ Foo.find_slow_stuff }
# in foo/show.html.erb
cache 'foo_slow_stuff' do
@foo_finder.call.each do
...
end
end
ルビーメタプログラミングの基本に精通している場合、これを好みのよりクリーンなAPIにまとめるのは簡単です。
これは、ファインダーコードをビューに直接配置するよりも優れています。
- 開発者が慣例により期待するファインダコードを保持します
- モデル名/メソッドを無視するビューを保持し、より多くのビューの再利用を可能にします
cache_fuは、そのバージョン/フォークの1つで同様の機能を備えている可能性がありますが、具体的に思い出すことはできません。
memcachedから得られる利点は、キャッシュヒット率に直接関係しています。同じコンテンツを複数回キャッシュして、キャッシュ容量を無駄にせず、不必要なミスを引き起こさないように注意してください。たとえば、一連のレコードオブジェクトとそのhtmlフラグメントを同時にキャッシュしないでください。一般に、フラグメントキャッシュは最高のパフォーマンスを提供しますが、実際にはアプリケーションの詳細に依存します。
他のヒント
コントローラでキャッシュをチェックする間にキャッシュの有効期限が切れるとどうなりますか ビューレンダリングでチェックされている時間は?
モデルに新しいメソッドを作成します:
class Post
def self.recent(count)
find(:all, :limit=> count, :order=>"updated_at DESC")
end
end
ビューでそれを使用します:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
<% Post.recent(20).each do |post| %>
...
<% end %>
<% end %>
わかりやすくするために、最近の投稿のレンダリングを独自のパーシャルに移動することも検討できます。
<% cache("recent_posts", :expires_in => 30.minutes) do %>
<%= render :partial => "recent_post", :collection => Post.recent(20) %>
<% end %>
調査することもできます
これを行うことができます:
<% cache("recent_posts", :expires_in => 30.minutes) do %>
...
<% end %>
コントローラー
unless fragment_exist?("recent_posts")
@posts = Post.find(:all, :limit=>20, :order=>"updated_at DESC")
end
私は、DRYの問題が2つの場所でキーの名前を必要とすることで頭を抱えていることを認めていますが。私は通常、ラースが提案した方法と同様にこれを行いますが、それは本当に味に依存します。私が知っている他の開発者は、フラグメントのチェックに固執しています。
更新:
フラグメントドキュメントを見ると、ビュープレフィックスの必要性をどのように取り除くことができるかを確認できます。
# File vendor/rails/actionpack/lib/action_controller/caching/fragments.rb, line 33
def fragment_cache_key(key)
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
end
Lars は、以下を使用すると失敗する可能性がわずかにあることについて非常に適切に指摘しています。
unless fragment_exist?("recent_posts")
キャッシュをチェックするときとキャッシュを使用するときの間にギャップがあるためです。
ジェイソンが言及したプラグイン (Interlock) は、フラグメントの存在をチェックしている場合、おそらくフラグメントも使用することになるため、コンテンツをローカルにキャッシュすると想定することで、これを非常に適切に処理します。私が Interlock を使用しているのはまさにこのような理由からです。
思考の一部として:
アプリケーションコントローラーの定義
def when_fragment_expired( name, time_options = nil )
# idea of avoiding race conditions
# downside: needs 2 cache lookups
# in view we actually cache indefinetely
# but we expire with a 2nd fragment in the controller which is expired time based
return if ActionController::Base.cache_store.exist?( 'fragments/' + name ) && ActionController::Base.cache_store.exist?( fragment_cache_key( name ) )
# the time_fraqgment_cache uses different time options
time_options = time_options - Time.now if time_options.is_a?( Time )
# set an artificial fragment which expires after given time
ActionController::Base.cache_store.write("fragments/" + name, 1, :expires_in => time_options )
ActionController::Base.cache_store.delete( "views/"+name )
yield
end
その後、任意のアクションで使用
def index
when_fragment_expired "cache_key", 5.minutes
@object = YourObject.expensive_operations
end
end
ビュー
cache "cache_key" do
view_code
end