簡略まとめ#
- Redis の理解
- メモリーデータベース
- キャッシュ、メッセージキュー、分散ロックによく使用される
- 原子操作
- Redis の値のデータ構造(5 つの一般的なもの)
- String 文字列:キャッシュオブジェクト、一般的なカウント、分散ロック、共有セッション情報など
- List リスト(連結リスト):メッセージキューなど
- Hash ハッシュ(無秩序ハッシュテーブル):キャッシュオブジェクト、ショッピングカートなど
- Set 集合(文字列の無秩序集合):集計計算(和、交差、差)シナリオ、例えばいいね、共通のフォロー、抽選イベントなど
- Zset 順序集合:ソートシナリオ、例えばランキング、電話番号と名前のソートなど
- …(他に 4 つ、今は書かない)
- Redis スレッドモデル
- 単一スレッドとは:
- [クライアントリクエストの受信> 解析 > データの読み書き > クライアントへのデータ送信] をメインスレッドが行う。
- Redis プログラムは単一スレッドではない
- 6.0 前後
- 前 - 単一スレッド:メンテナンスが容易、高い、ロック問題なし
- 後 - マルチスレッド:複数の I/O スレッドを使用してネットワークリクエストを処理(ネットワーク I/O のパフォーマンスボトルネックのため)
- まとめ:コマンドの実行は依然として単一スレッドであり、ネットワーク I/O 処理性能を向上させるためにマルチスレッドを導入
- 単一スレッドとは:
- Redis の永続化
- AOF ログ:コマンドを実行後、コマンドを AOF ファイルに書き込み、再起動時にそのファイルをロード
- 戦略:
- Always:コマンド実行後に同期的に書き込む
- Everysec:コマンドをカーネルバッファに書き込み、毎秒バッファの内容をディスクに書き込む
- NO:オペレーティングシステムがバッファの内容をディスクに書き込むタイミングを制御
- 大きくなった場合:AOF 再書き込みメカニズムをトリガー
- 欠点:ファイルが大きすぎると、ロード時間が長くなる
- 戦略:
- RDB スナップショット:特定の時点のメモリデータ(頻度は制御可能)をバイナリ形式でディスクに書き込む
- 方法:
- save コマンド:メインスレッドによって生成され、スレッドをブロックする
- bgsave コマンド:子プロセスを作成して生成し、スレッドをブロックしない
- 欠点:データ損失のリスクが高い
- 方法:
- ハイブリッド:4.0 で追加され、AOF の復元が遅い、RDB のデータ損失リスクが高いという問題を解決し、両者の利点を組み合わせる
- AOF ログ:コマンドを実行後、コマンドを AOF ファイルに書き込み、再起動時にそのファイルをロード
- Redis クラスター
- 方法:
- 主従複製:主が従に同期(初回全量、以降増分)、主が書き込み、従が読み込み
- センチネルモード:主従複製の基礎の上に、センチネル監視を追加し、主、従、センチネルの健康状態を監視し、自動フェイルオーバーを行う
- シャーディングクラスター:データを分散して異なるサーバーに配置し、単一障害依存を減らし、読み書き性能を向上させる
- クラスターの脳裂:ネットワーク分割(異なるネットワーク下)による二重主
- 解決策:
- min-slaves-to-write x、少なくとも x 個の従が主に接続されていることを制限し、下回ると主は書き込みを禁止
- min-slaves-max-lag x、主従データ同期の遅延が x 秒未満であることを制限し、超過すると主は書き込みを禁止
- 解決策:
- 方法:
- Redis の期限切れ削除とメモリ淘汰
- 期限切れ削除:期限切れ辞書と比較し、未期限切れのデータを返し、期限切れのものは削除
- 削除戦略:
- 遅延削除:キーにアクセスする際に期限切れかどうかを検出し、期限切れであればキーを削除して null を返す
- 定期削除:定期的にランダムに一定数のキーを取得してチェックし、その中の期限切れを削除
- プロセス:キーを抽出 → 期限切れかどうかをチェックし、削除 → 期限切れの数が 25%を超えた場合、前のステップを繰り返す → 25%未満の場合、次のチェックを待つ
- 永続化時の期限切れキー処理:
- AOF:
- 期限切れキーに削除命令を AOF ファイルに追加
- ロード時に期限切れキーは AOF に書き込まれない
- RDB:
- RDB 生成時に期限切れキーを無視
- ロード時に主の期限切れは読み込まず、従はすべて読み込む
- AOF:
- 主従の期限切れキー処理:従は主に依存(期限切れキーをスキャンしない)、主が期限切れキーを削除した後に従に同期
- メモリ満載:閾値に達するとメモリ淘汰メカニズムをトリガーし、maxmemory を設定
- メモリ淘汰メカニズム戦略:合計8種類、[淘汰しない] と [淘汰する] の 2 つのカテゴリに分類
- 淘汰しない:閾値を超えると、直接エラーを返す
- 淘汰する:[期限切れ時間が設定されたデータの中] と [すべてのデータ] の 2 つのカテゴリで淘汰し、最も良いのは期限切れ時間が設定されていて使用率が最低のキー
- Redis キャッシュ設計
- 雪崩:データの一貫性のために、キャッシュデータに期限切れ時間を設定し、期限切れ後にデータベースからリクエストしてキャッシュを再更新するが、大量のキーが期限切れになると、リクエストが直接 DB に行き、ダウンする。あるいは Redis がダウンする。
- 解決策:
- 期限切れ:
- 期限切れ時間を分散させる
- ミューテックスロック:同時に 1 つのビジネススレッドのみがキャッシュを更新
- ダブルキー(主が期限切れ、バックアップが永久)
- ダウン:
- サービスの熔断
- リクエストの制限
- 高可用性
- 期限切れ:
- 解決策:
- 撃破:ホットデータが期限切れ、リクエストが直接 DB に行く(雪崩のサブセットの状況)ためにダウンする
- 解決策:
- ミューテックスロック:同時に 1 つのビジネススレッドのみがキャッシュを更新
- 期限切れ時間を設定しない
- 解決策:
- 透過:データがキャッシュにも DB にも存在しない、一般的にはビジネスの誤操作と悪意のある攻撃の 2 つ
- 解決策:
- 不正リクエストを制限
- デフォルト値をキャッシュ
- ブルームフィルターを使用してデータの存在を判断し、DB の検索を避ける
- 解決策:
- 動的キャッシュホットデータ:データの最新アクセス時間でソートし、トップデータを残す
- 一般的なキャッシュ更新戦略:
- Cache Aside バイパスキャッシュ(実際の開発で使用され、最も一般的で、読み取りが多く書き込みが少ないのに適している):アプリケーションプログラムが直接 [データベース、キャッシュ] と対話し、キャッシュの維持を担当
- 書き込み戦略:まず DB を更新し、その後 Cache を削除(順序を変えないこと、そうでないとデータの不整合が生じ、削除は遅延読み込みの考え)
- 読み取り戦略:
- キャッシュがある場合、直接データを返す
- キャッシュがない場合、DB を読み込み、Cache に書き込み、データを返す
- Read/Write Through 読み込み透過 / 書き込み透過
- Write Back 書き戻し:データ損失のリスクがある
- Cache Aside バイパスキャッシュ(実際の開発で使用され、最も一般的で、読み取りが多く書き込みが少ないのに適している):アプリケーションプログラムが直接 [データベース、キャッシュ] と対話し、キャッシュの維持を担当
- Cache と DB のデータ一貫性を保証:データベースを更新 + キャッシュを更新(分散ロックを使用し、短い期限切れ時間を加える)
- 雪崩:データの一貫性のために、キャッシュデータに期限切れ時間を設定し、期限切れ後にデータベースからリクエストしてキャッシュを再更新するが、大量のキーが期限切れになると、リクエストが直接 DB に行き、ダウンする。あるいは Redis がダウンする。
- Redis 実践
- 遅延実行キュー: ZSet(順序集合)を使用し、Score 属性に遅延実行の時間を格納し、zrangebysocre を利用して時間を比較して処理待ちのものを見つける
- Redis の大きなキー:キーの値が非常に大きいことを指す
- 条件:
- String 型の値が 10KB を超える
- Hash、List、Set、ZSet 型の要素が 5k を超える
- 影響:
- クライアントのタイムアウトブロック:Redis は単一スレッドでコマンドを実行し、大きなキーの操作に時間がかかり、スレッドをブロックする
- ネットワークの混雑を引き起こす:データ * アクセス量 = 大流量を生む
- 作業スレッドをブロックする:大きなキーを削除すると、作業スレッドがブロックされる
- メモリの分布が不均一:クラスターモデルがスロットのシャーディングで均一な場合に、データとクエリの偏りが発生し、一部の大きなキーを持つノードが多くのメモリを占有し、QPS が大きくなる
- 調査:
redis-cli --bigkeys
- 注意:
- 従ノードで実行すること、スレッドをブロックするため
- 従ノードがない場合は低ピーク時間に実行し、
-i
を使用してスキャン間隔を制御し、パフォーマンスへの影響を減らす
- 注意:
SCAN
:まず SCAN でスキャンし、次に TYPE でキーのタイプを取得- String 型:STRLEN を使用して長さを取得
- セット型:
- 平均サイズ * 数量
MEMORY USAGE
でキーが占有するスペースを確認(Redis 4.0+)
- https://github.com/sripathikrishnan/redis-rdb-tools:RDB ファイルを解析
- 削除:直接削除すると大量のメモリが解放されるが、同時に空きメモリブロックのリスト操作時間が増加し、Redis のメインスレッドがブロックされ、さまざまなリクエストがタイムアウトし、最終的に Redis 接続が枯渇し、さまざまな例外が発生する
- 方法:
- バッチ処理
- 大きな Hash:
hscan
を使用して 100 フィールドを取得し、hdel
で一つずつ削除 - 大きな Set:
sscan
を使用して 100 要素をスキャンし、srem
で一つずつ削除 - 大きな ZSet:
zremrangebyrank
を使用してトップ 100 要素を削除
- 大きな Hash:
- 非同期(Redis 4.0+):
del
の代わりにunlink
を使用- 方法:
unlink
コマンドを手動で呼び出す- 条件を満たすときに非同期削除を設定
- 方法:
- バッチ処理
- 方法:
- 条件:
- Redis パイプライン(Pipeline):サーバーではなくクライアントが提供するバッチ処理技術であり、複数の Redis コマンドを一度に処理し、複数のコマンド実行時のネットワーク待機を解決する
- Redis トランザクションはロールバックをサポートしない:トランザクション実行中のエラーのトランザクションロールバックをサポートせず、
DISCARD
のみでトランザクション実行を放棄する- サポートしない理由:
- 著者は本番環境でエラーが発生することは少ないと考え、開発の必要がないと判断
- トランザクションロールバックは複雑で、Redis のシンプルで効率的な設計に合わない
- サポートしない理由:
- 分散ロック:分散環境での同時制御のメカニズムであり、リソースが同時に 1 つのアプリケーションによってのみ使用されることを制御する
-
原理:
SET
コマンドのNX
パラメータは [key が存在しない場合にのみ挿入] を実現できる- キーが存在しない:挿入成功を表示し、ロック成功を示す
- キーが存在する:挿入失敗を表示し、ロック失敗を示す
-
ロック条件:
- ロックはロック変数に対して [読み取り、チェック、設定] の 3 つの操作を行う必要があり、原子操作を完了する必要があるため、
SET
+NX
を使用 - ロック変数には期限切れ時間を設定する必要があり、
EX/PX
オプションを使用し、単位はミリ秒 - ロック変数の値はクライアントのユニークな識別子で出所を区別
# 3つの条件を満たすコマンド: SET lock_key unique_value NX PX 10000 - lock_key: キーの値 - unique_value: クライアントのユニークな識別子 - NX: lock_keyが存在しないときにのみ設定 - PX 10000: lock_keyの期限切れ時間を10秒に設定
- ロックはロック変数に対して [読み取り、チェック、設定] の 3 つの操作を行う必要があり、原子操作を完了する必要があるため、
-
ロック解除:2 つの操作が必要で、Lua スクリプトを使用して原子性を保証し、まず値が等しいかどうかを比較し、その後キーを削除
// ロックを解除する際、まずunique_valueが等しいかを比較し、ロックの誤解除を避ける if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
-
利点:
- 高性能
- 実装が簡単
- 単一障害を回避(Redis クラスターのデプロイ)
-
欠点:
- タイムアウト時間の設定が難しい:長すぎるとパフォーマンスに影響し、短すぎるとロック保護を失う
- Redis の主従複製モードではデータが非同期に複製されるため、分散ロックの信頼性が低下する可能性がある。
-
クラスター下の信頼性:公式は分散ロックアルゴリズム Redlock(レッドロック)を設計した。複数の Redis ノードに基づく分散ロックで、公式は少なくとも 5 つの孤立した主ノードを推奨
- 原理:クライアントが複数の独立した Redis ノードに順次ロックを要求し、過半数以上のノードでロックが成功すれば、クライアントのロック成功と見なす
- プロセス(ロック成功 = 過半数がロックを取得し、総時間 < ロックの有効時間):
- クライアントが現在の時間を取得(t1)
- クライアントが順番に N 個のノードにロックを実行
- クライアントが過半数(N/2+1 以上)のノードからロックを成功裏に取得した場合、再度現在の時間を取得(t2)、その後経過時間を計算(t2-t1)、t2-t1 < ロックの期限切れ時間であれば、ロック成功と見なす
-