Redis 缓存击穿的原因及解决方案
Redis 缓存击穿(Cache Breakdown)是指某个热点数据在缓存中失效或被淘汰的瞬间,大量并发请求直接穿透缓存层访问数据库,导致数据库压力骤增甚至崩溃的现象。其核心原因是高并发场景下热点数据失效,具体原因及解决方案如下:
一、击穿的直接原因
1. 热点数据过期
• 场景:某个高频访问的 Key(如秒杀库存、热门商品信息)因过期时间到期或被淘汰策略(如 LRU)移除。
• 影响:所有并发请求同时发现缓存失效,直接冲击数据库。
• 示例:电商大促期间,某爆款商品的缓存过期,用户集中抢购导致数据库压力激增。
2. 缓存重建耗时过长
• 场景:当缓存失效后,多个线程同时触发数据库查询和缓存重建操作。
• 影响:数据库短时间内承受大量重复查询,性能下降甚至宕机。
• 示例:用户首次访问某新闻详情页时,缓存失效,后续大量用户同时请求导致数据库过载。
二、击穿的深层诱因
1. 缓存设计缺陷
• 统一过期时间:大量 Key 设置相同过期时间,导致集体失效。
• 缺乏热点标记:未识别热点数据并单独管理其生命周期。
2. 并发控制不足
• 无锁机制:多个线程同时检测到缓存失效,重复执行数据库查询。
• 单线程重建:未利用异步或分布式锁控制缓存重建流程。
3. 业务场景特性
• 时点性事件:如零点促销、活动开启等,导致缓存集中失效。
• 数据冷热不均:部分数据长期未被访问,突然因业务需求成为热点。
三、解决方案
1. 互斥锁(Mutex Lock)
• 原理:通过分布式锁(如 Redis 的 SETNX)确保仅一个线程执行数据库查询和缓存重建,其他线程阻塞等待。
• 代码示例(Java):
public String getDataWithMutex(String key) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 尝试获取分布式锁
boolean locked = redissonClient.getLock(“lock:” + key).tryLock();
if (locked) {
try {
// 再次检查缓存(双重校验)
value = redisTemplate.opsForValue().get(key);
if (value == null) {
value = loadFromDB(key); // 从数据库加载
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
}
} finally {
redissonClient.getLock(“lock:” + key).unlock();
}
} else {
// 等待重试
Thread.sleep(100);
return getDataWithMutex(key);
}
}
return value;
}
• 优点:避免重复查询数据库。
• 缺点:可能引发线程阻塞,需权衡锁粒度与性能。
2. 永不过期 + 异步更新
• 原理:对热点数据设置永不过期,通过后台线程定期更新缓存。
• 实现步骤:
1. 缓存 Key 永久有效。
2. 启动独立线程或定时任务(如 Cron)异步加载新数据。
3. 更新时使用双删策略(先删缓存再更新数据库,最后再删缓存)避免脏数据。
• 适用场景:数据更新频率低且实时性要求不高(如配置信息)。
3. 逻辑过期时间
• 原理:在缓存 Value 中嵌入逻辑过期时间字段,业务层先校验时间,若过期则异步重建。
• 数据结构示例:
{
“data”: { … },
“expire_time”: “2025-11-17T10:00:00Z”
}
• 优点:无需物理过期,避免集中失效。
• 缺点:增加存储开销,需处理数据一致性。
4. 布隆过滤器(Bloom Filter)
• 原理:在缓存层前部署布隆过滤器,拦截不存在 Key 的请求。
• 适用场景:缓存穿透与击穿结合的场景(如恶意请求不存在 Key)。
• 代码示例(Guava):
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000);
// 预加载合法 Key
bloomFilter.put(“valid_key_1”);
// 请求拦截
if (!bloomFilter.mightContain(key)) {
return null; // 直接返回空
}
5. 多级缓存
• 架构:本地缓存(如 Caffeine) + Redis 缓存 + 数据库。
• 优势:本地缓存减少 Redis 依赖,降低击穿概率。
• 实现:
// 先查本地缓存
String value = localCache.get(key);
if (value == null) {
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value); // 回填本地缓存
}
}
四、最佳实践
1. 监控与预警:实时监控缓存命中率、数据库 QPS,设置阈值告警。
2. 容量规划:为热点数据预留缓存容量,避免淘汰策略误伤。
3. 限流降级:结合 Sentinel 或 Hystrix 对数据库访问限流,保障核心服务可用。
通过上述策略,可有效缓解缓存击穿问题,提升系统在高并发场景下的稳定性。





