【Redis 内部構造の闇】RANDOMKEYはなぜ本番環境で「地雷」になり得るのか?
テックリードの私だ。コードレビューや設計レビューの場で、若手エンジニアから「データベースからランダムにセッションやキャッシュを1つ選びたいので、`RANDOMKEY`を使います」という提案が上がってきたら、君ならどうする?
「お、いいですね、採用!」と言ってしまったら、君のアーキテクチャ設計のセンスは今すぐアップデートが必要だ。
一見、何気なく使えそうなこの`RANDOMKEY`コマンドだが、Redisの内部データ構造とメモリ管理のメカニズムを理解していないと、本番環境で突然のレイテンシスパイクを引き起こし、最悪の場合はクラスタ全体を巻き込む障害のトリガーになる。
今回は、この`RANDOMKEY`の正体を、Redisのソースコードレベルの仕組みから実務での正しい設計パターンまで、徹底的に剥き出しにして解説しよう。
—
1. `RANDOMKEY` の基本と、誰も教えてくれない「残酷なアルゴリズム」
まずは基本だ。`RANDOMKEY`は、現在選択されているRedisデータベースから、ランダムにキーを1つ返却する。
127.0.0.1:6379> SET user:101 “Alice”
OK
127.0.0.1:6379> SET user:102 “Bob”
OK
127.0.0.1:6379> RANDOMKEY
“user:101”
127.0.0.1:6379> RANDOMKEY
“user:102”
シンプルだな。だが、「どうやってランダムに選んでいるか」を知っているか?
ここがエンジニアの分かれ道だ。
内部実装の真実:Dictのサンプリングと「隠れコスト」
Redisのキー空間(Key space)は、内部的にはハッシュテーブル(`dict`構造体)で管理されている。`RANDOMKEY`が呼び出されると、Redisは内部で以下のようなアルゴリズムを走らせる。
1. ハッシュテーブルのバケット(スロット)をランダムに一つ選ぶ。
2. そのバケットにチェインされているエントリ(キーと値のペア)の中から、さらにランダムに1つを選ぶ。
3. もし選んだバケットが空(NULL)だった場合どうなるか?
ここが重要だ。Redisはシングルスレッドで動作する。完全なランダム性を担保するために、ハッシュテーブル全体をスキャンして空でないバケットに出会うまでループを回すような愚かなことはしない(そんなことをすれば $O(N)$ の計算量になり、他のリクエストが完全にブロックされる)。
代わりに、Redisの古いバージョンや特定の状態では、空バケットに当たった場合に再試行(リトライ)を行う。さらに悪質なのが、キー空間がまばら(スパース)な状態や、有効期限切れ(TTL)のキーが大量に残っている場合だ。
- 期限切れの罠: 選んだキーにTTLが設定されており、すでに期限切れだった場合、Redisはその場で遅延削除(Lazy Deletion)を行おうとする。これにより、予期せぬブロッキングが発生する。
—
2. なぜ実務で `RANDOMKEY` を使うべきではないのか?
コードレビューで私が「この`RANDOMKEY`の利用を直ちに差し替えろ」と指示する理由は、主に以下の3点に集約される。
① 計算量とスケーラビリティの幻想 ($O(1)$ の罠)
公式ドキュメントでは `RANDOMKEY` の時間計算量は $O(1)$ とされている。理論上は、ハッシュテーブルからランダムなインデックスを引くだけだからだ。
しかし、これは「キー空間が十分に高密度で、メモリ上に綺麗に分布している場合」という前提条件がある。
例えば、大量のキーが削除された直後や、特定のハッシュスロットに偏りが生じている場合、目的のキーにヒットするまでの確率論的な試行回数が増加し、実質的なレイテンシが跳ね上がる。
② クラスタ環境(Redis Cluster)における完全なミスマッチ
これが最大の致命傷だ。
Redis Clusterを使用している場合、キーは16383個のHash Slotに分散配置される。`RANDOMKEY`は、「接続している単一のノード(データベース)」の内部からしかキーを選出しない。
クラスタ全体から真にランダムなキーを取得したい場合、どのノードにリクエストを投げるべきかコントロールできず、データ分散の偏りがそのままランダム性の偏り直結する。分散システムの設計原則において、このような局所的なランダム取得はアンチパターンだ。
③ サンプリングの不確実性
「ちょっとしたサンプリングや、お遊び的な機能だからいいか」という軽い気持ちで使ってはならない。本番環境のデータ量が数千万件規模に達したとき、`RANDOMKEY`の挙動は予測不能なブラックボックスと化す。
—
3. では、どう設計すべきか?(実務で使える堅牢な代替パターン)
「じゃあ、Redisでランダムな要素を取得したいときはどうすればいいんだ?」という声が聞こえてくる。
プロフェッショナルなエンジニアなら、ユースペース側で制御するか、適切なデータ構造を選択すべきだ。
パターン A: `SRANDMEMBER` / `HRANDFIELD` を使う(コレクション単位のランダム)
もし、ランダムに取得したい対象が「特定のセット(Set)」や「ハッシュ(Hash)」の中に閉じ込められているのであれば、`RANDOMKEY`ではなく、専用のコマンドを使うべきだ。
セッションIDのプールからランダムに1つ取得する場合(Set構造)
127.0.0.1:6379> SADD active_sessions “sess:001” “sess:002” “sess:003”
(integer) 3
127.0.0.1:6379> SRANDMEMBER active_sessions
“sess:002”
これらは、特定のキー配下の要素数(Cardinality)に基づいた安全な乱数生成を行うため、グローバルなキー空間を汚染しない。
パターン B: アプリケーション層でのID生成・管理(サロゲートキー方式)
「DB全体からランダムなユーザーを1人選んでキャンペーンを適用したい」という要件があるとしよう。これをRedisの `RANDOMKEY` で解決しようとしてはならない。
【正しい設計】
1. ユーザーを登録する際、Redisの `Sorted Set (ZSET)` に、スコアを連番(またはタイムスタンプ)として登録するか、単にリスト(List / Set)にユーザーIDのリストを保持する。
2. ランダム取得の際は、アプリケーション側で `0` から `総数 – 1` までの乱数を生成し、ZSETの `ZRANGE`(インデックス指定)で取得する。
ZSETを使った安全なインデックスベースのランダム取得
127.0.0.1:6379> ZADD user_pool 0 “user:101” 0 “user:102” 0 “user:103”
(integer) 3
全体の要素数を取得 (O(1))
127.0.0.1:6379> ZCARD user_pool
(integer) 3
0〜2の間でランダムに選んだインデックス(例: 1)の要素を取得 (O(log(N) + M))
127.0.0.1:6379> ZRANGE user_pool 1 1
1) “user:102”
この方法であれば、計算量は $O(\log N)$ と明確であり、インデックスの範囲外を叩くリスクもなく、クラスタ環境でも予測可能なパフォーマンスを維持できる。
—
4. チーフアーキテクトからの総括
Redisは非常に高速で強力なツールだが、それは「それぞれのコマンドが持つ計算量と内部構造の物理的制約」を開発者が正しく理解していることが絶対条件だ。
`RANDOMKEY` は、その手軽さゆえにプロトタイプや小規模なシステムでは便利に見える。しかし、システムがスケールし、データ量が膨れ上がり、ミリ秒単位のレイテンシがビジネスの売上を左右するフェーズに入った途端、確実に牙を向く。
設計レビューでこのコマンドを見かけたら、こう問いかけろ。
「そのランダム性、本当にグローバルなキー空間から取る必要がありますか? データ構造を見直すか、アプリケーション側でコントロールできませんか?」と。
妥協のないアーキテクチャこそが、君のシステムを救う唯一の盾となる。次の設計では、ぜひこの知見を活かしてほしい。
コメント