Redis 过期策略与内存淘汰:缓存穿透/击穿/雪崩一次讲透
作者:鱼宵 | 实战驱动系列 · 第 10 篇
完整课程与可运行源码已开源在 Gitee:gitee.com/j67mk2/redi… (本文对应 lesson-04/)
一、这课解决两个问题
① 数据在 Redis 里放多久就该删(过期策略 + 内存淘汰); ② 用 Redis 当缓存时三个经典事故——穿透、击穿、雪崩——是什么、怎么解。
这课的面试题几乎场场必考,尤其三兄弟对比表,背下来就能过。
二、键过期:EXPIRE / TTL
不是所有数据都该永久留着。比如手机验证码 10 分钟后就没用了,放着占内存。Redis 允许设"存活时间",到点自动删:
| 命令 | 作用 |
|---|---|
EXPIRE key 100 | 设 100 秒后过期 |
TTL key | 还剩几秒(-1=永不过期,-2=已不存在/已过期) |
SET key v EX 100 | set 的同时设过期(常用写法) |
实测(10 秒过期验证码):
SET code:verify 123456 EX 10 -> OK
TTL code:verify -> 10
(过 3 秒后)TTL code:verify -> 7
(过 11 秒后)TTL code:verify -> -2 (已过期被删)
三、过期 key 怎么被删?惰性 + 定期
Redis 总不能挨个盯着"哪个到期了"——那得全表扫描。所以两招组合:
- 惰性删除(懒汉式):到期不立刻删,等你下次访问它时顺手检查"你过期了",删掉返回空。👍 省 CPU;👈 但如果一个过期 key 再没人访问,它一直赖在内存里。
- 定期删除(抽查):每隔一段时间,随机抽一批设了过期的 key 检查删掉,不是全扫,控制成本。
为什么这么设计(讲人话):纯懒汉式,过期 key 占着茅坑不拉屎;纯定时全扫,太费 CPU。两个一结合:抽查兜底 + 访问时补刀,既省 CPU 又不让过期 key 长期占位。
四、内存淘汰:真的写满了怎么办
过期只解决"设了过期时间的 key"。万一没设过期、内存还是写满了呢?用淘汰策略。
先设上限 maxmemory 64mb,写满后由 maxmemory-policy 决定,8 种里重点记 3 个:
| 策略 | 人话 |
|---|---|
noeviction | 不淘汰,写直接报错(最保守) |
allkeys-lru | 从所有 key 里淘汰最近最少用的(缓存最常用) |
volatile-lru | 只在设了过期时间的 key 里按 LRU 淘汰 |
LRU(Least Recently Used):谁最久没被访问踢谁。Redis 用的是近似 LRU(随机抽几个候选踢最久的),不是精确全局链表,但效果够用。LFU(Least Frequently Used):谁被访问次数最少踢谁,适合"热点数据"场景。
亲手看淘汰发生(实测):起一个 64MB 小内存 Redis(--maxmemory 64mb --maxmemory-policy allkeys-lru),塞 400 个约 200KB 的大 key(约 80MB,必然超限):
已写 100 个 key,used_memory≈23.2 MB
已写 200 个 key,used_memory≈45.1 MB
已写 300 个 key,used_memory≈63.9 MB <- 接近上限,开始淘汰
已写 400 个 key,dbsize=286 <- 想写400,只存下286
最早写的 k0 还在吗? exists = false <- 老 key 被 LRU 踢了
最后写的 k399 还在吗? exists = true <- 最新的留下
INFO stats 里 evicted_keys:115 <- 一共淘汰了 115 个
内存满了以后,Redis 把最近最少使用的老 key 踢出去腾地方给新 key——这就是 allkeys-lru 的真实表现。
五、缓存三兄弟:穿透、击穿、雪崩(必考)
正常流程:读 → 先查 Redis → 命中就返回 → 没命中再查 DB 并回填。三大坑都发生在"Redis 没命中"这个环节。
① 缓存穿透:查不到的数据,反复打到 DB
类比:有人专挑空货架问。Redis 和 DB 里都根本不存在这个数据(恶意攻击用不存在的 id 狂刷),每次请求都穿透 Redis 砸到 DB。
解决:
- 布隆过滤器:在 Redis 前放一个,它说"一定不存在"的请求直接挡掉(第 6 篇企业级场景写过手写版);
- 空值缓存:DB 查出来是空也往 Redis 塞空值(短过期 60 秒),下次直接命中空值不打 DB。
② 缓存击穿:某个热点 key,过期瞬间被高并发打穿
类比:一个超热门的 key(爆款商品详情)刚好在某一刻过期了。瞬间成千上万请求同时发现"Redis 没了",齐刷刷打到 DB。 (和穿透的区别:击穿是 key 本来存在、刚过期;穿透是根本不存在。)
解决:
- 互斥锁:只让第一个请求去查 DB + 回填,其他请求等一会儿再读 Redis;
- 逻辑过期:不设物理 TTL,在 value 里存"逻辑过期时间",发现逻辑过期就异步更新,请求先返回旧值。
③ 缓存雪崩:大量 key 同时过期,或 Redis 整个挂了
类比:缓存像一面"挡风墙",雪崩就是墙大面积同时塌了——要么大量 key 同一时刻集体过期,要么 Redis 整体宕机,所有请求瞬间全压到 DB。
解决:
- 随机过期:过期时间加随机值(30 分钟 ± 随机 0~5 分钟),别让大家同时到期;
- 多级缓存:本地缓存 + Redis 两层,Redis 挂了还有本地兜底;
- 高可用/集群:主从 + 哨兵或集群,别单点挂(第 4 篇高可用讲过)。
三兄弟对比表(面试直接背这个)
| 缓存穿透 | 缓存击穿 | 缓存雪崩 | |
|---|---|---|---|
| 是什么 | 查不存在的数据,请求全打到 DB | 某个热点 key 过期瞬间高并发打 DB | 大量 key 同时过期 或 Redis 宕机 |
| 类比 | 专挑空货架问 | 一个热门货架刚好空了,众人哄抢 | 整面墙同时塌了 |
| 解决 | 布隆过滤器 / 空值缓存 | 互斥锁 / 逻辑过期 | 随机过期 / 多级缓存 / 集群高可用 |
一句话区分:穿透=压根没有这个数据;击穿=一个热点刚好过期;雪崩=一大片同时挂。
六、挑战题
- ⭐ 命令行
SET t:a 1 EX 5,每秒TTL t:a一次,亲眼看到从 5 递减到-2。 - ⭐⭐ 把容器的淘汰策略改成
allkeys-lfu,重跑 demo,观察evicted_keys和exists k0/k399有什么不同。 - ⭐⭐⭐ 给"商品详情缓存"设计防穿透/击穿/雪崩方案:哪些数据加布隆过滤器、热点 key 怎么防击穿、批量 key 过期时间怎么加随机值。
七、面试回答模板
面试官:过期 key 怎么删?
惰性删除(访问时才查)+ 定期删除(定时抽样),组合使用平衡 CPU 和内存。
追问:内存满了呢?
按淘汰策略,缓存场景常用 allkeys-lru,从所有 key 里踢最近最少用的;Redis 的 LRU 是近似实现(随机采样),不是精确链表。
追问:穿透/击穿/雪崩?
穿透=查不存在的数据,布隆过滤器/空值缓存解决;击穿=热点 key 过期瞬间,互斥锁/逻辑过期;雪崩=大量 key 同时过期或宕机,随机过期/多级缓存/集群。
关于这个系列
本文是「Java 后端实战精通营」系列第 10 篇,原则:实战驱动、由浅到深、面试向。
👉 Redis 实战精通营(10 课):gitee.com/j67mk2/redi…
- 本文对应源码位置:
lesson-04/(过期 + 64MB 内存 LRU 淘汰完整 demo)
下一篇预告:《Redis 面试冲刺:30 问浓缩成一张网》——整个系列的收官篇,把高频面试题一次汇总,配简历亮点包装话术,学完直接去面试。
跑完有任何报错,把终端输出发评论区,一起排查。