3. 缓存:穿透、击穿、雪崩与一致性
❓ 面试官:能详细讲讲什么是缓存穿透、缓存击穿、缓存雪崩吗?它们之间有什么区别,分别怎么解决?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论):
- 穿透:查询了根本不存在的数据(黑客攻击),缓存和数据库都查不到,导致请求全打在 DB 上。
- 击穿:查询了热点数据,但它刚好过期了,瞬间大量并发请求打在 DB 上。
- 雪崩:大量缓存数据在同一时间过期,或者 Redis 直接宕机,导致所有请求全打在 DB 上。
📝 详细原理解析与解决方案:
1. 缓存穿透(Cache Penetration)
- 场景:黑客拿脚本疯狂请求
id=-1或者id=一串乱码的商品详情接口。由于这数据在 Redis 肯定没有,就会去查 MySQL。MySQL 也没有,所以不会写入缓存。下次同样的恶意请求来,依然会穿透缓存直接打挂 MySQL。 - 解决方案:
- 缓存空对象(最常用、最简单):即使 MySQL 查不到数据,我也往 Redis 里存一个
key=-1, value="null",并设置个短点的过期时间(比如 5 分钟)。下次黑客再查-1,Redis 直接返回空串,不再打 MySQL。 - 布隆过滤器(Bloom Filter,适合海量数据):在访问 Redis 之前加一层布隆过滤器,里面存放了所有存在的 ID。如果布隆过滤器说 ID 不存在,直接抛出异常,连 Redis 都懒得查。
- 缓存空对象(最常用、最简单):即使 MySQL 查不到数据,我也往 Redis 里存一个
2. 缓存击穿(Cache Breakdown)
- 场景:双十一零点,某款特价秒杀商品(绝对的热点数据
id=10086)的缓存刚好在那一秒钟过期了。就在这 1 秒内,有 10 万个并发请求查询这件商品。Redis 没查到,这 10 万个请求瞬间全部跑去 MySQL 查详情,MySQL 瞬间崩溃。 - 解决方案:
- 互斥锁(Mutex Lock,常用): 当在 Redis 查不到数据时,不要立刻去查数据库。而是让这 10 万个线程去抢分布式锁(如 Redisson 或 SETNX)。 只有一个线程能抢到锁,它去查 MySQL 并把结果写回 Redis。剩下 99999 个抢不到锁的线程,休眠 50 毫秒后重试(这时候 Redis 里已经有值了,直接返回)。
- 逻辑过期(永不过期): 不给热点数据的 Key 设置真正的过期时间(TTL)。而是在 Value 的 JSON 里加一个字段:
"expireAt": 1690000000。 所有线程查到数据后判断这个时间戳:如果已经过期,依然返回旧数据(脏读),但会在后台启动一个异步线程去 MySQL 查新数据并更新 Redis。
3. 缓存雪崩(Cache Avalanche)
- 场景:你的定时任务在每天晚上 2 点把几十万个商品的缓存全部加载到 Redis,并统一设置了 24 小时过期。结果到了第二天晚上 2 点,这几十万个 Key 瞬间同时失效。如果有大量用户正在访问,就会导致严重的数据库雪崩。
- 解决方案:
- 过期时间加随机值(最核心): 在原有的过期时间上加一个
1~5 分钟的随机数。这样保证几十万个 Key 不会在同一秒失效,而是平滑地分散在 5 分钟内。java// 错误做法 redis.set(key, value, 24 * 60 * 60); // 正确做法 redis.set(key, value, 24 * 60 * 60 + RandomUtil.randomInt(1, 300)); - 服务降级、限流、熔断: 如果是 Redis 节点挂了导致的雪崩,启用 Sentinel 降级或使用 Nacos/Sentinel 配置接口限流,保护 MySQL 不死。
- 过期时间加随机值(最核心): 在原有的过期时间上加一个
❓ 面试官:布隆过滤器到底是怎么工作的?如果它说这个数据存在,那一定存在吗?【中级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 布隆过滤器说存在,不一定存在(有误判率);但它说不存在,那就一定不存在。
📝 详细原理解析:
- 内部结构:
- 布隆过滤器本质上是一个极长的 Bitmap(位数组),里面只有 0 和 1。
- 当你要存入一个值(比如
user_1)时,它会使用多个(比如 3 个)不同的 Hash 函数对user_1进行计算,得到 3 个位置下标,然后把 Bitmap 里这 3 个位置的 0 改成 1。
- 为什么会误判(说存在但其实不存在)?:
- 当查询
user_1时,它用同样的 3 个 Hash 函数算一遍,如果发现这 3 个位置全都是 1,它就会告诉你存在。 - 坑在这里:假如你存了
user_2和user_3,它们经过 Hash 后把某些位置改成了 1。此时你查询一个根本没存过的hacker_999,极其倒霉的是,hacker_999算出来的 3 个位置,恰好被user_2和user_3占据了!这就导致了哈希冲突带来的误判。
- 当查询
- 为什么一定不存在?:
- 只要有任意一个 Hash 函数算出来的位上是
0,说明它绝对没被存入过,百分百肯定不存在。
- 只要有任意一个 Hash 函数算出来的位上是
🌟 面试加分项(实战经验): 面试官可能会问:"如果我把 MySQL 里的某条商品记录删了,我能去布隆过滤器里把它也删了吗?" 绝杀回答: "不能!传统的布隆过滤器只支持插入和查询,绝对不支持删除! 因为多个值可能会 Hash 映射到同一个位上(就是上面说的冲突)。如果你为了删除 user_1,强行把它的 3 个位置都清 0,那原本复用了这几个位置的 user_2 和 user_3 也跟着遭殃了,后续查询会直接判定它们也不存在了。 如果业务强依赖删除操作,必须改用稍微占内存多一点的布谷鸟过滤器(Cuckoo Filter)或者带计数器的 Counting Bloom Filter。"
热点 Key、大 Key、缓存一致性(补强)
热点 Key
爆款 Key 打满单分片/网卡。优化:本地缓存、多副本打散、互斥重建。详见 性能 · HotKey。
大 Key
单 Key 过大导致阻塞与复制变慢。优化:拆分、UNLINK、HSCAN。详见 性能 · BigKey。
缓存一致性
常用 Cache Aside:先更新 DB,再删除缓存。并发下可能短时脏读,普通业务可接受;更强一致可上 Binlog 监听删缓存或短延迟双删。展开见 缓存设计实战。
分区核心优化
| 问题 | 优先手段 |
|---|---|
| 穿透 | 空值缓存 / 布隆 |
| 击穿 | 互斥锁 / 逻辑过期 |
| 雪崩 | 过期随机 + 限流降级 |
| 热点 | 本地缓存 + 打散 |
| 一致 | 删缓存优于写缓存;异步对账 |