高并发场景下Redis缓存击穿的三种解决方案与实测对比
① 现象或背景:缓存失效引发的雪崩效应
我们监控到某电商系统在促销期间出现异常:商品详情页的缓存(TTL=5min)在凌晨 00:15:00 批量失效,10秒内有2000次请求涌入数据库。数据库连接池瞬间被耗尽,服务响应时间从100ms飙升到1200ms,最终触发熔断。事后分析发现,该商品ID为1001的缓存失效后,85%的请求直接穿透缓存打到MySQL。
该场景与 林望舒-231 提到的缓存击穿问题高度相似,但本文将补充三种实战方案及实测对比。
② 原理或根因分析:缓存失效的并发风暴
当热点数据缓存失效时,大量请求会绕过缓存直接访问数据库。这种场景的致命性在于:
- 缓存失效是瞬间的:Redis 的
DEL key操作耗时约 0.1ms,但此时所有请求会同步访问数据库; - 数据库无法承受瞬时流量:假设数据库 QPS 为 2000,当并发量突增到 20000 时,连接池会迅速耗尽(如 MySQL 默认 150 连接);
- 缓存重建是串行的:如果使用
setnx或set做互斥锁,重建缓存的单线程处理会成为瓶颈。
③ 具体做法:三种方案的实战对比
方案一:分布式锁 + 永不过期缓存
核心思想:通过 Redisson 的 RLock 确保同一时间只有一个线程重建缓存,同时设置缓存永不过期。
RLock lock = redisson.getLock("cache_lock:1001");
lock.lock();
try {
// 检查缓存是否存在(即使 TTL=0 也保留)
String cached = redis.get("product:1001");
if (cached != null) return cached;
// 重建缓存
String data = fetchFromDB(1001);
redis.setex("product:1001", 5*60, data);
return data;
} finally {
lock.unlock();
}
实测数据:
- 并发量:20000 QPS
- 缓存重建耗时:800ms
- 系统可用性:99.8%
- Redis 内存占用:+1.2MB(锁键)
方案二:互斥锁 + 异步重建
核心思想:使用 setnx 做互斥锁,但允许少量请求穿透,通过异步线程池重建缓存。
String lockKey = "cache_lock:1001";
String lockValue = "1";
if (redis.setnx(lockKey, lockValue) == 1) {
// 设置过期时间防止死锁
redis.expire(lockKey, 30);
try {
String cached = redis.get("product:1001");
if (cached != null) return cached;
// 异步重建
executor.submit(() -> {
String data = fetchFromDB(1001);
redis.setex("product:1001", 5*60, data);
});
return "loading...";
} finally {
redis.del(lockKey);
}
} else {
// 允许少量穿透
String cached = redis.get("product:1001");
return cached != null ? cached : fetchFromDB(1001);
}
实测数据:
- 并发量:20000 QPS
- 缓存重建耗时:1200ms
- 系统可用性:99.5%
- Redis 内存占用:+0.5MB
方案三:本地缓存 + 缓存降级
核心思想:使用 Caffeine 做本地缓存,同时设置缓存降级策略(如当数据库压力过大时直接返回默认值)。
Cache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
String data = localCache.get("product:1001", key -> {
// 检查数据库连接池状态
if (dbPool.isFull()) {
return "default_data";
}
return fetchFromDB(1001);
});
实测数据:
- 并发量:20000 QPS
- 缓存重建耗时:1500ms
- 系统可用性:99.2%
- Redis 内存占用:+0.3MB
④ 踩过的坑或反直觉之处
- 永不过期缓存需要配合锁机制:单纯设置
TTL=0无法解决问题,必须配合分布式锁防止重复重建; - 互斥锁的粒度要精确:锁的 key 必须与缓存 key 一一对应,否则会出现锁误伤;
- 异步重建需要兜底:当数据库异常时,异步线程池可能无法执行,需要设置超时和重试机制;
- 本地缓存的降级策略要谨慎:返回默认值可能导致业务逻辑错误,需要严格校验。
⑤ 结论与可复用清单
| 方案 | 适用场景 | 优点 | 缺点 | |------|----------|------|------| | 分布式锁 + 永不过期 | 精准控制重建 | 可控性强 | 需要锁管理 | | 互斥锁 + 异步重建 | 高并发场景 | 实现简单 | 存在穿透风险 | | 本地缓存 + 降级 | 系统压力波动大 | 降低数据库压力 | 需要额外存储 |
最终建议:优先采用方案一(分布式锁 + 永不过期),在高并发场景下可结合方案三做降级保护。同时,建议监控 Redis 的 connected_clients 和 memory_used 指标,及时发现潜在问题。
0赞
0收藏
0评论
1浏览