多くのプロジェクトで、キャッシュは便利である一方で、特にクライアント側では見落とされがちであることに気づきました。クライアント側のキャッシュは、待ち時間を短縮し、繰り返されるサーバー要求をオフロードすることでユーザー エクスペリエンスを向上させるために重要です。たとえば、無限スクロールや頻繁に更新されるダッシュボードを備えたアプリケーションでは、以前に取得したデータをキャッシュすることで不必要な API 呼び出しが防止され、よりスムーズな操作とより高速なレンダリング時間が保証されます。
私の最近のプロジェクトの 1 つでは、キャッシュを実装することで API 呼び出し量が 40% 以上削減され、顕著なパフォーマンスの向上とコストの削減につながりました。これは、クライアント側のキャッシュが基本的な最適化戦略とみなされる理由を強調しています。キャッシュは、開発時間の制約やその他の優先事項のため、比較的単純な実装ではパフォーマンスに大きな影響を与えるにもかかわらず、最後に検討される機能の 1 つになる傾向があります。
キャッシュは、静的コンテンツの CDN である Redis を使用したバックエンド キャッシュから、クライアント上のメモリ内キャッシュ、さらには永続性のための localStorage や IndexedDB の使用まで、アーキテクチャのさまざまなレベルで実装できます。理想的には、これらの戦略を組み合わせて、データベースと API の負荷とコスト、さらには、特に以前にフェッチされたデータの場合のクライアント/サーバー リクエストの遅延を軽減する必要があります。
この記事では、JavaScript で TTL (Time-to-Live) サポートを備えた LRU (Least Recent Used) キャッシュを設計および実装し、私の adev-lru と同様のパッケージを作成する方法を検討します。最後には、効果的なキャッシュ ソリューションの中核原則と機能を示す実用的な例が完成します。
LRU (最も最近使用されていない) キャッシュは、最近アクセスされた項目がメモリ内に残るようにし、容量を超えた場合は最も最近アクセスされた項目を削除します。この戦略は、使用順序を維持することによって機能します。各アクセサリはキャッシュ内のアイテムの位置を更新し、最もアクセスの少ないアイテムが最初に削除されます。
他のキャッシュ戦略と比較して、LRU はシンプルさと効率のバランスが取れており、最近の使用状況が将来のアクセスの信頼できる指標となるシナリオに適しています。たとえば、API 応答、サムネイル、または頻繁にアクセスされるユーザー設定をキャッシュするアプリケーションは、LRU を利用して、エビクション プロセスを過度に複雑にすることなく、冗長なフェッチ操作を削減できます。
アクセス頻度を追跡し追加の簿記を必要とする LFU (最低使用頻度) とは異なり、LRU はこの複雑さを回避しながら、実際の多くのユースケースで優れたパフォーマンスを実現します。同様に、FIFO (先入れ先出し) と MRU (最近使用されたもの) は代替のエビクション ポリシーを提供しますが、最近のアクティビティが重要な使用パターンにはあまり適合しない可能性があります。私の実装では LRU と TTL (Time-to-Live) サポートを組み合わせることで、データの自動有効期限が必要なシナリオも処理し、ライブ ダッシュボードやストリーミング サービスなどの動的な環境での適用性をさらに高めます。最新のデータへのアクセスが重要なアプリケーションで特に役立ちます。
LRUCache クラスは、効率的で、柔軟な構成をサポートし、自動エビクションを処理するように構築されています。以下にいくつかの主要なメソッドを示します:
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
この方法では、アプリケーション内にキャッシュのインスタンスが 1 つだけ存在することが保証され、リソース管理を簡素化する設計上の選択になります。キャッシュをシングルトンとして実装することで、冗長なメモリの使用を回避し、アプリケーション全体で一貫したデータを確保します。これは、追加の調整ロジックを必要とせずに競合を防止し、確実に同期できるため、複数のコンポーネントまたはモジュールが同じキャッシュされたデータにアクセスする必要があるシナリオで特に役立ちます。容量が指定されていない場合、デフォルトは 10 です。
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
このメソッドは、キャッシュ内の項目を追加または更新します。キーがすでに存在する場合、対応する項目は削除され、キャッシュの先頭に再追加されます。これを行うために、キャッシュは二重リンク リストを使用してデータをノードとして保存し、リストの最後 (末尾) からデータを削除してリストの先頭 (先頭) に移動する機能を維持し、定数 O を保証します。 (1) すべてのノードのデータを読み取り、リストの各ノードへのポインターを保存するためにハッシュ テーブルが使用されます。このプロセスは、最近アクセスしたアイテムが常に優先され、効果的に「最近使用された」アイテムとしてマークされるようにすることで、LRU の原則に沿っています。そうすることで、キャッシュは正確な使用順序を維持します。これは、容量を超えた場合にエビクションの決定を行うために重要です。この動作により、リソースが最適に管理され、頻繁にアクセスされるデータの取得時間が最小限に抑えられます。キーがすでに存在する場合、項目は前面に移動され、最近使用されたものとしてマークされます。
public get(key: string): T | undefined { const node = this.hash.get(key); const now = Date.now(); if (node == null || node.ttl < now) { return undefined; } this.evict(node); this.prepend(node.key, node.value, node.ttl); return node.value; }
このメソッドは、保存されているアイテムを取得します。アイテムの有効期限が切れた場合、アイテムはキャッシュから削除されます。
キャッシュの効率を評価するために、ヒット率、ミス、エビクションなどのパフォーマンス指標を実装しました。
public static getInstance<T>(capacity: number = 10): LRUCache<T> { if (LRUCache.instance == null) { LRUCache.instance = new LRUCache<T>(capacity); } return LRUCache.instance; }
public put(key: string, value: T, ttl: number = 60_000): LRUCache<T> { const now = Date.now(); let node = this.hash.get(key); if (node != null) { this.evict(node); } node = this.prepend(key, value, now + ttl); this.hash.set(key, node); if (this.hash.size > this.capacity) { const tailNode = this.pop(); if (tailNode != null) { this.hash.delete(tailNode.key); } } return this; }
このメソッドはすべてのアイテムをクリアし、キャッシュ状態をリセットします。
私の実装では、T | を返す代わりに getOption のような他のメソッドも追加しました。未定義の場合は、より機能的なアプローチを好む人のために、モナド オプションのインスタンスを返します。また、ロギング目的でキャッシュ上のすべての操作を追跡するための Writer モナドも追加しました。
このアルゴリズムに関連する他のすべてのメソッドは、このリポジトリで非常に詳しくコメントされています: https://github.com/Armando284/adev-lru
LRU キャッシュが唯一の選択肢ではありません。適切なキャッシュ アルゴリズムの選択は、アプリケーション固有の要件とアクセス パターンに大きく依存します。以下は、LRU と他の一般的に使用されるキャッシュ戦略との比較、およびそれぞれをいつ使用するかに関するガイダンスです:
LRU は、シンプルさと有効性のバランスをとっており、最近のアクティビティが将来の使用と強く相関するアプリケーションに最適です。例:
対照的に、アクセス パターンが頻度や挿入順序の方が関連性があることを示している場合は、LFU や FIFO などのアルゴリズムがより良い選択となる可能性があります。これらのトレードオフを評価することで、キャッシュ戦略がアプリケーションの目標とリソースの制約に確実に適合するようにします。
メモリ内キャッシュを実装すると、アプリケーションのパフォーマンスが大幅に向上し、応答時間が短縮され、ユーザー エクスペリエンスが向上します。
完全な LRU キャッシュの動作を確認したい場合は、私の npm パッケージ https://www.npmjs.com/package/adev-lru を使用できます。また、改善を続けるためにフィードバックもお待ちしています。
パッケージを試して感想を共有したり、もっと支援したいと感じたら貢献してみてはいかがでしょうか?!
以上がインメモリキャッシュを作成する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。