こんにちは、かつコーチです。
「とりあえずRedisを導入したものの、TTL(有効期限)を何秒にすればいいか分からない」
そんな相談を受けたことが何度かあります。
Redisをキャッシュとして使う上で、TTL戦略(キーの有効期限をどう設計するか)は、機能実装と同じくらい重要なテーマです。
この記事では、Redisをキャッシュとして使う基本的な考え方と、TTL設計でつまずきやすいポイントを解説します。
Redisをキャッシュとして使う基本パターン
Cache-Asideパターンとは
Redisをキャッシュとして使う最も基本的な実装パターンがCache-Aside(キャッシュを横に添える、という意味の設計パターン)です。
アプリケーションが、まずRedisにデータがあるか確認し、なければDBから取得してRedisに保存する、という流れになります。
# 1. まずRedisにキャッシュがあるか確認する
127.0.0.1:6379> GET product:1001:detail
(nil)
# 2. キャッシュがなければ、アプリケーション側でDBから取得し、
# その結果をRedisに保存する(TTLも同時に指定する)
127.0.0.1:6379> SET product:1001:detail '{"name":"商品A","price":3000}' EX 600
# 3. 次回以降のアクセスはRedisから直接取得できる
127.0.0.1:6379> GET product:1001:detail
"{\"name\":\"商品A\",\"price\":3000}"
擬似コードにすると、アプリケーション側の処理は次のような流れになります。
cache = redis.GET("product:1001:detail")
if cache が存在する:
return cache
else:
data = mysql.SELECT("SELECT * FROM products WHERE id = 1001")
redis.SET("product:1001:detail", data, EX=600)
return data
この「なければDBから取ってきてキャッシュに詰める」という流れがCache-Asideの基本形です。
一度キャッシュされれば、TTLが切れるまでの間はDBへのアクセスが発生しなくなり、DB負荷を大幅に削減できます。
なぜTTLを必ず設定するのか
キャッシュにTTLを設定しない場合、DBの元データが更新されても、Redis側の古いデータがいつまでも返され続けてしまいます。
これはキャッシュの不整合と呼ばれる問題で、初学者が最も陥りやすいトラブルの1つです。
TTLを設定しておけば、たとえキャッシュの更新処理を忘れても、一定時間後には自動的に古いデータが破棄され、次のアクセスで最新データが取得し直されます。
「更新処理を書き忘れても、最悪でもTTL秒後には正しいデータに戻る」という保険としての役割も、TTLは担っています。
TTLの決め方
データの更新頻度から逆算する
TTLを何秒に設定するかは、キャッシュ対象のデータがどのくらいの頻度で更新されるかから逆算するのが基本です。
| データの種類 | 更新頻度 | TTLの目安 |
|---|---|---|
| 商品の在庫数 | 頻繁(分単位) | 10〜30秒 |
| 商品の基本情報(名前・説明文) | 稀(日〜週単位) | 1〜24時間 |
| ランキング・集計結果 | 定期バッチ更新 | バッチ間隔に合わせる(例:1時間) |
| マスタデータ(都道府県一覧等) | ほぼ変わらない | 数日〜数週間 |
更新頻度が高いデータに長すぎるTTLを設定すると、古いデータがユーザーに見え続けるリスクが高まります。
逆に更新頻度が低いデータに短すぎるTTLを設定すると、キャッシュの効果が薄れ、DBへのアクセスが頻発してしまいます。
一斉失効を避けるためのTTL分散
大量のキーに対して、まったく同じタイミングで同じTTLを設定すると、一斉に期限切れになった瞬間、大量のリクエストが同時にDBへ流れ込むという問題が起きます。
これはキャッシュスタンピード(大量のキャッシュが同時に失効し、DBへのアクセスが殺到する現象)と呼ばれる現象です。
# ❌ Before:全キーに同じTTL(600秒)を一律で設定する
127.0.0.1:6379> SET product:1001:detail "data1" EX 600
127.0.0.1:6379> SET product:1002:detail "data2" EX 600
127.0.0.1:6379> SET product:1003:detail "data3" EX 600
このように同時に大量のキーを同じTTLで作成すると、600秒後にすべてのキーが一斉に失効します。
# ✅ After:基準のTTLに、多少のランダムな幅を持たせる
127.0.0.1:6379> SET product:1001:detail "data1" EX 600
127.0.0.1:6379> SET product:1002:detail "data2" EX 645
127.0.0.1:6379> SET product:1003:detail "data3" EX 587
アプリケーション側で「基準値(600秒)± ランダムな数十秒」のようにTTLをずらすことで、失効タイミングを分散させ、DBへのアクセス集中を緩和できます。
私が実際に負荷テストを行った際、TTLを完全に固定していたバッチ処理で、キャッシュ再生成のタイミングが集中しレスポンスタイムが一時的に跳ね上がる現象を確認したことがあります。
TTLに数十秒のランダム幅を持たせるよう変更したところ、その現象は解消されました。
つまずきやすいポイント
❌ Before:キャッシュ更新処理を忘れて古いデータが残り続ける
# 商品価格をキャッシュ
127.0.0.1:6379> SET product:1001:price "3000" EX 3600
# DB側で価格を2500円に変更したが、Redis側のキャッシュ更新を忘れる
# → 最大1時間、古い価格「3000」がユーザーに表示され続ける
127.0.0.1:6379> GET product:1001:price
"3000"
TTLだけに頼っていると、更新から反映までにタイムラグが生じます。
価格のように「即座に正確でなければ困る」データには、この方式だけでは不十分です。
✅ After:更新時にキャッシュを明示的に削除する(Cache Invalidation)
# DBの価格更新処理と合わせて、対応するキャッシュキーを削除する
127.0.0.1:6379> DEL product:1001:price
(integer) 1
# 次回アクセス時にDBから最新値を取得し、再度キャッシュされる
DB更新のタイミングで対応するキャッシュキーをDELする、この処理をキャッシュインバリデーション(キャッシュを意図的に無効化すること)と呼びます。
TTLによる自動失効と、更新時の明示的な削除を組み合わせることで、「即時性」と「実装のシンプルさ」のバランスを取るのが実務での定石です。
まとめ
この記事のポイント
- Redisキャッシュの基本はCache-Asideパターン:なければDBから取得しRedisに保存する
- TTLはデータの更新頻度から逆算して決め、更新頻度が高いデータほど短く設定する
- 大量のキーに同じTTLを設定すると、一斉失効によるキャッシュスタンピードが起きる
- TTLにランダムな幅を持たせて失効タイミングを分散させるのが実務での対策
- 即時性が求められるデータは、TTLだけでなく更新時の明示的な
DEL(キャッシュインバリデーション)も併用する
次に読むべき記事
- Redisのデータ構造(List・Hash・Set・Sorted Set)の使い分け
- Redisをセッション管理・Pub/Subに使う
- RedisとMemcached、どちらを選ぶべきか比較検証
タグ: Redis, 中級者向け, キャッシュ