【Redis超実務】秒単位・ミリ秒単位の寿命管理と、本番障害を防ぐ期限設定の鉄則
こんにちは。テクニカルリードの私だ。
今日のコードレビューで、誰かが`EXPIRE`や`TTL`の雑な使い方をしているのを見かけてこのペンを取った。
「とりあえずキーに1時間の有効期限をつけておけば安全」――そんな甘い認識で設計しているなら、今すぐそのコードを止めてほしい。Redisの有効期限管理は、単なる「お掃除機能」ではない。メモリという極限の資源を支配し、大規模トラフィックを捌くための戦略的アーキテクチャそのものなのだ。
今回は、`EXPIRE` / `PEXPIRE` による期限設定、`TTL` / `PTTL` による残り時間の観測、そして `PERSIST` による期限の剥奪まで、実務で絶対に知っておくべき「裏側のメカニズム」と「堅牢な設計パターン」を叩き込む。
—
1. コマンドの正確な挙動と「ミリ秒」の解像度
まずは基本のおさらいだが、プロとして「なぜそれを使い分けるのか」の理由まで言えなければならない。
秒か、ミリ秒か:EXPIRE vs PEXPIRE
- `EXPIRE key seconds`: 指定したキーに秒単位の有効期限を設定する。
- `PEXPIRE key milliseconds`: 指定したキーにミリ秒単位の有効期限を設定する。
実務において、セッション管理やキャッシュの寿命が「秒単位」で足りるなら `EXPIRE` で十分だ。しかし、分散ロックの取得期間、ミリ秒単位のレートリミッター(API制限)、あるいはフラッシュセール時の厳密なタイムアウト制御においては、`PEXPIRE` による高精度な制御が必須となる。
残り時間の観測:TTL と PTTL
- `TTL key`: 残り秒数を返す。
- `PTTL key`: 残りミリ秒数を返す。
ここで、シニアエンジニアとして絶対に覚えておくべき戻り値の仕様がある。
| 戻り値 | 意味 |
| :— | :— |
| `> 0` | 残り有効期限(秒 or ミリ秒) |
| `-1` | キーは存在するが、有効期限が設定されていない(永続キー) |
| `-2` | キーが存在しない(すでに期限切れで削除された、または最初から無い) |
「`-1`と`-2`のハンドリングを間違えて無限ループに陥った」というのは、新人レビューで毎月のように見るバグだ。条件分岐では必ずこのステータスコードを厳密に評価しろ。
期限の剥奪:PERSIST
- `PERSIST key`: 設定されている有効期限を削除し、キーを「永続状態(`-1`)」に戻す。
「一時キャッシュとして作ったが、特定のイベントが発生したためマスターデータとして永続化したい」といった、ステータス遷移を伴う複雑なドメインロジックにおいて極めて有用なコマンドだ。
—
2. Redis内部で何が起きているか?(有効期限の裏側)
表面的なコマンドの使い方を覚えたところで、アーキテクチャの話をしよう。
Redisは、有効期限を迎えたキーをどのように処理しているか知っているか? 「秒数になった瞬間にCPU割込みで消える」などと考えていたら大間違いだ。
Redisは、メモリ上のキーの期限切れを検知するために、主に以下の2つのアプローチを組み合わせている。
1. 受動的削除 (Passive Expiration)
クライアントがそのキーにアクセスしようと `GET` などを実行した瞬間、Redisが「おっと、こいつはすでに寿命を迎えているな」と判断し、その場でメモリから消去して `nil` を返す方式。
2. 能動的削除 (Active Expiration / Eviction)
Redisはバックグラウンドで定期的に(デフォルトでは1秒間に10回=10Hz)、期限付きキーのサンプルをランダムに抽出し、すでに期限切れになっているものをまとめてパージする。
なぜこの仕組みを知る必要があるのか?
もし、「アクセスされず、かつ能動的削除のサンプルにも選ばれない期限切れキー」が大量にあった場合、それらは一時的にメモリを圧迫し続ける。
特に、書き込みが爆発的に多く、かつ一度作ったら二度と参照されないような一時キーを大量に生成する設計(例:ログの断片をすべてキーとして保存するなど)は、Redisのメモリを枯渇させるアンチパターンの最たるものだ。
—
3. 実務で使える堅牢な設計パターン
では、実際のシステム開発において、どのようにこれらを使いこなすべきか。代表的なパターンを授けよう。
パターンA:セッション・スライディング・タイムアウト(活動延長)
ユーザーがWebアプリケーションを操作し続けている間は、セッションの有効期限を自動的に延長させたい。
ユーザーがリクエストを送るたびに、有効期限を「今から30分後」に再設定する
EXPIRE session:user:1001 1800
これだけで、セッションストアとしての寿命管理が実装できる。わざわざ現在の `TTL` を取得して計算し直す必要はない。`EXPIRE` は常に「現在の時刻からの相対秒数」で上書きするからだ。
パターンB:二重登録を防ぐイデムポントなキャッシュ戦略
キャッシュを生成する際、すでに別のプロセスが生成中(あるいは期限切れ間近)であるかを `PTTL` で検知し、Thundering Herd(雷鳴効果:一斉アクセスによるDB崩壊)を防ぐ。
疑似コード(Python / redis-py)
ttl = client.pttl(“cache:hot_item”)
if ttl == -2:
# キーが存在しない、または期限切れ
if client.set(“lock:hot_item”, “1”, px=5000, nx=True):
try:
# DBから重いデータを取得
data = fetch_from_db()
client.set(“cache:hot_item”, data, ex=60)
finally:
client.delete(“lock:hot_item”)
elif ttl > 0 and ttl < 5000:
# 期限切れまであと5秒未満:バックグラウンドで非同期リフレッシュを検討する領域
pass
---
4. パフォーマンスと運用の罠(チーフからの警告)
最後に、本番環境で障害を起こさないために、絶対に守るべき注意点を挙げる。
1. 大量キーの同時期限切れ(Expiration Storm)に気をつけろ
例えば、夜間バッチで「10万件のセッションキーに一律で `EXPIRE 3600`」を設定したとする。ちょうど1時間後、その10万件が一斉に期限切れを迎える。
Redisはシングルスレッドで動作するため、能動的削除のループが膨大な数の期限切れキーのパージにCPUを奪われ、一時的にアプリケーションからのリクエストが完全にフリーズ(レイテンシスパイク)する。
対策: 期限には必ず数秒〜数十秒の「ジッター(ランダムな揺らぎ)」を持たせ、削除タイミングを分散させろ。
2. レプリケーション環境でのTTLの挙動
Master-Replica構成の場合、期限切れの削除はMasterで行われ、その `DEL` コマンドがReplicaに伝播する仕組みになっている(Redis 5.0以降)。そのため、Replica側で勝手に古いキーが消えるタイミングとMasterのタイミングにミリ秒単位のズレが生じることがある。厳密なデータ整合性を求めるロジックをReplica側の読み込みに対して組むな。
—
総括
`EXPIRE` や `TTL` は、ただの「おまけの便利機能」ではない。
Redisのメモリ効率を最大化し、システム全体の耐障害性を担保するための最前線の武器だ。
次に君がコードを書くとき、あるいはコードレビューをするときは、「このキーのライフサイクルはメモリ上でどう消えていくのか?」をミリ秒単位でイメージしてほしい。
プロのエンジニアなら、動くだけのコードではなく、インフラの挙動までハックした美しいコードを書こう。
以上だ。次のレビューを楽しみにしている。
コメント