ステージ 10: トピックの概要 (第 6 章: キャッシュ)
第 6 章: キャッシング
1. Redisのデータ型
必要とする
- 一般的なデータ型の基礎となる構造をマスターする
概要
データ型は実際には値の型を表します。キーはすべて文字列です。一般的なデータ型 (値) には次のものがあります。
- (弦)文字列(embstr、raw、int)
- (リスト) リスト (複数の ziplist の二重リンク リストで構成されるクイックリスト)
- (ハッシュ表)ハッシュ(ジップリスト、ハッシュテーブル)
- (コレクション内の要素を繰り返すことはできません)set(intset、ハッシュテーブル)
- (セット単位でのご注文)ソートセット(ジップリスト、スキップリスト)
- (あまり使用されず、統計目的のみ)ビットマップ
- (あまり使用されず、統計目的のみ)ハイパーログログ
各タイプは redisObject構造によって表され、各タイプは状況に応じて異なるエンコーディング (つまり、基礎となるデータ構造) を持ちます。
弦
-
文字列が整数値を格納する場合、基礎となるエンコーディングは
int
(4 バイト) であり、実際にはストレージに Long (8 バイト) が使用されます。利点: ① 数字は場所をとらない、② データの計算をするのに便利で、計算のために文字を数字に変換する必要がない。
-
文字列に非整数値 (浮動小数点数またはその他の文字) が格納されている場合、次の 2 つの状況が考えられます。
- 長さ <= 39 バイト、
embstr
エンコーディングを使用して保存します。つまり、redisObject 構造と sdshdr 構造を一緒に保存し、メモリを 1 回だけ割り当てます。 - 長さ > 39 バイト、
raw
エンコーディングを使用して保存します。つまり、redisObject 構造体はメモリを 1 回割り当て、sdshdr 構造体はメモリを 1 回割り当て、ポインタで接続されます。
- 長さ <= 39 バイト、
-
sdshdr は単純な動的文字列と呼ばれます、実装は Java の StringBuilder に似ており、次の特徴があります。
- 文字長を個別に保存するchar* よりも長さを取得するのに効率的です (char* は C 言語のネイティブ文字列表現です)。
- 文字列のスプライス操作を容易にするための動的拡張をサポートします。
- メモリ割り当てと解放時間を短縮するためにスペースを予約する(< 1M の場合、容量は文字列の実際の長さの 2 倍になります。>= 1M の場合、容量は元の容量 + 1M です)
- バイナリセキュリティ, たとえば、従来の char* は終了文字として \0 を使用するため、ビデオや写真などのバイナリ データを保存できませんが、sds は長さによって読み取ります。
リスト
インタビューの質問: クイックリストとジップリストの違い
-
3.2 スタート、Redis はエンコード方式としてクイックリスト (大きなリンク リスト) を使用します。これは双方向のリンク リストであり、ノード要素は ziplist (データが保存される小さなリンク リスト) です。
- リンクされたリストであるため、メモリ内で連続していません。
- 頭と尾の操作効率は高く、時間計算量は O(1)、その他の部分の操作時間の平均は O(n)
- リンク リスト内の ziplist のサイズと要素数を設定でき、デフォルトのサイズは 8kb です。
-
ziplist は、連続したメモリを使用してデータを保存します。設計目標は、データストレージがよりコンパクトになり、断片化のオーバーヘッドが軽減され、メモリが節約されます。、その構造(理解するだけ)は次のとおりです
- zlbytes – ジップリスト全体が占めるバイト数を記録します。
- zltail-offset – 末尾ノードのオフセットを記録します。
- zllength – ノードの数を記録します
- エントリ – ノード、1 ~ N、各エントリは、前のエントリの長さ (逆順トラバーサルの場合)、エンコード、長さ、およびこのエントリの実際のデータを記録します。メモリを節約するために、長さの記録に使用されるワードは次のとおりです。実際のデータ長に応じて異なり、セクション数も異なります。たとえば、前のエントリの長さが 253 の場合は 1 バイト必要ですが、253 を超える場合は 5 バイト必要になります。
- zlend – 終了タグ
-
ziplist は少数の要素を格納するのに適しています。そうでない場合、クエリ効率が高くなく、可変長設計によりチェーン更新の問題が発生します。
ハッシュ
-
データ量が少ない場合は、エンコードとして ziplist が使用されます (キー値は 2 つの連続したエントリとして扱われます)、キーまたは値の長さが大きすぎる場合 (64)、または数値が大きすぎる場合 (512)、ハッシュテーブルエンコーディングに変換されます。
-
ハッシュテーブルエンコーディング【以下は重要なポイントですので割愛させていただきます】
-
ハッシュ関数、Redis 5.0 は SipHash アルゴリズムを使用します
-
ジッパー方式を使用してキーの競合を解決します (ハッシュの競合を解決します)。
-
リハッシュのタイミング
① 要素数 < 1 * バケット数の場合、拡張は必要ありません
② 要素数 > 5 * バケット数の場合、容量を拡張する必要があります
③ 1 * バケット数 <= 要素数 <= 5 * バケット数の場合、この時点で AOF または RDB 操作が実行されていない場合は、リハッシュが実行されます。
④要素数<バケット数/10の場合、サイズを小さくする
-
重要なポイントを蒸し返す
① 各ディクショナリには 2 つのハッシュ テーブルがあり、バケットの数は2 n 2^nです。2n、 ht[0] が通常使用され、 ht[1] は最初は null で、展開時の新しい配列サイズは要素数 * 2 になります。
②プログレッシブ再ハッシュ(1 回が長すぎるのを防ぐため)。つまり、すべてのバケットが一度に移行されるのではなく、毎回 1 つのバケットのみがこのテーブル CRUD に移行されます。
③アクティブ (アクティブ) リハッシュ、サーバーのメイン ループで、アクティブな移行のために 100 ミリ秒ごとに 1 秒を確保します。
④ リハッシュ処理では、新しい演算 ht[1] が追加され、他の演算はまず ht[0] を演算し、そうでない場合は ht[1] を演算します。
⑤ Redis 内のすべての CRUD はシングルスレッドであるため、再ハッシュはスレッドセーフである必要があります。
-
ソートされたセット
-
データ量が少ない場合はエンコードにziplistを使用し、スコア順に並べますが、キーや値の長さが長すぎる場合(64)や数値が大きすぎる場合(128)はスキップリスト+に変換されます。ハッシュテーブルエンコーディング。、両方を採用する理由は、
- ハッシュテーブルのみを使用すると、CRUD は O(1) になりますが、順序付けされた操作を実行するにはソートが必要となり、時間とスペースがさらに複雑になります。
- スキップリストのみを使用すると、範囲操作の利点は維持されますが、時間の複雑さが増加します。
- 2 つの構造体が同時に使用されますが、ポインターを使用するため、要素が 2 倍のメモリを占有することはありません。
-
スキップリストのポイント:複数レベルのリンクリスト、ソートルール、後方、レベル(スパン、前方)
- スコアはスコアを保存し、メンバーはデータを保存します。スコア順に並べ替えます。スコアが同じ場合はメンバー順に並べ替えます。
- backward は前のノード ポインタを保存します (逆方向のトラバーサルに便利)
- 各ノードにはレベル情報 (レベル) が格納されます。同じノードに複数のレベルがある場合があり、各レベルには属性があります。
- 同じレイヤー内の次のノードへの前方ポインタ (前方ポインタ、正の順序のトラバーサルに便利)
- スパンはランキングの計算に使用されます。すべてのジャンプ テーブルがスパンを実装しているわけではありません。これは Redis 実装に固有のものです。
-
マルチレベルのリンク リストによりクエリが高速化され、クエリの数が削減されます。、ルールは上から始めることです
-
同じレイヤーの右側のものより大きい場合は、引き続き同じレイヤーの右側を見てください。
-
同様に見つかりました
-
同じレイヤーの右側が小さいか、右側が NULL の場合は、次のレイヤーに移動して手順 1 と 2 を繰り返します。
-
- [Cui Ba] を検索する例を見てみましょう
- 一番上の (4) 層から右に [王五] ノードを見つけます (22 > 7)。右に進みますが、右側は NULL (次の層) です。
- [王五] ノードの右の 3 番目の層、22 < 37、次の層で [Sun Er] ノードを見つけます。
- [Wang Wu] ノードの 2 番目のレイヤーで、右に移動して [Zhao Liu] ノード、22 > 19 を見つけます。さらに右に進み、次のレイヤーの [Sun Er] ノード、22 < 37 を見つけます。
- 右側の [Zhao Liu] ノードの最初の層で [Cui Ba] ノードを見つけます (22 = 22)。
知らせ
- データ量が少ない場合、スキップテーブルの性能向上が反映されない スキップテーブルクエリの計算量はlog2(N) log_2(N)ログ_ _2( N )、バイナリ ツリーのパフォーマンスと同等
2. キーコマンドの問題
インタビューの質問: Redis には 1 億のキーがあります。keys コマンドを使用するとオンライン サービスに影響しますか?
必要とする
- シングルスレッド Redis に対する非効率なコマンドの影響を理解する
問題の説明
- Redis には 1 億のキーがあります。keys コマンドを使用するとオンライン サービスに影響しますか?
答え
- キーコマンドの時間計算量はO ( n ) O(n)ですO ( n )、 n はキーの総数です。 n が大きい場合、パフォーマンスは非常に低くなります。
- redis 実行コマンドは単一スレッドで実行されます。コマンドの実行が遅すぎると、他のコマンドがブロックされます。ブロック時間が長いと、redis がフェイルオーバーする可能性もあります。
改善計画【重要】
scan
コマンド置換keys
コマンド、構文、および戻り値を使用してscan 起始游标 match 匹配规则 count 提示数目
、次の開始点を表すことが できます。- スキャン コマンドの時間計算量は依然としてO ( n ) O(n)ですが、O ( n )ですが、カーソルを介して段階的に実行され、長いブロックは発生しません。
- count パラメータを使用すると、返されるキーの数を要求できます (デフォルトは 10)。
- 戻り値は次の開始点 (バケットの添字) を表します。
- 弱い状態。クライアントはカーソルを維持するだけで済みます。
- スキャンにより、リハッシュも正常に動作することを確認できます
- 欠点は、キーが繰り返し走査される可能性があること (縮小時) であり、アプリケーションは繰り返しキーを独自に処理する必要があることです。
3. 期限切れのキーの削除戦略
必要とする
- Redis がキーの有効期限を記録する方法を理解する
- マスター Redis の期限切れキーの削除戦略
レコードキーの有効期限
- 各ライブラリには以下が含まれます有効期限が切れる 辞書の有効期限が切れる
- ハッシュテーブル構造。キーは実際のキーを指すポインタ、値はミリ秒精度のロング型のタイムスタンプです。
- キーに有効期限が設定されている場合、このキーのポインタとタイムスタンプが有効期限ディクショナリに追加されます。
期限切れのキーの削除戦略
-
遅延削除
- データベースの読み取りまたは書き込みコマンドを実行する場合、コマンド実行前にキーの有効期限が切れているかどうかが確認され、有効期限が切れている場合はキーが削除されます。
-
定期的に削除する
-
Redis には、定期的なタスク処理を担当するスケジュールされたタスク プロセッサーの serverCron があり、デフォルトで 100 ミリ秒に 1 回実行されます (hz パラメーター制御)。これには、① 期限切れのキーの処理、② ハッシュ テーブルの再ハッシュ、③ 統計結果の更新、④ 永続化が含まれます。 , ⑤期限切れのお客様のクリーンアップ終了
-
期限切れのキーを処理する場合: ライブラリを順番に走査し、指定された時間 (デフォルトは 2.5 ミリ秒) 内に次の操作を実行します (毎回少しずつ削除します)。
① 各ライブラリの期限切れ辞書からランダムに 20 個のキーを選択してチェックし、期限切れの場合は削除します。
② 削除数が 5 に達した場合は手順①を繰り返し、削除数が 5 に達していない場合は次のライブラリに移動します。
③ 指定時間内に作業が完了しない場合は、次のserverCron操作を待ちます。
-
4. Redis の永続化
必要とする
- マスター AOF 永続性と AOF 書き換え
- マスター RDB 永続性
- ハイブリッド永続性について学ぶ
AOF の永続性
- AOF -各書き込みコマンドを aof ファイルに追加します。再起動時に、aof ファイル内の各コマンドが実行されてメモリ データが再構築されます。
- AOF ログは書き込み後のログです。つまり、コマンドが最初に実行され、その後ログが記録されます。
- パフォーマンスのための Redis、aof にログを記録するときにコマンドの構文チェックが行われないため、先にログを記録すると、構文エラーのあるコマンドがログに記録されます。
- AOF ログを記録する場合、3 つの同期戦略があります。
Always
同期書き込み [高セキュリティ、低パフォーマンス]、ログはディスクに書き込まれて返されます、基本的にデータは失われませんが、パフォーマンスは高くありません- 基本的には失われないのはなぜでしょうか? aof はserverCron イベント ループ内に書き込まれており、今回書き込まれるのは前のサイクルで aof バッファに一時的に保存されたデータであるため、最大 1 サイクルのデータが失われる可能性があります。
Everysec
1 秒ごとに書き込み、ログは AOF ファイルのメモリ バッファに書き込まれ、メモリ バッファのデータは 1 秒ごとにディスクにフラッシュされ、最大 1 秒のデータが失われます。No
オペレーティング システムは [高パフォーマンス、低セキュリティ] を書き込み、ログは AOF ファイルのメモリ バッファに書き込まれ、オペレーティング システムがいつデータをディスクにフラッシュするかを決定します。
AOFリライト
- 大きすぎる AOF ファイルによって引き起こされる問題の場合
- ファイルサイズはオペレーティングシステムによって制限されます
- ファイルが大きすぎて書き込み効率が悪くなります。
- ファイルが大きすぎるため、回復が非常に遅い
- 書き換えとは、同じキーに対する複数の操作をスリム化することです。
- たとえば、キーを 100 回変更した場合、100 件の変更ログが aof に記録されますが、実際には最後の 1 つだけが有効です。
- 書き換えには既存のaofログを操作する必要はなく、現在のメモリデータの状態に基づいて対応するコマンドを生成し、新しいログファイルに記録するだけで済みます。
- 書き換えプロセスは別のバックグラウンド サブプロセスによって完了され、メイン プロセスはブロックされません。
- AOF書き換え処理
- サブプロセスを作成すると、メイン プロセスに基づいてメモリ スナップショット (現在の状態を記録する) が生成されます。サブプロセスのメモリを走査し、各キーに対応するコマンドを新しいログ ファイルに書き込むだけです。 (つまり、ログを書き換えます)。
- この時点で新しいコマンドが実行されると、メイン プロセスのメモリが変更され、子プロセスのメモリには影響せず、新しいコマンドが記録されます。
重写缓冲区
- 子プロセスのすべてのキーが処理されるまで待ってから、
重写缓冲区
記録された増分命令を書き換えログに書き込みます。 - この間、古い AOF ログはまだ動作しており、書き換えが完了すると、古い AOF ログは書き換えログに置き換えられます。
RDBの永続性
- RDB - メモリ データ全体をバイナリ形式でディスクに書き込みます
- 対応するデータファイルは、
dump.rdb
- 回復が早いのがメリット
- 対応するデータファイルは、
- 関連するコマンドが 2 つあります
- save - メインプロセスで実行され、他のコマンドをブロックします
bgsave
- デフォルトの方法であるブロックを回避するために、実行用のサブプロセスを作成します。- 子プロセスはメインプロセスをブロックしませんが、子プロセスの作成中には引き続きブロックされ、メモリが大きいほどブロック時間は長くなります。
- bgsave はスナップショット機構も使用しており、RDB 永続化中に新しいデータが書き込まれると、新しいデータの変更はメインプロセスで発生し、子プロセスが古いデータを RDB ファイルに書き込むため、新しい変更は RDB に影響を与えません。操作する
- ただし、これらの新しいデータは RDB ファイルには追加されません[バックアップ中に変更があった場合、次のバックアップ中にマシンがダウンすると情報が失われます]
- デメリット:redis.confのsaveパラメータを調整することでrdbの実行周期を制御できますが、周期を把握するのが難しいです。
- 頻繁に実行すると、パフォーマンスに影響します。
- 時々実行すると、マシンがクラッシュしたときにさらに多くのデータが簡単に失われます。
ハイブリッド永続性
- Redis 4.0 以降、Redis はハイブリッド永続性をサポートします。つまり、RDB を完全バックアップとして使用し、AOF を 2 つの RDB 間の増分バックアップとして使用します。
- ハイブリッド永続性を有効にするかどうかを制御するために使用される構成項目
aof-use-rdb-preamble
。デフォルト値は no です。 - 永続化中、すべてのデータは AOF ログに保存され、ログの前半はバイナリ RDB 形式で、後半は AOF コマンド ログになります。
- 次回 RDB を実行すると、以前のログ ファイルは上書きされます。
- ハイブリッド永続性を有効にするかどうかを制御するために使用される構成項目
- 長所と短所
- これは、RDB と AOF の利点を組み合わせたもので、高速な回復速度、AOF で表される増分、より完全なデータ (同期戦略に応じて)、AOF の書き換えの必要がありません。
- 古いバージョンの Redis ファイル形式と互換性がない
5. キャッシュの問題
必要とする
- マスターキャッシュの浸透
- キャッシュ雪崩をマスターする
- マスターキャッシュの浸透
- マスターバイパスキャッシュとキャッシュコヒーレンス
キャッシュの内訳
-
キャッシュの内訳とは次のことを指します。特定のホットスポット キーがキャッシュとデータベースの両方に存在しますが、その有効期限が切れると、同時ユーザー数が多いため、キャッシュは同時に読み取られず、データベースも同時に読み取られ、データベースに負荷がかかります。
-
解決
- ホットスポットのデータは期限切れになりません【推奨】
- 右【クエリ キャッシュがありません。データベースにクエリを実行し、結果をキャッシュに入れます。】これらの 3 つの手順はロックの手順です。このとき、ロックを取得できるのは 1 つのクライアントだけであり、他のクライアントはブロックされます。ロックが解放されると、キャッシュにはすでにデータがあり、他のクライアントはデータベースにアクセスする必要はありません。ただし、スループットに影響します (損失のあるソリューション)
キャッシュ雪崩
-
ケース 1 :多数のキーに同じ有効期限が設定されているため (データはキャッシュとデータベースの両方に存在します)、有効期限に達すると、これらのキーはまとめて無効になり、これらのキーにアクセスするすべてのリクエストがデータベースに入ります。
特定の鍵をロックすれば雪崩を解決できるでしょうか? :いいえ -
解決:
- 有効期限をずらす: 有効期限にランダムな値を追加します(1~5分など)。
- サービスの低下: 非コア データ クエリ キャッシュを一時停止し、事前定義された情報 (エラー ページ、null 値など) を返します (損失の多いソリューション)
-
ケース 2 :Redis インスタンスがクラッシュし、大量のリクエストがデータベースに入りました。
-
解決:
- 注意事項: 高可用性クラスターを構築する
- マルチレベルキャッシュ (ローカルキャッシュを構築): 欠点は実装の複雑さが高いことです
- サーキットブレーカー: 監視によって雪崩が発生すると、インスタンスが復元され、事前定義された情報が返されるまでキャッシュアクセスが一時停止されます (損失のあるソリューション)
- 電流制限:データベースのアクセス量が監視により閾値を超えた場合、データベースへのアクセスリクエスト数を制限する(非可逆ソリューション)
キャッシュの侵入
-
キャッシュの侵入とは次のことを指します。キーがキャッシュまたはデータベースに存在しない場合、キーにアクセスすると毎回データベースにアクセスすることになります。
- 悪意のあるリクエストによって悪用される可能性がある
- キャッシュなだれとキャッシュ ブレークダウンの両方がデータベースに存在しますが、キャッシュが一時的に失われています。
- キャッシュなだれとキャッシュペネトレーションはどちらも自然に回復できますが、キャッシュペネトレーションは自然に回復できません。
-
解決
-
データベースにキーがない場合は、この存在しないキーに関連付けられた null 値もキャッシュに入れられます。、欠点は、そのようなキーにはビジネス機能がなく、無駄にスペースを占有することです。
-
ブルームフィルター(プラグイン)
キャッシュとデータベースの前に追加
①フィルターを使用すると、キーが存在しないかどうかを判断できます。存在しないキーを見つけた場合は、それらをフィルターで除外します。
②すべてのキーをブルーム フィルターにプリロードする必要がある
③ブルームフィルターは削除できないため、クエリで削除されたデータは確実に侵入してしまいます。(カッコーフィルターでこれを解決できます) -
キャッシュの一貫性の問題 - キャッシュのバイパス
キャッシュをバイパスする
-
キャッシュアサイドはキャッシュを使用するための一般的な戦略です
-
クエリルール
- ファーストキャッシュの読み取り
- 当たったらそのまま返す
- 欠落している場合は、DB (データベース) を確認し、結果をキャッシュに入れて返します。
-
ルールの追加、削除、変更
- 新しいデータを追加する、DBに直接保存(データベース)
- データの変更と削除、まずDB(データベース)を更新してからキャッシュを削除します[最終的には整合性。データの整合性要件が高くない場合、一時的に不整合が発生しますが、最終的には整合性が保たれます]
最初にライブラリを操作し、次にキャッシュを操作する必要があるのはなぜですか?
- 操作ライブラリとキャッシュの両方が正常に操作できると仮定すると、キャッシュを先に操作すると、データベースとキャッシュの間で不整合が発生する可能性が高くなります。
一貫性分析 - 最初にキャッシュをクリアしてから、ライブラリを更新します
一貫性分析 - 最初にライブラリを更新し、次にキャッシュをクリアします
-
一時的に不一致は発生しますが、最終的には一貫性が保たれます
-
クエリ スレッド A がデータをクエリするときに、時間切れによりキャッシュされたデータが無効になった場合、またはそれが最初のクエリであると仮定すると、上の図に示すように不整合が発生しますが、これが発生する可能性は非常に小さいです
。
ロックを使用して一貫性を解決する
- 短所: スループットに影響し、分散ロックの設計がより複雑になります。
6. キャッシュの原子性
必要とする
- Redis トランザクションの制限を理解する
- 原子性を確保するための楽観的ロックの使用について理解する
- Lua スクリプトを使用して原子性を確保する方法を理解する
Redis トランザクションの制限: ロールバックはサポートされていません
- 単一のコマンドはアトミックです、これは Redis シングルスレッドによって保証されます
- アトミック性を確保するために複数のコマンドを使用できますか
multi + exec
?
Redis はmulti + exec
ロールバックをサポートしていません。たとえば、初期データは次のとおりです
set a 1000
set b 1000
set c a
埋め込む
multi
decr a /*a减1*/
incr b /*b加1*/
incr c
exec
を実行するとincr c
、文字列が自動インクリメントをサポートしていないため、このコマンドは失敗しました。、しかし前の 2 つのコマンドはロールバックされません[上記の結果は、最初の 2 つのコマンドが成功し、最後のコマンドが失敗するということです。 ]
さらに重要なことには、multi + exec
の読み取り操作は意味がありません, 読み取り結果をその後の書き込み操作のために一時変数に代入できないためですmulti + exec
。読み取りと書き込みのアトミック性は保証されません(1 つのトランザクションで読み取りと書き込みを同時に制御することはできません)たとえば、初期データは次のとおりです
set a 1000
set b 1000
a と b が 2 つの口座の残高を表すと仮定し、古い値を取得して 500 の送金を実行します。
get a /* 存入客户端临时变量 */
get b /* 存入客户端临时变量 */
/* 客户端计算出 a 和 b 更新后的值 */
multi
set a 500
set b 1500
exec
ただし、他のクライアントが get と multi の間に a または b を変更すると、更新は失われます。
楽観的ロックによりアトミック性が保証される
watch
トランザクション中のkey
場合に (1 つ以上)を追跡するコマンド:key
- 他のクライアントによって変更されていない場合にのみ
exec
成功します。 - 別のクライアントによって変更された場合は、それが
exec
返されるnil
ため、アトミック性が保証されます。
前の例と同じ
get a /* 存入客户端临时变量 */
get b /* 存入客户端临时变量 */
/* 客户端计算出 a 和 b 更新后的值 */
watch a b /* 盯住 a 和 b */
multi
set a 500
set b 1500
exec
このとき、他のクライアントが a と b の値を変更すると、exec は nil を返し、設定された 2 つのコマンドは実行されませんが、このときクライアントはリトライすることができます。
Lua スクリプトは原子性を保証します
Redis は Lua スクリプトをサポートしています。これにより、Lua スクリプトの実行のアトミック性が確保され、置き換えることができます。multi + exec
たとえば、上記の問題を解決するには、次のコマンドを実行します。
eval "local a = tonumber(redis.call('GET',KEYS[1]));local b = tonumber(redis.call('GET',KEYS[2]));local c = tonumber(ARGV[1]); if(a >= c) then redis.call('SET', KEYS[1], a-c); redis.call('SET', KEYS[2], b+c); return 1;else return 0; end" 2 a b 500
- eval は lua スクリプトの実行に使用されます
- 2 は、スペースで区切られたパラメータのうち、最初の 2 つがキーで、残りが通常のパラメータであることを意味します。
- スクリプトでは、 を使用して
keys[n]
n 番目のキーを参照し、argv[n]
n 番目の通常のパラメータを参照できます。 - 二重引用符で囲まれたものは lua スクリプトで、次のようにフォーマットされています。
local a = tonumber(redis.call('GET',KEYS[1]));//tonumber把字符串转换成数字
local b = tonumber(redis.call('GET',KEYS[2]));
local c = tonumber(ARGV[1]);
if(a >= c) then
redis.call('SET', KEYS[1], a-c);
redis.call('SET', KEYS[2], b+c);
return 1;
else
return 0;
end
7. LRUキャッシュ(消去戦略)の実装
必要とする
- リンクリストに基づいた LRU キャッシュの実装をマスターする
- Redis の LRU キャッシュ実装の変更を理解する
LRUキャッシュ削除ルール
最も最近使用されていない、最も最近使用されていないキーをキャッシュから削除します。
- やがて、新しいものは残り、古いものは排除されます。
- キーにアクセスすると、最新のキーになります
実装戦略:
- リンク リスト方式では、最近アクセスされたキーはリンク リストの先頭に移動され、アクセス頻度の低いキーは自然にリンク リストの最後尾に配置されます。容量と数の制限を超えると、最後尾のキーは移動されます。削除されました。
- ランダム サンプリング方式 (Redis で使用)リンク リスト方式はより多くのメモリを消費します。Redis はランダム サンプリング方式を使用します。毎回 5 つのキーのみが選択されます。各キーは最新のアクセス時間を記録します。これら 5 つのキーの中から選択します。最も古いものは削除されます。
リンク リスト方式を例にとると、最近アクセスされたキーはリンク リストの先頭に移動され、アクセス頻度が低いキーは自然にリンク リストの最後に移動されます。容量と数の制限を超えると、キーはリンク リストの先頭に移動します。最後に削除されます。
-
たとえば、元のデータは次のとおりで、容量を 3 と指定します。
-
時間的には、新しいものは残り、古いものは削除されます。たとえば、d を入力すると、最も古い a が削除されます。
-
キーにアクセスすると、get b のように最新のキーになり、b はリンクされたリストの先頭に移動されます。
LRU キャッシュのリンク リストの実装 (コード テスト)
- ノードリンクを解除する方法
- ヘッドノードにリンクする方法
参照コード 1 : (このコードに回答すると、リンク リストについての理解が反映されます)
package day06;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LruCache1 {
static class Node {
//节点的参数;
Node prev;
Node next;
String key;
Object value;
public Node(String key, Object value) {
this.key = key;
this.value = value;
}
// 打印节点的信息:node的toString(prev <- node -> next)
public String toString() {
StringBuilder sb = new StringBuilder(128);
sb.append("(");
sb.append(this.prev == null ? null : this.prev.key);
sb.append("<-");
sb.append(this.key);
sb.append("->");
sb.append(this.next == null ? null : this.next.key);
sb.append(")");
return sb.toString();
}
}
//断开节点链接的方法(删除一个节点)
public void unlink(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
//把新节点加入到head头结点之后
public void toHead(Node node) {
node.prev = this.head;
node.next = this.head.next;
this.head.next.prev = node;
this.head.next = node;
}
int limit; //元素上限,超出则删除较老的
Node head; //头结点
Node tail; //尾节点
Map<String, Node> map; //Map集合存放真正的键值,键存放key,值存放每个节点对象
//初始化上面的这些成员
public LruCache1(int limit) {
this.limit = Math.max(limit, 2); //通过参数传进来,最小值设置成2;
this.head = new Node("Head", null);
this.tail = new Node("Tail", null);
head.next = tail;//头结点尾结点相连;
tail.prev = head;//头结点尾结点相连;
this.map = new HashMap<>();//空Map
}
//删除逻辑
public void remove(String key) {
Node old = this.map.remove(key);//调用底层map的remove将key删掉;
unlink(old);//断开节点的连接
}
//查询逻辑
public Object get(String key) {
Node node = this.map.get(key);
if (node == null) {
//如果key在链表中没有
return null;
}
//如果key在链表中有,在链表中断开这个节点,并把它插入到头部;
unlink(node);
toHead(node);
return node.value;//返回值;
}
//新增逻辑;
public void put(String key, Object value) {
Node node = this.map.get(key); //先查一下这个key在链表中有没有
if (node == null) {
//没有
node = new Node(key, value); //创建一个新的节点;
this.map.put(key, node);//存入map;
} else {
//有
node.value = value;//更新一下值
unlink(node); //断开连接
}
toHead(node); //将节点移入头部
if(map.size() > limit) {
//看看当前map的大小是否超出了上限,如果超出了
Node last = this.tail.prev;//找到最后一个节点;
this.map.remove(last.key);//将最后一个节点删掉;
unlink(last); //链表中断开最后一个节点的链接;
}
}
@Override
//cache的toString
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.head);//拼接头结点
Node node = this.head;
while ((node = node.next) != null) {
//遍历链表;
sb.append(node);
}
return sb.toString();
}
public static void main(String[] args) {
LruCache1 cache = new LruCache1(5); //创建 LruCache的实例,规定上限是5;
System.out.println(cache);
cache.put("1", 1);//添加元素
System.out.println(cache);
cache.put("2", 1);
System.out.println(cache);
cache.put("3", 1);
System.out.println(cache);
cache.put("4", 1);
System.out.println(cache);
cache.put("5", 1);
System.out.println(cache);
cache.put("6", 1);//此时将删除1结点
System.out.println(cache);
cache.get("2");//cache的get方法,此时2到达了头部
System.out.println(cache);
cache.put("7", 1);//cache的put方法,此时又将删除一个节点;
System.out.println(cache);
}
}
参照コード 2
親クラスからの継承を使用するLinkedHashMap
package day06;
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache2 extends LinkedHashMap<String, Object> {
private int limit; //作为元素个数的限制;
public LruCache2(int limit) {
// 1 2 3 4 false
// 1 3 4 2 true ,此时调用2,就把2调到了右侧头部;按访问顺序调整;
super(limit * 4 /3, 0.75f, true);//调用有参构造,参数:长度limit * 4 /3防止扩容,扩容因子,true
this.limit = limit;
}
@Override
//此方法把最老的键值对移除掉;返回true时则移除最老的
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
if (this.size() > this.limit) {
return true;
}
return false;
}
public static void main(String[] args) {
LruCache2 cache = new LruCache2(5);
System.out.println(cache);
cache.put("1", 1);
System.out.println(cache);
cache.put("2", 1);
System.out.println(cache);
cache.put("3", 1);
System.out.println(cache);
cache.put("4", 1);
System.out.println(cache);
cache.put("5", 1);
System.out.println(cache);
cache.put("6", 1);//此时由于新加了一个元素,超出了上限,会将最老的元素移除;
System.out.println(cache);
cache.get("2");
System.out.println(cache);
cache.put("7", 1);
System.out.println(cache);
}
}
Redis LRU キャッシュの実装
Redis はランダム サンプリング メソッドを使用しており、リンク リスト メソッドよりもメモリの使用量が少なくなります。毎回 5 つのキーのみがサンプリングされます。各キーは最新のアクセス時間を記録し、これら 5 つのキーのうち最も古いものが選択されて削除されます。
- たとえば、元のデータは次のようになり、容量は 160 と指定され、新しいキーを置きます
-
各キーには、LRU に格納された時刻が記録されており、ランダムに選択された 5 つのキー (16、78、90、133、156) のうち、最も古い時刻を持つものが削除されます (16)。
-
再度 b を入力すると、前のラウンドの残り 4 つのキー (78、90、133、156) にランダムなキー (125) が追加され、最も古いキー (78) が選択されます。
- キーが取得されると、そのアクセス時間が更新され (下図の 90)、次のラウンドでキーが削除されるのを防ぎます。