Article
🧠 RedisのTTLとEvictionを混同しない:Spring Bootのキャッシュ設計でハマるポイント
Redisをキャッシュとして使い始めると、よく出てくるのが次の2つです。
- TTL(Time To Live)
- Eviction(メモリ不足時のキー追い出し)
どちらも「Redisからキーが消える」ため混同しやすいのですが、発動する条件はまったく違います。
この記事では、Spring Boot + Redisでキャッシュを実装するときに知っておきたいTTLとEvictionの違いを、実装例と運用上の注意点まで含めて整理します。
先に結論
TTLは「このキーを何秒後に期限切れにするか」というキー単位の有効期限です。
Evictionは、Redisがmaxmemoryで設定されたメモリ上限に達したとき、どのキーを追い出して空きを作るかという仕組みです。
つまり、
TTL
キー自身の有効期限が来た
↓
キーが消える
Eviction
Redisのメモリが上限に近づいた
↓
ポリシーに従ってキーを追い出す
という違いがあります。
TTLとは
TTLは Time To Live の略で、「あと何秒そのキーを生存させるか」を表します。
Redisでは例えば次のように設定できます。
SET user:123 "Alice"
EXPIRE user:123 300
EXPIRE user:123 300 は、user:123を300秒後に期限切れにするコマンドです。
残り時間はTTLで確認できます。
TTL user:123
Redis公式ドキュメントでは、TTLが経過するとキーは自動的に削除されます。またSETのオプションを使えば、値の保存と有効期限設定をまとめて行えます。
SET user:123 "Alice" EX 300
Spring BootでTTLを設定する
Spring Data RedisのRedisCacheManagerを使う場合は、entryTtlでキャッシュエントリのTTLを設定できます。
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
RedisCacheManager cacheManager(
RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
これでキャッシュに保存されたエントリには10分のTTLが設定されます。
例えばサービス側ではSpring Cacheを使って、
@Cacheable(cacheNames = "users", key = "#userId")
public User findUser(String userId) {
return userRepository.findById(userId)
.orElseThrow();
}
のように書けます。
初回アクセスではDBから取得し、以降はRedisキャッシュを利用します。TTLが切れた後のアクセスでは再びDBから取得されます。
「アクセスされたらTTLが延びる」とは限らない
ここはハマりやすいポイントです。
通常のTTLでは、キャッシュをGETしただけでは有効期限はリセットされません。
例えばTTLが5分なら、途中で何回読まれても、書き込みから5分後には期限切れになります。
一方、TTI(Time To Idle) は「最後にアクセスしてから何分」という考え方です。
Spring Data RedisのCache実装にはTTI相当の動作を有効化する仕組みがあります。
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.enableTimeToIdle();
Spring Data Redisの公式ドキュメントによると、この機能では読み取り時にRedisのGETEXを利用してTTLを更新します。
注意点として、GETEXはRedis 6.2以降で利用できます。対応していないRedisに対してTTIを有効化するとコマンド実行エラーになります。
Evictionとは
TTLとは別に、RedisにはEvictionがあります。
Redisではmaxmemoryを設定すると、利用メモリに上限を設けられます。
maxmemory 2gb
上限に達したときの挙動を決めるのがmaxmemory-policyです。
代表的なものは次の通りです。
| ポリシー | 動作 |
|---|---|
noeviction |
キーを追い出さず、新しいデータを書き込むコマンドでエラーを返す |
allkeys-lru |
全キーから、最近使われていないキーを優先して追い出す |
allkeys-lfu |
全キーから、利用頻度が低いキーを優先して追い出す |
allkeys-random |
全キーからランダムに追い出す |
volatile-lru |
TTLが設定されたキーから、最近使われていないものを追い出す |
volatile-lfu |
TTLが設定されたキーから、利用頻度が低いものを追い出す |
volatile-ttl |
TTLが設定されたキーから、残りTTLが短いものを優先して追い出す |
LRUは Least Recently Used、つまり「最近使われていないもの」。
LFUは Least Frequently Used、つまり「利用頻度が低いもの」です。
TTLが1時間でも5分後に消えることはある
ここがTTLとEvictionを分けて理解する理由です。
例えば、
TTL = 1時間
maxmemory = 2GB
maxmemory-policy = allkeys-lru
という設定だったとします。
キーのTTLがあと55分残っていても、Redisがメモリ上限に達してLRUの追い出し対象になれば、そのキーは消える可能性があります。
そのため、
TTLを1時間にしたから、必ず1時間キャッシュされる
とは考えない方が安全です。
TTLは「期限切れになる時刻」を決める仕組みであり、Evictionによる削除を防ぐ保証ではありません。
キャッシュ用途ならどう設計するか
Redis公式ドキュメントでは、すべてのキーをキャッシュとして扱うケースではallkeys-lruなどを検討できます。
例えば、
maxmemory 2gb
maxmemory-policy allkeys-lru
という構成です。
ただし、Redisにキャッシュだけでなく重要なデータも混在させている場合は注意が必要です。
例えば、
session:xxx
cache:user:xxx
cache:product:xxx
important:job-state:xxx
が同じRedisにあり、allkeys-lruを使えば、important:job-state:xxxも追い出し候補になり得ます。
「Redisだから消えてもよい」ではなく、キーごとの役割を明確にする必要があります。
キャッシュミス前提で実装する
Redisキャッシュを使うコードでは、キャッシュが存在しないケースを正常系として扱うことが重要です。
基本的なCache-Asideパターンは、
1. Redisを見る
2. あれば返す
3. なければDBを見る
4. Redisへ保存する
5. 結果を返す
です。
Springの@Cacheableを使うと、このパターンをかなり簡潔に実装できます。
RedisのキーはTTLやEvictionで消える可能性があるため、キャッシュ消失をアプリケーション障害として扱わない設計にします。
TTLを短くしすぎるとDB負荷が増える
TTLを短くするとデータの鮮度は上げやすくなりますが、その分キャッシュミスが増えます。
例えばアクセスの多いデータでTTLを10秒にすると、期限切れのたびにDBへ問い合わせが発生します。
逆にTTLを長くするとDB負荷は減りますが、更新後も古いキャッシュを返す時間が長くなります。
そのため、TTLは単純に短いほど良いわけではありません。
更新頻度
データ鮮度の要求
アクセス頻度
DBへの負荷
キャッシュ削除の仕組み
を合わせて決めます。
更新時にキャッシュを消す方法もある
例えばユーザー情報を更新したタイミングで、対応するキャッシュを削除できます。
@CacheEvict(cacheNames = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
この場合、次回のfindUserでDBから最新値を読み込み、再びRedisへキャッシュします。
TTLだけに頼るより、更新処理とキャッシュ削除を組み合わせた方が古いデータを返す時間を短くできます。
運用ではevicted_keysを見る
RedisのEvictionが頻発しているか確認したい場合はINFOが役立ちます。
INFO stats
特に確認したいのが、
evicted_keys
expired_keys
です。
expired_keysは期限切れになったキー、evicted_keysはメモリ制約などによって追い出されたキーを把握するための指標です。
evicted_keysが想定以上に増えている場合、
- Redisのメモリ容量が不足していないか
- Eviction Policyが用途に合っているか
- キャッシュするデータ量が多すぎないか
を確認します。
まとめ
Redisでキーが消える理由を調査するときは、まずTTLとEvictionを分けて考えると整理しやすくなります。
TTL
→ キーに設定した有効期限で消える
Eviction
→ メモリ上限に達したとき、ポリシーに従って消える
Spring BootでRedisをキャッシュとして使うなら、
entryTtlでキャッシュの寿命を決める- 必要なら更新時に
@CacheEvictする - キャッシュミスを正常系として扱う
- Redis側の
maxmemory-policyも確認する expired_keysとevicted_keysを監視する
という流れで考えると、キャッシュ周りのトラブルを切り分けやすくなります。
参考資料
- Redis公式: Key expiration
- Redis公式: EXPIRE
- Redis公式: Key eviction
- Redis公式: Redis configuration
- Spring Data Redis 4.1.1 Reference
- Spring Data Redis: Redis Cache