缓存击穿本质是热点key过期瞬间的并发重建问题,需用redisson分布式锁+双重检查解决,@cacheable无法单独防护,永不过期+主动刷新为替代方案但有数据一致性代价。

缓存击穿本质是热点 key 过期瞬间的并发重建问题
缓存击穿不是“查不到”,而是“刚过期就被大量请求撞上”。比如 product:1001 设置了 30 分钟 TTL,零点整过期,此时 200 个线程同时发现缓存为空,全部涌向数据库——这就是击穿。它和穿透(查根本不存在的 key)、雪崩(大批 key 集中失效)逻辑不同,必须用“串行化重建”来解决。
用 Redisson 分布式锁 + 双重检查是最稳妥的落地方式
单纯用 SETNX 手写锁容易出错(比如没处理锁释放异常、没设过期时间导致死锁)。Redisson 封装了自动续期、可重入、看门狗等能力,生产环境更可靠。
- 加锁 key 要带业务标识,如
lock:product:1001,避免不同商品抢同一把锁 -
tryLock(10, 15, TimeUnit.SECONDS)表示最多等 10 秒,拿到锁后自动续期 15 秒,防止业务执行超时导致锁提前释放 - 锁内必须做第二次缓存检查(
get(cacheKey)),因为可能有其他线程在你等待期间已写入 - 空值也要缓存(哪怕只缓存 5 分钟),否则恶意刷不存在的 ID 会绕过锁直接打库
@Cacheable 注解无法单独防击穿,必须配合自定义逻辑
@Cacheable 默认是“缓存未命中就放行所有请求去执行方法”,它不提供锁机制。如果你直接写:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productMapper.selectById(id);
}
那击穿风险完全暴露。可行的折中方案是:保留 @Cacheable 做基础缓存,但对明确的热点接口(如首页商品、秒杀商品)改用手动锁 + RedisTemplate 控制流程,不要图省事全包给注解。
永不过期 + 主动刷新是替代方案,但有数据一致性代价
对极少数超高频且更新不频繁的 key(比如系统配置、城市字典),可以设为永不过期,再起一个定时任务或监听 binlog 异步刷新。但这要求业务能接受“缓存比数据库慢几秒”的延迟,且刷新失败时需有降级兜底(比如报警+人工介入),不能当作通用解法。
真正难的不是写锁,而是判断哪些 key 算“热点”——它得靠监控(如 Redis 的 KEYS product:* | grep -c '1001' 或 APM 工具统计访问频次),而不是凭感觉拍脑袋。漏判会导致击穿,误判又会浪费锁资源。










