Redis Streamsの核心:なぜ「ただのログ」として使うとシステムが崩壊するのか
チーフアーキテクトの私だ。コードレビューのたびに言っているはずだ。「RedisのStreamは、単なるDBの代わりのイベントログではない」と。
KafkaやRabbitMQの代替として、あるいは堅牢なメッセージキューとしてRedis Streams(`XADD`, `XDEL`, `XRANGE`, `XREVRANGE`)を導入するプロジェクトが後を絶たない。しかし、メモリ構造の特性を理解せずに「とりあえずエントリを突っ込んで、適当に消す」という実装をしている現場があまりにも多すぎる。本番環境でメモリが爆発し、OOM Killerにプロセスを屠られてから泣きを見るのはごめんだ。
今回は、Redis Streamsの基本操作コマンド群のメカニズムを解剖し、「実務で絶対に事故らない設計パターン」を叩き込む。
—
1. メモリの魔獣:`XADD` と ID自動生成の罠
Streamは、実態として radix tree(基数木)と Consumer Group のメタデータの組み合わせで構築されている。まずは基本のエントリ追加から見ていこう。
基本的なエントリの追加(IDは自動生成: タイムスタンプ-シーケンス)
127.0.0.1:6379> XADD mystream sensor_id “temp-1” value “23.5”
“1708310400000-0”
ここで “ を指定すると、Redisはミリ秒単位のUnixタイムスタンプと、同一ミリ秒内の連番(シーケンス番号)を組み合わせて自動的にIDを生成する。
テクニカルリードからの警告:カスタムIDを使うな
`XADD`の第一引数に明示的なID(例: `1000-0`)を指定することもできるが、特段の理由がない限り、自動生成(“)以外のIDを手動でぶち込むな。
RedisのStream IDは「厳密な単調増加(Monotonically Increasing)」でなければならない。クライアント側の時計のズレやロジックのバグにより、既存のIDより小さい、あるいは同等のIDを挿入しようものなら、容赦なくエラー(`ERR The ID specified in XADD is equal or smaller than the target stream top item`)が返る。タイムスタンプの逆転はシステム全体の致命傷になる。
—
2. 「消せない」現実:`XDEL` の本当の挙動と省メモリ設計
「エントリを処理したから `XDEL` で消す」——これはアンチパターンだ。
エントリの削除
127.0.0.1:6379> XDEL mystream 1708310400000-0
(integer) 1
なぜ `XDEL` だけではメモリが解放されないのか?
`XDEL` は、指定したIDのエントリを論理削除(Tombstone)する。エントリが指し示す実データ(ペイロード)の領域は、ラディックスツリーのノード内に残り続ける。真にメモリが解放されるのは、そのノード内の全てのエントリが削除され、ノード自体がパージされた時だ。
実務でログやイベントを流す場合、`XDEL` を個別に叩く設計は無駄なCPUサイクルとメモリ断片化を招くため即刻廃止せよ。代わりに、`XADD` の時点で上限を決めるか、後述するトリム機構を使うべきだ。
【推奨】MAXLENを使った上限管理付きのXADD
近似的な長さ(~)を指定することで、メモリ効率を保ちながら古いエントリを自動パージする
127.0.0.1:6379> XADD mystream MAXLEN ~ 10000 sensor_id “temp-1” value “24.0”
—
3. タイムトラベルと範囲走査:`XRANGE` と `XREVRANGE`
データの取得には `XRANGE`(古い順)と `XREVRANGE`(新しい順)を使用する。
全範囲から最新の2件を取得したい場合(XREVRANGEの真骨頂)
127.0.0.1:6379> XREVRANGE mystream + – COUNT 2
1) 1) “1708310400001-0”
2) 1) “sensor_id”
2) “temp-2”
3) “value”
4) “24.1”
2) 1) “1708310400000-0”
2) 1) “sensor_id”
2) “temp-1”
3) “value”
4) “23.5”
- `-` は最小ID(`-inf`)、`+` は最大ID(`+inf`)を意味する。
実務におけるパフォーマンスの罠:巨大なレンジ取得
`COUNT` オプションを付けずに `XRANGE mystream – +` のようなクエリを発行するとどうなるか?
Streamに数百万件のエントリが溜まっている状態でこれをやると、Redisのシングルスレッドがブロックされ、他のすべてのクライアントリクエストが数秒間にわたって停止(レイテンシスパイク)する。
【鉄則】
1. `XRANGE` / `XREVRANGE` をバッチ処理で回す際は、必ず `COUNT` を指定し、前回取得した最後のIDを次のリクエストの始点(排他制御)として使う「ページネーション設計」にすること。
2. 無限の過去を遡るクエリは禁忌とする。TTLや `MAXLEN` で保持期間を物理的に制限しておけ。
—
4. チーフアーキテクトが推す:堅牢な設計パターン
最後に、実務の現場で私が承認するRedis Streamsのデータフロー設計を提示する。
[Web/IoT Clients]
│
▼ (XADD + MAXLEN ~ 50000)
┌─────────────────────────────┐
│ Redis Stream │
└──────────────┬──────────────┘
│
├──────────────────────────────┐ (Consumer Groupで水平分散)
▼ ▼
[Worker Node A] [Worker Node B]
アーキテクチャの要件定義:
1. ストリーム長の上限を必ず設ける: `MAXLEN ~` を用いて、メモリの暴走を防ぐガードレールを敷く。
2. 生データではなくIDベースでカーソル管理する: 単発の `XDEL` でゴミ掃除をするな。自動パージと、Consumer Group(`XREADGROUP`)によるオフセット管理を組み合わせよ。
3. エラーハンドリングを見据えたレンジ検索: 万が一のコンシューマ障害時に `XRANGE` で未処理データを再スキャンする場合も、一度に取得するチャンクサイズ(`COUNT`)は1,000件程度に絞れ。
—
結び
Redisは高速だが、それは「正しく使われている時」の条件付きだ。データ構造の裏側にあるメモリ管理の仕組み(ラディックスツリーとパージの挙動)を理解せず、リファレンスをなぞっただけのコードは、高負荷時に必ずシステムを沈没させる。
次のコードレビューでは、`XADD` に `MAXLEN` が入っているか、そして不要な `XDEL` を乱発していないか、私が厳しくチェックさせてもらう。準備を怠るな。
コメント