大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
TTL 与过期淘汰:数据的保质期
这一课你将能做到
- 熟练使用
EXPIRE/PEXPIRE/PERSIST,并准确解释TTL返回 -1 和 -2 的区别 - 说清 Redis 删除过期 key 的两条路径(惰性删除 + 定期删除),以及内存满时 8 种淘汰策略怎么选
- 用「TTL 抖动 + 空值缓存 + 互斥重建」三个手段讲清并预防缓存穿透、击穿、雪崩
为什么要学
给缓存设过期时间,看似一行 EXPIRE 的事,实际上藏着三个经典生产事故:
- 雪崩:500 个缓存 key 设了同一时刻过期,那一秒所有请求打到 MySQL,数据库直接被压垮
- 击穿:某个热点 key 过期瞬间,上千并发同时 miss,全部涌向数据库重建同一份缓存
- 穿透:有人恶意请求根本不存在的 id,缓存里永远没有,每次都打到数据库
这三个问题的解药全部和 TTL 设计有关。本站第 12 课的实战会逐条落地这些策略,这一课先把原理和命令打牢。另外本站限流器「首次 INCR 时才 EXPIRE」的设计,也只有理解 TTL 机制才能看懂。
先建立图像
每个 key 可以挂一个「电子保质期标签」:
SET coderoadmap:tracks:list '...' EX 300
│
└─ 300 秒后标签归零
TTL 返回值的三种状态:
≥ 0 → 还剩多少秒
-1 → key 存在,但永不过期(缓存场景的危险信号)
-2 → key 不存在
「过期了」不等于「立刻从内存消失」。Redis 用两条路径清理过期 key:
惰性删除:下次有人摸这个 key 时顺手删(长期没人摸 → 一直占内存)
定期删除:后台周期性随机抽检一批 key,删掉已过期的(抽样,扫不全)
两条路径都不保证及时 → 内存仍可能一直涨 → 需要淘汰策略兜底
人话:TTL 像「便签上的保质期」。保质期全印成同一天,仓库(MySQL)会在那天被挤爆。
跟着做
步骤 1:TTL 三态实验
docker exec -it redis-dev redis-cli
SET demo:a "1" EX 30
TTL demo:a # (integer) ≥0,倒计时
SET demo:b "2"
TTL demo:b # (integer) -1 —— 存在但永不过期
TTL demo:nope # (integer) -2 —— 不存在
PERSIST demo:a # 摘掉保质期
TTL demo:a # (integer) -1
EXPIRE demo:a 10 # 再挂 10 秒
步骤 2:观察「过期 ≠ 瞬间删」
SET demo:c "3" EX 2
# 等 3 秒
EXISTS demo:c # (integer) 0 —— 业务视角已不存在
MEMORY USAGE demo:c # (nil) —— 空间已被回收或将被回收
Redis 不维护全局倒计时表,过期 key 在被访问或被抽检前可能仍占内存——这就是需要内存淘汰策略兜底的原因。
步骤 3:看淘汰策略配置
CONFIG GET maxmemory-policy
CONFIG GET maxmemory
默认 noeviction(内存满后所有写命令报错)。8 种策略一览:
noeviction 内存满,写命令报错(默认)
allkeys-lru 全库范围 LRU 淘汰 ← 纯缓存实例首选
volatile-lru 只在设了 TTL 的 key 里 LRU
allkeys-lfu 全库 LFU(按访问频率)
volatile-lfu 带 TTL 的 key 里 LFU
allkeys-random 全库随机淘汰
volatile-random 带 TTL 的 key 里随机
volatile-ttl 优先淘汰剩余 TTL 最短的
纯缓存实例的标准搭配:CONFIG SET maxmemory 512mb + CONFIG SET maxmemory-policy allkeys-lru。
步骤 4:TTL 抖动防雪崩(Java 片段)
错误示范 vs 正确示范:
// 错:所有实例同一时刻写入同一 TTL → 到点集体失效
stringRedisTemplate.opsForValue().set(key, json, Duration.ofMinutes(5));
// 对:基础 TTL + 随机抖动
long ttl = 300 + ThreadLocalRandom.current().nextLong(60);
stringRedisTemplate.opsForValue().set(key, json, Duration.ofSeconds(ttl));
第 12 课 TrackCacheService 的 listTtl() 就按这个写。
步骤 5:穿透与击穿的防御演练
模拟穿透——请求一个不存在的课程 slug:
# 缓存空值:把「查过、不存在」也缓存下来,配短 TTL
SET coderoadmap:track:no-such-slug "" EX 60
应用层约定:读到空字符串表示「已知不存在」,与「缓存未命中」(nil)区分开。
模拟击穿的互斥重建——多并发 miss 时,只有一个线程能抢到锁去查库:
SET mutex:track:no-such-slug "1" NX EX 10 # 抢到的线程回源重建
# 其余线程:等一小会儿重试读缓存,或直接返回默认值
讲清楚要点
- EXPIRE 是挂在 key 上的独立属性,重写 value 会清掉它:再次
SET key value(不带 EX)会把 TTL 重置为永不过期——「更新值把过期时间弄丢了」是最常见的缓存永生事故。所以 Java 里永远用带 Duration 的 set 重载。 - 惰性 + 定期删除保证「过期 key 终将被清」但不保证准时;业务上别依赖「过期那一纳秒 key 就没了」,击穿防护要自己做。
- 淘汰策略是内存上限的兜底,不是过期机制的一部分:缓存实例配 allkeys-lru;会话 + 缓存混布实例要谨慎——会话 key 被 LRU 挤掉等于用户被踢下线,要么容量给足加监控,要么分实例部署。
- 雪崩解法 = TTL 抖动(错开过期时刻)+ 多级缓存(Caffeine 顶在 Redis 前面,本站已有)。
- 击穿解法 = 互斥重建(NX 锁让一个线程回源)或逻辑过期(值里带过期字段,后台异步刷新)。
- 穿透解法 = 缓存空值短 TTL + 入参校验,进阶再上布隆过滤器。
你可能会懵的地方
-
过期删除和淘汰策略有什么区别? 过期删除处理「到了保质期的 key」(惰性 + 定期两条路径);淘汰策略处理「内存超上限时留谁删谁」。两条独立机制,TTL 管新鲜度,淘汰管容量。
-
为什么 Redis 不做到期即删? 精确到期即删需要维护海量定时器,代价太大。惰性 + 定期是 CPU 和内存之间的工程折中。
-
本站会话 key 会被 allkeys-lru 挤掉吗? 会——如果 maxmemory 太小、缓存 key 把内存占满。所以会话敏感的实例要么缓存/会话分开部署,要么容量规划 + 监控(第 11 课讲 INFO)。这是「数据分级」思想的落地。
-
TTL 抖动区间怎么定? 经验值:基础时长的 10%
20%(5 分钟 TTL 抖 3060 秒)。目的只是让过期时刻错开,不需要精确计算。 -
缓存空值会不会把垃圾存满 Redis? 用短 TTL(30~120 秒)控制生命周期,攻击停止后自动清空;同时配入参校验拦截明显非法的 id。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| SET 更新缓存值不带 EX | TTL 被清,key 永生 | 每次写入都带 Duration |
| 全部 key 同一 TTL | 雪崩 | 300 + random(60) 抖动 |
| miss 后所有线程都回源 | 击穿打爆 MySQL | NX 互斥重建 / 逻辑过期 |
| 不存在的数据不缓存 | 穿透 | 空值短 TTL + 入参校验 |
| 生产不设 maxmemory 用默认 noeviction | 内存满写入全挂 | maxmemory + allkeys-lru |
| 缓存空值设成永不过期 | 新数据永远「不存在」 | 空值必须短 TTL |
卡点急救
- TTL 显示 -1:SET 时漏了 EX,或更新时覆盖了 TTL。补
EXPIRE,并检查代码是否统一走带 Duration 的写入。 - 写 Redis 报
OOM command not allowed:maxmemory 打满且策略是 noeviction。先CONFIG GET maxmemory*确认,再清理或改策略。 - CONFIG SET 改了重启就丢:CONFIG SET 只运行时生效,要持久化用
CONFIG REWRITE(写回配置文件);容器部署更推荐把参数写进启动命令。 - key 明明到期了还在:惰性删除还没触发,占内存但不影响正确性(EXISTS 返回 0)。
- MySQL 监控出现周期性尖峰:对照 Redis 里大批 key 的写入时刻 + TTL,典型的雪崩节律;上抖动。
面试常问
-
缓存穿透、击穿、雪崩分别是什么,怎么解决? 穿透 = 查不存在的数据(空值缓存短 TTL、入参校验、布隆过滤器);击穿 = 热点 key 过期瞬间并发回源(互斥重建、逻辑过期);雪崩 = 大批 key 同时过期或 Redis 宕机(TTL 抖动、多级缓存、高可用)。三个词对应三种「请求绕过缓存打到 DB」的模式。
-
Redis 过期 key 是怎么删的? 惰性删除(访问时检查)+ 定期删除(后台随机抽检),两者结合在 CPU 和内存之间折中。所以「刚过期」的 key 可能还占内存。
-
内存淘汰策略有哪些?你们用哪种? 8 种,默认 noeviction。纯缓存实例用 allkeys-lru;注意会话和缓存混布时 LRU 可能挤掉会话(用户被踢),要容量规划或分实例——本站会话实例靠容量 + 监控保障。
自测三问
- TTL 返回 -1 和 -2 分别什么意思?PERSIST 之后 TTL 变成什么?
- 为什么「再次 SET 不带 EX」会弄丢过期时间?工程上怎么从代码层面杜绝?
- 穿透、击穿、雪崩各自的一句话解法是什么?分别对应哪类「打爆 DB」的模式?
小结一句话
TTL 是缓存的生命线:写入必带过期、TTL 加抖动防雪崩、空值短缓存防穿透、互斥重建防击穿,内存上限靠淘汰策略兜底。