最近在浏览 Java 后端秋招提前批的面经时,发现 Redis 被问得非常频繁。不再是那种「Redis 有哪些数据结构」的泛泛提问,而是直接切入项目场景:你的简历里写了缓存优化,那缓存穿透、击穿、雪崩分别该如何处理?为什么采用这种方案?线上环境真的实施过吗?

这道题看似是八股文,其实很容易被追问到深处。很多人一上来就背诵「穿透用布隆过滤器,击穿用互斥锁,雪崩加随机过期时间」,面试官再追问两句就答不上来了。
更推荐按照「一次请求的完整链路」来组织回答。
先把三件事界定清楚
缓存穿透,指的是请求的数据在数据库中根本不存在。比如有人持续查询 id 为负数的商品,缓存里没有,数据库里也没有,每次请求都直接打到数据库,这才是穿透。
缓存击穿,说的是一个热点 key 突然过期失效。例如首页推荐配置、爆款商品库存、秒杀活动信息,这些数据平时都由缓存扛住,一旦某个 key 因过期消失,大量请求瞬间涌入数据库。
缓存雪崩,则是一批 key 在同一时间过期,或者 Redis 集群本身短时间不可用。它不是一个热点 key 的问题,而是缓存层大面积失效。
面试时先讲清这三个概念的区别,后续才有讨论空间。否则把击穿也说成穿透,面试官基本会默认你只是背过题。
穿透:不只局限于布隆过滤器
穿透的第一层防护其实是参数校验。
如果业务上要求商品 id 必须是正整数,那么负数、超长字符串、明显不合法的枚举值,应该在入口处就拦截掉。不要让这类请求进入缓存层,更不要让它们打到数据库。
第二层才是缓存空值。当数据库查询不到结果时,可以将空结果也写入缓存,过期时间设置短一些,比如几十秒到几分钟。这样同一个不存在的 key 就不会反复冲击数据库。
布隆过滤器适用于更重的场景:数据集合相对稳定、非法 key 数量庞大、数据库压力已经明显上升。它的特点是可能误判「存在」,但不会把真实存在的数据判为不存在。因此用它做前置过滤是有效的,但不能把它当作最终真相。
一个比较稳妥的回答可以这样表述:
先做入参校验,挡住明显不合法的请求;
查库为空时缓存空值,避免同一个不存在的 key 重复击穿数据库;
如果非法 key 规模很大,再把合法 id 集合放入布隆过滤器做前置判断。
面试官如果继续追问「缓存空值会不会污染 Redis」,你可以说空值 TTL 要短、value 要小,并且只对高频不存在的 key 进行缓存,而不是所有未命中都无脑写入。
击穿:互斥锁不是只写一个 SETNX
热点 key 过期时,最怕的是大量线程同时查询数据库、同时回写缓存。
常见的做法是加互斥锁:只有抢到锁的线程去查数据库并重建缓存,其他线程稍等后重试,或者直接返回旧值。
这里有一个坑,不要说「SETNX 后再 EXPIRE」。这两个命令拆开不是原子操作,中间进程崩溃就可能留下死锁。Redis 更通用的做法是用一条 SET 命令同时带上 NX 和 EX 参数,把「只在不存在时设置」和「过期时间」放在同一个原子命令里。解锁时还需要校验 value,通常使用 Lua 脚本确保只删除自己的锁。
如果业务允许短暂返回旧数据,逻辑过期方案会更优:缓存里不仅存放数据,还额外存放一个逻辑过期时间。请求到达时发现逻辑过期,不是立刻让所有线程查库,而是一个线程异步刷新,其他请求先拿旧数据顶住。
这在首页配置、榜单、推荐位上非常常见。用户看到几十秒前的数据影响不大,但数据库被打爆就是真实事故。
我会这样回答:
对强一致性要求高的热点 key,使用互斥锁重建缓存;
对读多写少、允许短暂旧值的热点 key,采用逻辑过期加异步刷新;
锁必须设置过期时间,value 要保证唯一,删除锁时需校验 owner。
这几句话比单独背「互斥锁」更有分量。
雪崩:随机 TTL 只是最低配置
雪崩的典型原因有两个。
一是大量 key 同时过期。例如批量导入缓存时全部设为 30 分钟,半小时后一起失效。这种情况用随机 TTL 就能缓解:在基础过期时间上增加一段随机抖动,让 key 分散失效。
二是缓存服务本身不可用。这就不是随机 TTL 能解决的了。需要看系统是否具备本地缓存、限流、降级、熔断等机制,以及核心数据能否提前预热。
面试时可以按层次来阐述:
过期时间层:TTL 加上随机值,避免同批 key 同时失效。
热点数据层:在上线或活动开始前进行预热,不等待用户请求触发重建。
系统保护层:本地缓存兜底、接口限流、数据库保护、非核心功能降级。
如果你项目里只是普通后台系统,没有高并发活动,也不要硬编秒杀场景。可以说「我没做过秒杀,但在后台字典、配置缓存里会做随机 TTL,避免定时任务批量刷新导致同时过期」。这样的回答反而更真实。
面试官真正想听的,不是名词
这道题最后通常会落到项目实践上。
如果你的简历写了 Redis,就提前想清楚这几个问题:
哪些数据用到了缓存?为什么这些数据适合缓存?
key 如何设计?有没有前缀、版本号、业务 id?
TTL 设置多久?是固定值还是带随机?
数据更新时,是删除缓存、更新缓存,还是延迟双删?
如果缓存未命中,数据库能否扛住?有没有限流措施?
这比背十页 Redis 面试题更重要。
