Skip to content

3. 缓存:穿透、击穿、雪崩与一致性 ​


❓ 面试官:能详细讲讲什么是缓存穿透、缓存击穿、缓存雪崩吗?它们之间有什么区别,分别怎么解决? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论):

  • 穿透:查询了根本不存在的数据(黑客攻击),缓存和数据库都查不到,导致请求全打在 DB 上。
  • 击穿:查询了热点数据,但它刚好过期了,瞬间大量并发请求打在 DB 上。
  • 雪崩:大量缓存数据在同一时间过期,或者 Redis 直接宕机,导致所有请求全打在 DB 上。

📝 详细原理解析与解决方案:

1. 缓存穿透(Cache Penetration) ​

  • 场景:黑客拿脚本疯狂请求 id=-1 或者 id=一串乱码 的商品详情接口。由于这数据在 Redis 肯定没有,就会去查 MySQL。MySQL 也没有,所以不会写入缓存。下次同样的恶意请求来,依然会穿透缓存直接打挂 MySQL。
  • 解决方案:
    1. 缓存空对象(最常用、最简单):即使 MySQL 查不到数据,我也往 Redis 里存一个 key=-1, value="null",并设置个短点的过期时间(比如 5 分钟)。下次黑客再查 -1,Redis 直接返回空串,不再打 MySQL。
    2. 布隆过滤器(Bloom Filter,适合海量数据):在访问 Redis 之前加一层布隆过滤器,里面存放了所有存在的 ID。如果布隆过滤器说 ID 不存在,直接抛出异常,连 Redis 都懒得查。

2. 缓存击穿(Cache Breakdown) ​

  • 场景:双十一零点,某款特价秒杀商品(绝对的热点数据 id=10086)的缓存刚好在那一秒钟过期了。就在这 1 秒内,有 10 万个并发请求查询这件商品。Redis 没查到,这 10 万个请求瞬间全部跑去 MySQL 查详情,MySQL 瞬间崩溃。
  • 解决方案:
    1. 互斥锁(Mutex Lock,常用): 当在 Redis 查不到数据时,不要立刻去查数据库。而是让这 10 万个线程去抢分布式锁(如 Redisson 或 SETNX)。 只有一个线程能抢到锁,它去查 MySQL 并把结果写回 Redis。剩下 99999 个抢不到锁的线程,休眠 50 毫秒后重试(这时候 Redis 里已经有值了,直接返回)。
    2. 逻辑过期(永不过期): 不给热点数据的 Key 设置真正的过期时间(TTL)。而是在 Value 的 JSON 里加一个字段:"expireAt": 1690000000。 所有线程查到数据后判断这个时间戳:如果已经过期,依然返回旧数据(脏读),但会在后台启动一个异步线程去 MySQL 查新数据并更新 Redis。

3. 缓存雪崩(Cache Avalanche) ​

  • 场景:你的定时任务在每天晚上 2 点把几十万个商品的缓存全部加载到 Redis,并统一设置了 24 小时过期。结果到了第二天晚上 2 点,这几十万个 Key 瞬间同时失效。如果有大量用户正在访问,就会导致严重的数据库雪崩。
  • 解决方案:
    1. 过期时间加随机值(最核心): 在原有的过期时间上加一个 1~5 分钟的随机数。这样保证几十万个 Key 不会在同一秒失效,而是平滑地分散在 5 分钟内。
      java
      // 错误做法
      redis.set(key, value, 24 * 60 * 60); 
      // 正确做法
      redis.set(key, value, 24 * 60 * 60 + RandomUtil.randomInt(1, 300));
    2. 服务降级、限流、熔断: 如果是 Redis 节点挂了导致的雪崩,启用 Sentinel 降级或使用 Nacos/Sentinel 配置接口限流,保护 MySQL 不死。

❓ 面试官:布隆过滤器到底是怎么工作的?如果它说这个数据存在,那一定存在吗?【中级】 ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): 布隆过滤器说存在,不一定存在(有误判率);但它说不存在,那就一定不存在。

📝 详细原理解析:

  1. 内部结构:
    • 布隆过滤器本质上是一个极长的 Bitmap(位数组),里面只有 0 和 1。
    • 当你要存入一个值(比如 user_1)时,它会使用多个(比如 3 个)不同的 Hash 函数对 user_1 进行计算,得到 3 个位置下标,然后把 Bitmap 里这 3 个位置的 0 改成 1。
  2. 为什么会误判(说存在但其实不存在)?:
    • 当查询 user_1 时,它用同样的 3 个 Hash 函数算一遍,如果发现这 3 个位置全都是 1,它就会告诉你存在。
    • 坑在这里:假如你存了 user_2 和 user_3,它们经过 Hash 后把某些位置改成了 1。此时你查询一个根本没存过的 hacker_999,极其倒霉的是,hacker_999 算出来的 3 个位置,恰好被 user_2 和 user_3 占据了!这就导致了哈希冲突带来的误判。
  3. 为什么一定不存在?:
    • 只要有任意一个 Hash 函数算出来的位上是 0,说明它绝对没被存入过,百分百肯定不存在。

🌟 面试加分项(实战经验): 面试官可能会问:"如果我把 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 监听删缓存或短延迟双删。展开见 缓存设计实战。

分区核心优化 ​

问题优先手段
穿透空值缓存 / 布隆
击穿互斥锁 / 逻辑过期
雪崩过期随机 + 限流降级
热点本地缓存 + 打散
一致删缓存优于写缓存;异步对账