DUICUO

[経験共有] 分散システムに Redis が不可欠なのはなぜですか?

ビジネス ロジックを記述するほとんどのプログラマーは、実際の開発で Redis を使用するときに Set Value 操作と Get Value 操作しか知らないため、Redis に関する包括的な理解が欠けています。

したがって、皆さんの知識のギャップを埋めるために、Redis の一般的な問題を要約することにしました。

この記事では、以下の点について詳しく説明します。

  • Redis を使用する理由
  • Redis を使用することの欠点は何ですか?
  • シングルスレッドの Redis がなぜこんなに高速なのでしょうか?
  • Redis のデータ型とその使用例。
  • Redisの有効期限ポリシーとメモリ削除メカニズム
  • Redis とデータベースの二重書き込みの一貫性の問題
  • キャッシュ侵入とキャッシュアバランシェ問題への対処方法
  • Redis での同時キー競合の問題を解決するにはどうすればよいですか?

Redis を使用する理由

プロジェクトで Redis を使用する際の主な考慮事項は、パフォーマンスと同時実行性だと思います。

もちろん、Redisには分散ロックなどの他の機能もありますが、これらの機能だけが必要な場合は、Zookeeperなどの代替ミドルウェアオプションがあり、Redisは必須ではありません。したがって、この質問には、主にパフォーマンスと同時実行性の観点から答えるべきです。

パフォーマンス

下の図に示すように、実行に非常に時間がかかり、結果が頻繁に変更されないSQLクエリに遭遇した場合、結果をキャッシュすることが特に適しています。これにより、後続のリクエストはキャッシュから読み込まれるため、応答時間が短縮されます。

余談ですが、突然ですが、迅速な対応の基準についてお話ししたくなりました。交互作用効果にもよりますが、対応時間には決まった基準はありません。

しかし、かつて誰かが私にこう言いました。「理想的には、ページジャンプは瞬時に解決される必要があり、ページ内操作も瞬時に解決される必要があります。」

さらに、指をスナップするよりも長い時間がかかる操作には進行状況インジケーターが必要であり、ユーザーに最高のエクスペリエンスを提供するためにいつでも停止またはキャンセルできるようにする必要があります。

では、一瞬、瞬間、あるいは指を鳴らす動作には、実際どれくらいの時間がかかるのでしょうか?

*マハサンギカ ヴィナヤ* によれば、次のようになります。

一つの瞬間は一つの考え、二十の考えは一つの瞬間、二十の瞬間は一つの指鳴らし、二十の指鳴らしは一つの莖玉、二十莖は一つの徐玉、そして一昼夜には三十の徐玉がある。

したがって、慎重に計算すると、瞬間は 0.36 秒、瞬間は 0.018 秒、指を鳴らす時間は 7.2 秒になります。

同時

下の図に示すように、同時実行性が高い場合、すべてのリクエストがデータベースに直接アクセスするため、接続エラーが発生する可能性があります。

この時点で、データベースに直接アクセスするのではなく、最初に Redis にアクセスするリクエストを許可するために、Redis をバッファとして使用する必要があります。

Redis を使用することの欠点は何ですか?

Redisを長年使用してきた方なら、これは必ず理解しておくべき問題です。基本的に、Redisを使用する際には誰もが何らかの問題に遭遇しますが、よくある問題はごくわずかです。

答えは主に次の 4 つの質問から構成されます。

キャッシュとデータベース書き込み間の一貫性の問題

  • キャッシュアバランシェ問題
  • キャッシュ故障の問題
  • キャッシュの同時実行の問題
  • これら 4 つの問題は、プロジェクトで頻繁に発生する問題であると私は考えており、具体的な解決策については後ほど説明します。

シングルスレッドの Redis がなぜこんなに高速なのでしょうか?

この質問は、Redisの内部メカニズムに関する理解度を測るものです。私の面接経験からすると、Redisがシングルスレッドモデルで動作することを知らない人が多いようです。そのため、この質問は確認する価値があります。

その答えは主に以下の3点になります。

  • 純粋なメモリ操作
  • シングルスレッド操作により、頻繁なコンテキスト切り替えを回避できます。
  • 非ブロッキングI/O多重化機構を採用しています。

余談ですが、ここでは I/O 多重化について詳しく説明します。この用語は非常に一般的なので、ほとんどの人はその意味を理解していないからです。

例:シャオ・クはS市に宅配便店を開き、市内の速達サービスを提供しています。資金が限られていたため、シャオ・クは宅配便業者を数人雇いましたが、資金が不足していることが判明し、配達用の車両を1台しか購入できませんでした。

ビジネスモデル1

顧客が荷物を配達するたびに、Xiao Qu は宅配業者に荷物を監視させ、その後宅配業者が車で荷物を配達しに行きます。

徐々に、Xiao Qu はこのビジネス モデルに次のような問題があることを発見しました。

  • 数十人の配達員のほとんどは、車を探すのに時間を費やし、ほとんどが手持ち無沙汰になっていた。なんとか車を手に入れた人が荷物を配達できるのだ。
  • 速達便の増加に伴い、配達員も増え続けています。小曲さんは、速達店がますます混雑し、新しい配達員を雇う余裕がないことに気づきました。
  • 配達員間の調整には多くの時間がかかります。

上記の欠点を慎重に検討した後、Xiao Qu は次のビジネス モデルを提案しました。

ビジネスモデル2

Xiao Quは配達員を1人だけ雇っています。顧客が荷物を送ると、Xiao Quは配達先住所に応じて荷物に印を付け、一箇所にまとめて保管します。

配達員は荷物を一つずつ取りに行き、配達のために車で出発し、配達が終わると次の荷物を取りに戻りました。

上記の 2 つの操作方法を比較すると、2 番目の方法の方が明らかに効率的で優れているのではないでしょうか。

上記の例えでは、

  • 各配達員 → 各スレッド
  • 各パッケージ → 各ソケット(I/Oストリーム)
  • 荷物の配達場所→ソケットの状態の違い
  • 顧客の荷物配送依頼→依頼者からの依頼
  • Xiao Quのビジネスモデル → サーバーサイドコード
  • 車 → CPUコアの数

したがって、次のように結論付けることができます。

  • 最初の動作方法は、各 I/O フロー (配信) が新しいスレッド (クーリエ) によって管理される従来の並行モデルです。
  • 2つ目の動作方法はI/O多重化です。単一のスレッド(クーリエ)が、各I/Oストリームの状態(各パッケージの配送先)を追跡することで、複数のI/Oストリームを管理します。

実際の Redis スレッド モデルとの類似性は、図に示すように次のようになります。

簡単に言うと、redis クライアントが動作すると、さまざまなイベント タイプのソケットが生成されます。

サーバー側には、イベントをキューに格納するI/O多重化プログラムがあります。その後、ファイルイベントディスパッチャがキューからイベントを1つずつ取得し、異なるイベントハンドラーに転送します。

なお、Redis は、この I/O 多重化メカニズム用に select、epoll、evport、kqueue などの多重化関数ライブラリも提供しており、これらを独自に調べることができます。

Redis のデータ型とその使用例。

この質問は初歩的な質問に思えますか?私もそう思いました。しかし、私の面接経験から言うと、少なくとも80%の人は答えられません。

私の提案は、まずプロジェクトで実際に使ってみて、それから類似点を見出しながら記憶し、より深く理解することです。丸暗記しようとしないでください。基本的に、有能なプログラマーであれば、5つのタイプすべてを使えるはずです。

これについては特に説明することはありません。最も基本的なset/get操作です。値は文字列または数値です。通常は複雑なカウント関数のキャッシュに使用されます。

ハッシュ

ここで、Value は構造化されたオブジェクトを格納し、その中の特定のフィールドを操作するのに便利になります。

シングル サインオンを実装したとき、CookieId をキーとして使用し、30 分のキャッシュ有効期限を設定して、このデータ構造を使用してユーザー情報を保存し、セッションのような効果を効果的にシミュレートしました。

リスト

Listデータ構造を使用することで、シンプルなメッセージキュー機能を実装できます。また、`lrange`コマンドを使用してRedisベースのページネーションを実装できるという利点もあり、優れたパフォーマンスと優れたユーザーエクスペリエンスを実現します。

セット

Set は一意の値のコレクションを格納するため、グローバルな重複排除に使用できます。重複排除には、JVM 組み込みの Set を使用するのがよいでしょう。

当社のシステムは一般的にクラスタ構成になっているため、JVM 組み込みの Set を使用するのは非常に面倒です。グローバル重複排除を実行するためだけに、新たにパブリックサービスを起動するのは非常に面倒です。

さらに、積、和、差などの演算を使用することで、共通の好み、すべての好み、固有の好みを計算することができます。

ソートされたセット

ソート セットには追加の重みパラメータ「スコア」があり、これによりセット内の要素をスコアに従ってソートすることができます。

リーダーボードアプリケーションで上位N件の結果を取得するために使用できます。ソートセットは遅延タスクに使用できます。*** 例として、範囲検索が挙げられます。

Redisの有効期限ポリシーとメモリ削除メカニズム

これは非常に重要な質問です。Redis が効果的に使用されているかどうかがわかります。

例えば、Redisインスタンスが5GBのデータしか保存できないのに、10GBを書き込むと5GBのデータが削除されます。この削除がどのように行われるか考えたことがありますか?

また、データの有効期限を設定しているにもかかわらず、有効期限を過ぎてもメモリ使用量がかなり高いままです。その理由についてご検討いただけましたか?

回答: Redis は、定期的な削除と遅延削除の戦略を組み合わせて使用​​します。

時間制限付き削除戦略を使用しないのはなぜですか?

スケジュール削除では、タイマーを使用してキーを監視し、有効期限が切れると自動的に削除します。これによりメモリは速やかに解放されますが、CPUリソースを大量に消費します。

同時実行性が高い場合、CPU はキーを削除するのではなく、リクエストの処理に時間を費やす必要があるため、この戦略は採用されませんでした。

スケジュールされた削除と遅延削除はどのように機能しますか?

定期的な削除: Redis は、デフォルトで 100 ミリ秒ごとに期限切れのキーをチェックし、必要に応じて期限切れのキーを削除します。

Redis は 100 ミリ秒ごとにすべてのキーをチェックするのではなく、チェックするキーをランダムに選択することに注意してください (すべてのキーを 100 ミリ秒ごとにチェックすると、Redis がフリーズします)。

したがって、定期的な削除戦略のみを使用すると、多くのキーが期限までに削除されない可能性があります。ここで遅延削除が役立ちます。

つまり、キーを取得する際に、Redis はキーに有効期限が設定されている場合、そのキーが期限切れかどうかを確認します。期限切れの場合は、キーは削除されます。

定期削除と遅延削除を組み合わせて使用​​すると、他のすべての問題が解消されますか?

いいえ、スケジュールされた削除時にキーが削除されず、すぐにキーの返却を要求しない場合、つまり遅延削除が効果を発揮しない場合、Redis のメモリ使用量は急激に増加します。その場合は、メモリエビクションメカニズムを実装する必要があります。

redis.conf には次の設定行があります:

  1. # 最大メモリポリシー 揮発性 LRU

この構成は、メモリ削除ポリシーを設定するためのものです (まだ構成していないのですか? アプローチを真剣に検討する必要があります)。

  • `noeviction`: 新しく書き込まれたデータを保持するのに十分なメモリがない場合、書き込み操作でエラーが報告されます。これを使用している人は少ないでしょう。
  • allkeys-lru: 新しく書き込まれたデータを格納するためのメモリが不足している場合、キー空間から最も最近使用されていないキーを削除します。推奨。現在プロジェクトで使用されています。
  • `allkeys-random`: 新しく書き込まれたデータを格納するためのメモリが不足している場合、キー空間からキーをランダムに削除します。おそらく誰もこれを使用しないでしょう。最も使用頻度の低いキーを削除するのではなく、ランダムにキーを1つ削除するでしょう。
  • `volatile-lru`: 新しく書き込まれたデータを格納するためのメモリが不足している場合、最も古いキーが有効期限付きでキー空間から削除されます。これは通常、Redisをキャッシュと永続ストレージの両方として使用する場合に使用されます。推奨されません。
  • volatile-random: 新しく書き込まれたデータを格納するためのメモリが不足している場合、有効期限が設定されているキー空間からキーがランダムに削除されます。ただし、推奨されません。
  • volatile-ttl: 新しく書き込まれたデータを格納するためのメモリが不足している場合、有効期限が設定されているキー空間から、有効期限が古いキーが削除されます。推奨されません。

PS: 有効期限が切れるキーが設定されていない場合、前提条件は満たされません。したがって、volatile-lru、volatile-random、および volatile-ttl 戦略の動作は、基本的に noeviction (削除なし) と同じです。

Redis とデータベースの二重書き込みの一貫性の問題

一貫性の問題は分散システムにおいて一般的な問題であり、さらに結果整合性と強い一貫性に分類できます。データベースとキャッシュへの二重書き込みは、必然的に不整合の問題を引き起こします。

この質問に答えるには、まず一つの前提を理解する必要があります。データに強い一貫性が必要な場合、キャッシュは選択肢ではありません。私たちが行うすべてのことは、結果的一貫性しか保証できないということです。

さらに、当社のソリューションは不整合の可能性を低減するだけで、完全に排除することはできません。そのため、強い整合性が求められるデータはキャッシュできません。

回答:まず、正しい更新戦略を採用してください。まずデータベースを更新し、次にキャッシュを削除します。次に、キャッシュの削除に失敗する可能性があるため、メッセージキューなどの補償策を用意してください。

キャッシュ侵入とキャッシュアバランシェ問題への対処方法

率直に言って、これらは中小規模の従来型ソフトウェア企業ではほとんど遭遇しない2つの問題です。しかし、トラフィックが数百万に達するような高同時実行プロジェクトの場合は、これら2つの問題を慎重に検討する必要があります。

キャッシュ侵入は、ハッカーがキャッシュに存在しないデータを意図的に要求し、すべての要求がデータベースにアクセスしてデータベース接続エラーが発生する場合に発生します。

キャッシュ侵入ソリューション:

  • ミューテックスロックを使用すると、キャッシュの有効期限が切れると、システムはまずロックの取得を試みます。ロックが取得されると、データベースへのリクエストが発行されます。ロックが取得されない場合、システムは一定時間待機してから再試行します。
  • 非同期更新戦略を採用し、キーが取得されたかどうかに関係なく値を返します。値にはキャッシュの有効期限が保持され、キャッシュの有効期限が切れると、非同期スレッドが開始され、データベースから読み取り、キャッシュを更新します。キャッシュの事前読み込み(プロジェクト起動前にキャッシュをロードすること)が必要です。
  • リクエストの有効性を迅速に判断するメカニズムを提供します。例えば、ブルームフィルタを用いて有効なキーの系列を内部的に保持するなどです。リクエストに含まれるキーが有効かどうかを迅速に判断します。無効な場合は、無効なキーを直接返します。

キャッシュアバランシェは、多数のキャッシュが同時に期限切れになったときに発生します。次のリクエストの波が到来すると、それらすべてがデータベースに送信され、データベース接続障害につながります。

キャッシュアバランシェソリューション:

  • 一括的な有効期限切れを回避するために、キャッシュの有効期限にランダムな値を追加します。
  • ただし、ミューテックスを使用すると、スループットが大幅に低下します。
  • デュアルキャッシュ。キャッシュAとキャッシュBの2つのキャッシュがあります。キャッシュAの有効期限は20分ですが、キャッシュBには有効期限がありません。キャッシュの事前加熱は、独自に実行します。

次に、以下の点に分解します。データベースからキャッシュAからデータを読み取り、存在する場合はそのデータを直接返します。Aにデータが存在しない場合は、キャッシュBから直接データを読み取り、それを直接返します。そして、非同期で更新スレッドを開始します。更新スレッドは、キャッシュAとキャッシュBの両方を同時に更新します。

Redis での同時キー競合の問題を解決するにはどうすればよいですか?

問題は大まかに言うと、複数のサブシステムが同時に単一のキーを設定していることです。このような状況では、どのような予防策を講じるべきでしょうか?

事前に Baidu で簡単に検索してみたところ、ほとんどの回答で Redis のトランザクション メカニズムの使用が推奨されていることが分かりました。

Redisのトランザクションメカニズムの使用は推奨しません。当社の本番環境は、主にデータシャーディングを備えたRedisクラスタ環境です。

トランザクションが複数のキーの操作を伴う場合、これらのキーは必ずしも同じRedisサーバーに保存されるとは限りません。そのため、Redisのトランザクションメカニズムは実用的ではありません。

このキーに対して操作を実行する場合、順序は必要ありません。

このシナリオでは、分散ロックを準備し、参加者がロックをめぐって競争することができます。ロックが取得されると、比較的単純なセット操作を実行できます。

このキーに対する操作を次の順序で実行する必要がある場合...

key1 があるとします。システム A は key1 を valueA に設定する必要があり、システム B は key1 を valueB に設定する必要があり、システム C は key1 を valueC に設定する必要があります。

key1の値は、valueA > valueB > valueCの順に変化することが期待されます。この場合、データベースにデータを書き込む際にタイムスタンプを保存する必要があります。

タイムスタンプが次のとおりであると仮定します。

  • システムAキー1 {valueA 3:00}
  • システムBキー1 {valueB 3:05}
  • システムCキー1 {valueC 3:10}

例えば、システムBが最初にロックを取得し、key1を{valueB 3:05}に設定するとします。次にシステムAがロックを取得し、valueAのタイムスタンプがキャッシュ内のタイムスタンプよりも古いため、設定操作を実行しません。以下、同様の処理が繰り返されます。

キューを使って`set`メソッドを逐次アクセスするといった他の方法も可能です。つまり、柔軟性と適応性を持つことが重要です。

要約

この記事では、Redis に関するよくある質問をまとめています。そのほとんどは、私が仕事で遭遇した質問や、面接でよく聞く質問です。

また、直前に詰め込むのはお勧めしません。経験豊富なエンジニアの質問は、あなたを簡単に困惑させる可能性があります。お役に立てれば幸いです!