现象或背景:一次因缓存失效导致的数据库雪崩
我们线上有个高并发的订单查询接口,日均处理120万次请求。某次促销活动期间,商品ID为10001的缓存突然失效,导致10秒内有3500次请求直接打到MySQL。数据库连接池瞬间被耗尽,QPS从平常的2000次/秒飙升到20万次/秒,服务响应时间从100ms暴涨到1500ms,甚至出现连接超时。
具体数据:
- 缓存类型:Redis Cluster 6.2
- 缓存过期策略:统一设置为24小时
- 数据库:MySQL 8.0,连接池配置为20
- 事故时间:2024-05-15 14:30
- 影响范围:订单查询接口、库存扣减接口
这次事故暴露了缓存穿透、雪崩和击穿的三重隐患,最终通过布隆过滤器、热点数据永不过期、分布式锁等组合拳解决。
原理或根因分析:三类缓存问题的本质差异
1. 缓存穿透:恶意请求的「穿刺」
当查询一个不存在的数据(如商品ID为9999999999),缓存和数据库都没有,每次请求都打到数据库。如果攻击者用脚本批量查询不存在的ID,数据库压力会呈指数级增长。
2. 缓存雪崩:大量缓存同时失效的「雪崩」
当大量缓存同时过期(如促销活动结束后,所有商品缓存统一过期),会导致瞬时请求洪峰。如果数据库无法处理,就会引发服务不可用。
3. 缓存击穿:热点数据失效的「穿刺」
当热点数据(如爆款商品)缓存失效,大量请求同时查询该数据,瞬间穿透到数据库。这种场景比穿透更危险,因为请求量级更大。
具体做法:四步构建缓存防御体系
1. 布隆过滤器:拦截无效请求的「第一道防线」
在缓存层前加布隆过滤器,过滤掉不存在的请求。我们采用Redis的BitMap实现,误判率控制在0.1%以下。
// 布隆过滤器初始化
BitMap bloomFilter = new BitMap(1000000, 0.1);
// 查询前先校验
if (!bloomFilter.contains(productId)) {
return "不存在的ID";
}
2. 热点数据永不过期 + 永久缓存
对访问频率高的数据(如爆款商品),设置永不过期,通过独立线程异步更新。我们用Redis的EXPIRE命令设置不同的过期时间,避免批量失效。
// 设置永不过期
redisTemplate.opsForValue().set("product:10001", product, -1, TimeUnit.SECONDS);
// 异步更新线程
@Async
public void asyncUpdateProduct(int productId) {
Product product = productService.getProduct(productId);
redisTemplate.opsForValue().set("product:"+productId, product, 24, TimeUnit.HOURS);
}
3. 本地缓存 + 分布式锁:应对热点击穿
对热点数据,使用本地缓存(如Guava Cache)做兜底。当缓存失效时,用分布式锁(如Redisson)确保只有一个线程去更新数据库。
// 本地缓存配置
Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
// 查询时优先本地缓存
Object cached = localCache.getIfPresent(productId);
if (cached != null) return cached;
// 使用分布式锁
RLock lock = redisson.getLock("lock:product:"+productId);
if (lock.tryLock()) {
try {
// 查询数据库并更新缓存
Product product = productService.getProduct(productId);
redisTemplate.opsForValue().set("product:"+productId, product, 24, TimeUnit.HOURS);
localCache.put(productId, product);
} finally {
lock.unlock();
}
}
4. 随机过期时间 + 预热机制
对批量过期的缓存,设置随机过期时间(如在24小时基础上加0-3小时随机数),避免集中失效。同时在业务高峰前预热缓存。
// 设置随机过期时间
long randomExpire = 24 * 3600 + new Random().nextInt(3 * 3600);
redisTemplate.opsForValue().set("product:"+productId, product, randomExpire, TimeUnit.SECONDS);
踩过的坑或反直觉之处
- 布隆过滤器的误判率:初期设置为0.5%,导致误判率高达1.5%,需要调整到0.1%以下
- 热点数据更新线程:初期用单线程导致更新延迟,改为线程池后性能提升3倍
- 本地缓存与Redis缓存的同步:需要确保本地缓存更新与Redis缓存更新时间一致,否则会出现数据不一致
- 分布式锁的性能开销:对非热点数据加锁反而增加延迟,需精准控制锁粒度
结论与可复用清单
| 问题类型 | 解决方案 | 关键参数 | |----------|----------|----------| | 缓存穿透 | 布隆过滤器 | 误判率<0.1% | | 缓存雪崩 | 随机过期时间 | 基础时间±随机数 | | 缓存击穿 | 本地缓存+分布式锁 | 线程池大小=CPU核心数 | | 数据一致性 | 异步更新 | 预热时间=业务高峰前1小时 |
最终效果:
- 缓存穿透率下降92%
- 缓存雪崩场景下数据库压力降低80%
- 热点数据查询响应时间从1500ms降至150ms
- 系统整体吞吐量提升3倍
参考: