【Redisの深層】全9データ構造の完全解剖:アーキテクトが教える「間違えない設計」の極意
システム設計のレビューをしていると、Redisを単なる「ちょっと速いキーバリューストア」や「セッションの置き場」と勘違いしているコードに出くわすことがある。
「とりあえず何でもString型でシリアライズして放り込む」
「リレーショナルデータベースのノリでHashをテーブルの代わりにする」
――待ってほしい。Redisの真価は、メモリ上に構築された多彩なデータ構造と、それらが単一スレッドのイベントループ上でアトミックに動作するという点にある。適切なデータ構造を選択すれば、RDBでは数秒かかる複雑なクエリやロック処理を、O(1)〜O(log N)かつノンブロッキングで完結させられる。
今回は、Redisがサポートする主要な9つのデータ構造について、内部特性、実務での正しいユースケース、そして「やってはいけないアンチパターン」を、チーフアーキテクトの視点から徹底的に解説する。
—
1. String: 生存戦略の基盤、ただし使い所に注意
RedisのStringは単なる文字列ではない。最大512MBのバイナリセーフなデータであり、数値であれば内部で自動的に整数として扱われ、インクリメント/デクリメントの演算(`INCR`, `DECR`)が可能だ。
実務での活用パターン
- 分散ロック (Redlock / SET NX EX):
アトミックな条件付き書き込みによる排他制御。
- APIのレスポンスキャッシュ:
JSON等にシリアライズしたデータの格納。
アーキテクトの戒め:メモリの断片化とjemalloc
Stringに頻繁な更新や大きなサイズの変更を加えると、メモリ管理アロケータ(jemalloc)レベルで断片化(Fragmentation)を引き起こす。特に揮発性の高い巨大なJSONをStringで保持し続ける設計は、メモリ効率の観点から最悪だ。構造化データなら次項のHashを検討すべきである。
—
2. Hash: 「フィールドの泥沼」を回避するオブジェクト表現
Hashは、フィールドと値のペアのコレクション(いわゆる連想配列)だ。
実務での活用パターン
- ユーザープロファイルの保持:
`user:1001` というキーに対して、`name`, `email`, `age` をフィールドとして持たせる。
設計の急所:巨大なHashの罠
Hashは要素数が少ないうちはメモリ効率の極めて良いエンコーディング(`ziplist` / `listpack`)で保持されるが、`hash-max-ziplist-entries`などの閾値を超えると、ハッシュテーブル(`dict`)へ昇格し、メモリフットプリントが急増する。
数百万のフィールドを持つ単一のHashを作るような設計は、Redisのシングルスレッドをブロックする原因(`HGETALL`のO(N)によるレイテンシスパイク)になるため、絶対に避けること。
—
3. List: 双方向リンクリストの本質と「キュー」としての限界
RedisのListは双方向リンクリスト(Linked List)ベースである。先頭(LPUSH/LPOP)および末尾(RPUSH/RPOP)の挿入・削除がO(1)であるため、メッセージキューのプリミティブとして多用される。
実務での活用パターン
- 非同期ジョブのタスクキュー:
Workerへのタスク配布。
- タイムラインの直近N件保持:
`LPUSH` で追加し、`LTRIM` で常に最新100件に切り詰める。
アーキテクトの戒め:中間の操作は「罪」
リンクリストの性質上、インデックスを指定した操作(`LINDEX` や `LSET`)や、特定の値の検索(`LREM` の走査など)は O(N) の計算量を持つ。Listを「配列」としてランダムアクセスに使おうとした瞬間、パフォーマンスは崩壊する。順序付きのキューやストリーム用途に限定すべきだ。
—
4. Set: 重複排除と集合演算の高速化
Setは、順序を持たないユニークな文字列のコレクションである。
実務での活用パターン
- 「いいね!」や「既読」の管理:
`sadd post:42:likes user_123`
- レコメンドエンジンのタグ付け / 共通友人の算出:
`SINTER`(積集合)や `SUNION`(和集合)を用いた高速なマッチング。
パフォーマンス上の注意
集合演算(`SINTER`, `SUNDIFF` 等)は非常に強力だが、対象となるSetの要素数が数百万規模になると、CPUを激しく消費し、Redisのメインスレッドをブロックする。バックグラウンドで処理したい場合は `SINTERSTORE` を使い、結果を別のキーに退避させる非同期設計を取り入れること。
—
5. Sorted Set (ZSet): スコアによる順序制御の最高峰
Redisの真髄とも言えるのがこのSorted Set(ZSet)だ。すべての要素に浮動小数点数の「スコア」が紐づき、スコア順(同点の場合は辞書順)に常にソートされている。内部では「ハッシュテーブル」と「スキップリスト(Skip List)」のハイブリッドで実装されており、検索も範囲取得も O(log N) を保証する。
スコア(ミリ秒単位のタイムスタンプ)と共にイベントを投入
ZADD timeline:feed 1718438400000 “event:A”
ZADD timeline:feed 1718438405000 “event:B”
指定範囲(ページング)の取得は O(log N + M) で爆速
ZRANGEBYSCORE timeline:feed 1718438400000 1718438500000 LIMIT 0 10
実務での活用パターン
- リアルタイム・リーダーボード(ランキング):
ゲームのスコアや、アクティビティのランキング。
- 遅延キュー(Delayed Queue):
スコアに「実行予定時刻のタイムスタンプ」を設定し、ワーカーが定期的に `ZRANGEBYSCORE` で取得して実行する。
—
6. Stream: イベント駆動アーキテクチャのためのKafka的データ構造
Redis 5.0で導入されたStreamは、追記型(Append-only)のログ構造を持ち、Apache Kafkaのコンセプトに強くインスパイアされている。コンシューマーグループ、メッセージのAck(確認応答)、ペンディング状態の管理など、本格的なメッセージング基盤の要件を満たす。
実務での活用パターン
- マイクロサービス間のイベントバス:
イベント駆動型アーキテクチャにおける信頼性の高いメッセージング。
- ログ収集パイプライン:
エッジからのログストリームの一時バッファリング。
ListやPub/Sub(Pub/Subはメッセージの永続化がない)の限界を感じたなら、迷わずStreamを採用すべきだ。ただし、メモリ管理の観点から `XADD` には `MAXLEN` オプションを付与し、ストリームの肥大化を必ず防ぐ運用設計にすること。
—
7. Bitmaps: ビット演算による省メモリ・カウンター
String型をベースに、ビット単位での操作(`SETBIT`, `GETBIT`, `BITCOUNT`)を可能にする。
実務での活用パターン
- ユーザーの当日アクティブ状態(DAU)のトラッキング:
ユーザーIDをオフセットとし、ログインしていれば `1`、していなければ `0` とする。数千万ユーザー規模であっても、わずか数MBのメモリで高速に日別アクティブ判定が可能になる。
—
8. HyperLogLog: 誤差を許容した爆速のユニーク要素数カウント
「数千万人のユニークユーザー数(UU)を、わずか12KBの固定メモリで数えたい」――それを実現するのがHyperLogLogだ。確率的アルゴリズムを用い、標準誤差約0.81%という高精度を維持しながら、要素の重複排除カウント(`PFADD`, `PFCOUNT`)を行う。
実務での活用パターン
- リアルタイムのユニークアクセス数計測:
正確な一意性を求めないダッシュボードのPV/UU表示。
—
9. Geospatial: 2次元空間データのインメモリ索引
内部でGeohash(緯度経度を52ビットの整数にエンコード)アルゴリズムを使用し、Sorted Setをベースに空間インデックスを提供する。
実務での活用パターン
- 「近くの店舗・配達員」の検索:
`GEOSEARCH` コマンドを使い、指定座標から半径〇km以内のエンティティをO(log N + M)で抽出。
—
アーキテクトからの提言:データ構造選択のチェックリスト
システム設計レビューの際、私はチームメンバーに必ず次の問いを投げかける。
1. 「そのデータ構造の操作計算量は、O(1) または O(log N) に収まっているか?」
(O(N) のコマンドをプロダクションのホットパスで使っていないか?)
2. 「メモリの肥大化(断片化や巨大なコレクション)に対する上限ガード(TTLやMAXLEN)はあるか?」
3. 「RDB的な正規化を持ち込んでいないか?」
(Redisは非正規化を恐れるな。必要な形にデータを加工して適切な型に格納せよ)
Redisのデータ構造は、開発者の武器である。ツールの特性を正確に理解し、適材適所でアーキテクチャに組み込むこと。それこそが、秒間数十万リクエストを捌く堅牢なシステムを作り上げる唯一の道である。
コメント