生产环境的"三座大山":如何优雅解决 Redis 缓存穿透、击穿与雪崩?
在高并发系统中,Redis 几乎是标配的缓存中间件。但缓存用不好,不仅不能加速,反而会成为系统崩溃的导火索。
今天我们来拆解 Redis 缓存层最令人头疼的”三座大山”:缓存穿透、缓存击穿与缓存雪崩,以及它们对应的实战解决方案。
一、缓存穿透 (Cache Penetration)
场景还原: 黑客或者恶意用户疯狂请求一个在数据库中根本不存在的数据(例如 ID 为 -1 的商品)。每次请求先去 Redis 查,发现没有,然后打到 MySQL。数据库里也没有,无法将空结果回写到 Redis。最终结果:Redis 形同虚设,海量请求直接把 MySQL 打挂。
方案 1:缓存空对象
最简单粗暴的方法——哪怕数据库查出来是空的,也把这个”空值”存进 Redis,设置一个较短的过期时间(比如 5 分钟):
1 | public Item getItem(Long id) { |
优点:实现简单。缺点:浪费 Redis 内存,可能存在短期数据不一致。
方案 2:布隆过滤器(推荐)
这是在请求到达 Redis 之前加一道”预筛网”。布隆过滤器能以极小的内存开销,快速判断一个 Key “绝对不存在”或”可能存在”。如果判定不存在,直接拒绝请求。
1 | // 使用 Guava 的布隆过滤器 |
二、缓存击穿 (Cache Breakdown)
场景还原: 某个超级爆款商品的缓存刚好在秒杀进行到一半时过期了。就在这失效的瞬间,成千上万的并发请求发现缓存没数据,全部涌向 MySQL 去重建缓存,瞬间造成数据库瘫痪。
方案 1:互斥锁 (Mutex Lock)
发现缓存失效后,不要所有线程都去查数据库。让大家先去 Redis 里”抢锁”。抢到锁的去查数据库重建缓存;没抢到的休眠后重试:
1 | public Item getItemWithLock(Long id) { |
方案 2:逻辑过期(永不过期)
物理上不给热点 Key 设过期时间,而是把过期时间写在业务 Value 里。取出数据时判断是否”逻辑过期”。如果过期了,返回旧数据同时异步更新:
1 | public Item getItemLogicalExpire(Long id) { |
三、缓存雪崩 (Cache Avalanche)
场景还原: 大量 Key 被设置了相同的过期时间,导致在同一时刻集体失效。或者 Redis 节点意外宕机。
防御策略:
- 过期时间加随机值:
expire = 3600 + random(0, 300),避免集体过期 - 多级缓存:本地 Caffeine + Redis 双层缓存
- Redis 哨兵/集群:主从 + 哨兵自动故障转移
- 熔断降级:Redis 不可用时,直接返回降级数据或限流
1 | // 过期时间随机化 |
总结
Redis 缓存是典型的”用好了锦上添花,用不好火上浇油”。穿透用布隆过滤器挡,击穿用互斥锁或逻辑过期防,雪崩用随机过期和集群高可用扛。三层防线各司其职,才能在高并发场景下真正做到游刃有余。
