最新のWeb開発では、キャッシュは物事をスピードアップするための迅速で強力な方法です。キャッシュを正しく実行すると、アプリケーションの全体的なパフォーマンスが大幅に向上します。間違って行われると、間違いなく災害に終わります。
ご存知かもしれませんが、キャッシュの無効化は、コンピュータサイエンスで最も難しい3つの問題の1つです。他の2つは、名前の付け方と1つずつのエラーです。簡単な方法の1つは、何かが変更されるたびに、左右のすべてを無効にすることです。しかし、それはキャッシュの目的を無効にします。どうしても必要な場合にのみキャッシュを無効にします。
キャッシュを最大限に活用したい場合は、無効にするものに非常に注意を払い、繰り返しの作業で貴重なリソースを浪費しないようにアプリケーションを保存する必要があります。
このブログ投稿では、Railsキャッシュの動作をより適切に制御するための手法、具体的には、フィールドレベルのキャッシュ無効化の実装について学習します。この手法はに依存しています Rails ActiveRecord およびActiveSupport::Concern
touch
の操作と同様にメソッドの動作。
このブログ投稿は、フィールドレベルのキャッシュ無効化を実装した後にパフォーマンスが大幅に向上したプロジェクトでの最近の経験に基づいています。これは、不要なキャッシュの無効化とテンプレートの繰り返しのレンダリングを減らすのに役立ちました。
Rubyは最速の言語ではありませんが、全体として、開発速度が懸念される場合に適したオプションです。さらに、その メタプログラミング 組み込みのドメイン固有言語(DSL)機能により、開発者は非常に柔軟に対応できます。
のような研究がそこにあります ヤコブニールセンの研究 これは、タスクに10秒以上かかると、フォーカスが失われることを示しています。そして、私たちの焦点を取り戻すには時間がかかります。したがって、これは予想外にコストがかかる可能性があります。
残念ながら、Ruby on Railsでは、テンプレート生成でその10秒のしきい値を超えるのは非常に簡単です。 「HelloWorld」アプリや小規模なペットプロジェクトではこれが発生することはありませんが、多くのものが1つのページに読み込まれる実際のプロジェクトでは、テンプレートの生成は非常に簡単にドラッグを開始できます。
そして、それはまさに私が私のプロジェクトで解決しなければならなかったことです。
しかし、どのくらい正確に物事をスピードアップしますか?
答え:ベンチマークと最適化。
私のプロジェクトでは、最適化における2つの非常に効果的なステップは次のとおりです。
修正 N +1クエリ は簡単だ。できることは、ログファイルを確認することです。ログに以下のような複数のSQLクエリが表示された場合は、それらを積極的な読み込みに置き換えて削除します。
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
これには、と呼ばれる宝石があります 弾丸 この非効率性を検出するのに役立ちます。また、各ユースケースをウォークスルーし、その間に、上記のパターンと照らし合わせてログを確認することもできます。 N + 1の非効率性をすべて排除することで、データベースに過負荷がかからず、ActiveRecordに費やす時間が大幅に短縮されることを確信できます。
これらの変更を行った後、私のプロジェクトはすでにより活発に実行されていました。しかし、私はそれを次のレベルに引き上げて、そのロード時間をさらに短縮できるかどうかを確認することにしました。テンプレートではまだかなりの不要なレンダリングが行われており、最終的にはフラグメントキャッシングが役立ちました。
フラグメントキャッシングは通常、テンプレートの生成時間を大幅に短縮するのに役立ちます。しかし、デフォルトのRailsキャッシュの動作は、私のプロジェクトではそれをカットしていませんでした。
Railsフラグメントキャッシングの背後にある考え方は素晴らしいです。これは、非常にシンプルで効果的なキャッシュメカニズムを提供します。
Ruby On Railsの作者は、Signalv。Noiseに非常に優れた記事を書いています。 フラグメントキャッシュは機能します 。
エンティティのいくつかのフィールドを表示するユーザーインターフェイスが少しあるとしましょう。
cache_key
を計算しますエンティティのクラスとupdated_at
に基づくフィールド。cache_key
を使用して、そのキーに関連付けられたキャッシュに何かがあるかどうかを確認します。これは、キャッシュを明示的に無効にする必要がないことを意味します。エンティティを変更してページをリロードするたびに、エンティティの新しいキャッシュコンテンツがレンダリングされます。
Railsは、デフォルトで、子が変更された場合に親エンティティのキャッシュを無効にする機能も提供します。
belongs_to :parent_entity, touch: true
これをモデルに含めると、自動的に 接する 子供がいるときの親 触れた 。 touch
について詳しく知ることができます ここに 。これにより、Railsは、子エンティティのキャッシュと同時に親エンティティのキャッシュを無効にする簡単で効率的な方法を提供します。
ただし、Railsでのキャッシュは、親エンティティを表すHTMLフラグメントに親の子エンティティのみを表すHTMLフラグメントが含まれるユーザーインターフェイスを提供するために作成されます。つまり、このパラダイムの子エンティティを表すHTMLフラグメントには、親エンティティのフィールドを含めることはできません。
しかし、それは現実の世界では起こりません。 Railsアプリケーションでこの条件に違反することを行う必要があるかもしれません。
ユーザーインターフェイスに、子エンティティを表すHTMLフラグメント内の親エンティティのフィールドが表示される状況をどのように処理しますか?
子に親エンティティのフィールドが含まれている場合は、Railsのデフォルトのキャッシュ無効化動作に問題があります。
親エンティティから提示されたこれらのフィールドが変更されるたびに、その親に属するすべての子エンティティに触れる必要があります。たとえば、Parent1
の場合が変更された場合、Child1
のキャッシュを確認する必要がありますおよびChild2
ビューは両方とも無効になります。
明らかに、これは大きなパフォーマンスのボトルネックを引き起こす可能性があります。親が変更されるたびにすべての子エンティティに触れると、正当な理由もなく多くのデータベースクエリが発生します。
別の同様のシナリオは、エンティティがhas_and_belongs_to
に関連付けられている場合です。アソシエーションがリストに表示され、それらのエンティティを変更すると、アソシエーションチェーンを通じてキャッシュ無効化のカスケードが開始されました。
class Event したがって、上記のユーザーインターフェースの場合、ユーザーの場所が変更されたときに参加者またはイベントに触れるのは非論理的です。しかし、ユーザーの名前が変わったら、イベントと参加者の両方に触れる必要がありますね。
そのため、上記のように、Signalv。Noiseの記事の手法は特定のUI / UXインスタンスでは非効率的です。
Railsは単純なことには非常に効果的ですが、実際のプロジェクトには独自の複雑さがあります。
フィールドレベルのRailsキャッシュの無効化
私のプロジェクトでは、上記のような状況を処理するために小さなRubyDSLを使用しています。これにより、関連付けを通じてキャッシュの無効化をトリガーするフィールドを宣言的に指定できます。
それが本当に役立つ場所のいくつかの例を見てみましょう:
レスポンシブデザインの標準画面サイズ
例1:
class Event このスニペットは、メタプログラミング機能と内部を活用します RubyのDSL機能 。
具体的には、イベントでの名前の変更のみが、関連するタスクのフラグメントキャッシュを無効にします。目的や場所など、イベントの他のフィールドを変更しても、タスクのフラグメントキャッシュは無効になりません。私はこれを呼ぶでしょう フィールドレベルのきめ細かいキャッシュ無効化制御 。

例2:
has_many
によるキャッシュの無効化を示す例を見てみましょう。アソシエーションチェーン。
以下に示すユーザーインターフェイスフラグメントは、タスクとその所有者を示しています。

このユーザーインターフェイスの場合、タスクを表すHTMLフラグメントは、タスクが変更されたとき、または所有者の名前が変更されたときにのみ無効にする必要があります。所有者の他のすべてのフィールド(タイムゾーンや設定など)が変更された場合、タスクキャッシュはそのままにしておく必要があります。
これは、ここに示すDSLを使用して実現されます。
class User DSLの実装
DSLの主な本質はtouch
です。方法。その最初の引数は関連付けであり、次の引数はtouch
をトリガーするフィールドのリストです。その協会について:
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
このメソッドはTouchable
によって提供されますモジュール:
module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end
このコードで重要なのは、touch
の引数を格納することです。コール。次に、エンティティを保存する前に、指定されたフィールドが変更された場合、関連付けをダーティとしてマークします。アソシエーションがダーティである場合は、保存後にそのアソシエーションのエンティティにアクセスします。
次に、懸念の私的な部分は次のとおりです。
... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map).present? end …
check_touchable_entities
でメソッド、チェックします 宣言されたフィールドが変更された場合 。その場合、meta_info[association]
を設定して、関連付けをダーティとしてマークします。 true
へ。
次に、エンティティを保存した後、 汚い協会 必要に応じて、その中のエンティティに触れます。
… def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …
そして、それだけです!これで、単純なDSLを使用してRailsでフィールドレベルのキャッシュ無効化を実行できます。
結論
Railsキャッシングは、比較的簡単にアプリケーションのパフォーマンスを向上させることを約束します。ただし、実際のアプリケーションは複雑になる可能性があり、多くの場合、固有の課題が発生します。デフォルトのRailsキャッシュの動作はほとんどのシナリオでうまく機能しますが、キャッシュの無効化をもう少し最適化することが大いに役立つ特定のシナリオがあります。
Railsでフィールドレベルのキャッシュ無効化を実装する方法がわかったので、アプリケーションのキャッシュの不要な無効化を防ぐことができます。
基本を理解する
DSLは何の略ですか?
DSLは「ドメイン固有言語」の略です。
ActiveRecordタッチメソッドは何をしますか?
touchメソッドは、updated_at / onフィールドを現在の時刻に設定し、レコードを保存します。