双11大促前夕,缓存全挂了,我从“被骂”到“救火”的全复盘
兄弟们,你们遇到过这种情况吗?凌晨2点,业务方突然在群里@你:“系统卡死了,页面全白了!”你爬起来一看,数据库连接池被打满,慢查询堆积如山,CPU飙升到95%——原因很简单:Redis缓存挂了。
那天晚上我翻了整整4个小时的源码和监控,才发现我们所谓的“缓存架构”,在穿透、击穿、雪崩面前,简直就是纸糊的。今天我就把这次“翻车”经历完整复盘,从三个问题的本质区别,到布隆过滤器、互斥锁、永不过期、熔断降级的组合拳方案,一次性讲透。
一、事故还原:缓存是怎么一步步被打穿的?
那天晚上8点,业务方开始推一波大促预热活动。流量从平时的5000 QPS瞬间飙到4万 QPS。我们的架构是:Nginx → 应用服务 → Redis → MySQL。正常情况下,Redis命中率在95%以上,MySQL稳稳当当。
但那天,Redis集群里一批热点key刚好在活动开始后陆续过期。更糟的是,有爬虫在疯狂扫描不存在的商品ID——这些ID在Redis和MySQL里都不存在,每次请求都直穿缓存打到数据库。
结果:MySQL连接池在3分钟内被耗尽,大量请求超时,页面白屏,用户无法下单。

监控数据显示:活动开始前MySQL QPS约2000,活动开始后飙到2.8万,直接打满了数据库的最大连接数。
二、缓存穿透:最隐蔽的“杀手”
2.1 什么是缓存穿透?
缓存穿透是指查询的数据在Redis和数据库中都不存在。因为缓存中查不到,每次请求都会穿透缓存直达数据库。
这类请求通常来自:
-
恶意攻击(爬虫用不存在的ID扫库)
-
业务bug(前端传入了非法参数)
-
缓存未预热的新数据
危害:如果攻击者每秒发起1万个不存在ID的请求,相当于数据库要承受1万QPS的无效查询。假设单次查询耗时10ms,1万QPS意味着数据库每秒要处理10000次查询——这还没算正常业务流量。
2.2 解决方案一:布隆过滤器
布隆过滤器是一个概率型数据结构,用极小的内存空间快速判断一个元素“一定不存在”或“可能存在”。
原理:
-
底层是一个二进制位数组,初始全部为0
-
添加元素时,用k个哈希函数计算k个位置,全部设为1
-
查询时,同样计算k个位置,如果有一个为0,则元素一定不存在;如果全部为1,则元素可能存在(有误判可能)

Guava实现示例:
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),
1000000, // 预期元素数量
0.001 // 误判率 0.1%
);
// 缓存所有合法Key
for (String key : allValidKeys) {
bloomFilter.put(key);
}
// 查询前置过滤
public Object getData(String key) {
if (!bloomFilter.mightContain(key)) {
return null; // 一定不存在,直接拦截
}
// 继续查Redis和MySQL
}
布隆过滤器为什么不能删除?
布隆过滤器的每个bit位可能被多个元素共享。如果删除某个元素,将该元素对应的bit位重置为0,可能会误删其他元素的标记,导致后续查询时把本应存在的元素误判为不存在。
那数据变更时怎么办?
-
定期重建布隆过滤器(如每天凌晨从DB全量刷新)
-
或使用支持删除的Counting Bloom Filter(用计数器代替bit位)
2.3 解决方案二:缓存空值
对于查询结果为空的key,也写入一个空值到缓存,并设置较短的过期时间。
Object data = redis.get(key);
if (data == null) {
data = db.query(key);
if (data == null) {
redis.setex(key, 300, "NULL"); // 缓存空值5分钟
return null;
}
redis.set(key, data);
}
缺点:大量不存在的key会占用内存(缓存污染);如果真实数据后来写入,空值过期前会返回错误结果。
建议:布隆过滤器 + 空值缓存组合使用——布隆过滤器拦截大部分非法请求,空值缓存兜底。
三、缓存击穿:热点key失效的“瞬间爆破”
3.1 什么是缓存击穿?
缓存击穿是指一个访问极其频繁的热点key在过期失效的瞬间,大量并发请求同时发现缓存失效,一起涌向数据库。
典型场景:某明星微博突然爆了,缓存了该明星信息的key在高峰期刚好过期,成千上万的请求瞬间打到数据库。
3.2 解决方案一:互斥锁(分布式锁)
当缓存失效时,不立即查数据库,而是先获取分布式锁。只有拿到锁的线程去查数据库并重建缓存,其他线程等待。
3.2 解决方案一:互斥锁(分布式锁)
当缓存失效时,不立即查数据库,而是先获取分布式锁。只有拿到锁的线程去查数据库并重建缓存,其他线程等待。

浙公网安备 33010602011771号