缓存击穿需用分布式锁而非本地锁,因sync=true仅限单jvm;redisson双重检查方案最稳妥:锁内二次查缓存防穿透,锁key须与业务key分离且带标识,leasetime需大于查库+写缓存耗时并留余量。

缓存击穿在高并发场景下会直接压垮数据库,核心原因是热点 Key 过期瞬间多个请求同时穿透。用本地锁(synchronized)完全无效,必须用分布式锁;而「双重检查 + Redisson」是目前最稳妥、开箱即用的组合方案。
为什么 @Cacheable(sync = true) 不足以解决缓存击穿
Spring Cache 的 sync = true 确实会在缓存未命中时加本地锁,但它只在单 JVM 进程内生效。在多实例部署(如 Kubernetes 多 Pod、Tomcat 集群)下,每个节点都各自加自己的锁,根本无法互斥 —— 依然会有 N 个请求同时查库。
- 它底层用的是
ConcurrentHashMap+ReentrantLock,不跨进程 - 即使你配置了 RedisCacheManager,
sync = true也不走 Redis,更不涉及分布式协调 - 如果你的应用是单体且确定永不水平扩展,它才勉强可用;否则就是假安全感
Redisson.getLock() 加锁时的三个关键参数怎么设
调用 lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS) 时,这三个值不是随便填的,直接影响锁的可靠性与响应性:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
waitTime(等待获取锁的最大时间):建议 1–3 秒。太长会让请求堆积,太短容易频繁重试加重负载 -
leaseTime(锁自动释放时间):必须大于「查库 + 写缓存」的最长耗时。例如 DB 查询平均 200ms,序列化+写入最多 800ms,那就至少设为 2 秒;再加点余量,设为 3–5 秒更稳妥 - 单位统一用
TimeUnit.SECONDS,避免传错单位导致锁秒级失效 - 注意:
leaseTime不能设为 -1(永久),否则依赖手动 unlock,一旦异常未执行就会死锁;Redisson 的看门狗机制只对有限期锁生效
双重检查为什么必须写在 tryLock 成功后
这是防止「锁已释放但缓存还没写完」导致的重复加载。典型错误是把第二次查缓存放在 tryLock 外面,或只做一次检查。
// ❌ 错误:没进锁就查了一次,可能刚查完别人就写进去了,但你还去查库
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) return product;
RLock lock = redissonClient.getLock("lock:product:" + id);
if (lock.tryLock(2, 5, TimeUnit.SECONDS)) {
// ✅ 正确:进锁后再查一次 —— 防止 A 刚释放锁、B 就拿到锁、C 还在等,此时缓存可能已被 A 写入
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) return product;
product = productRepository.findById(id).orElse(null);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
}
return product;
}
- 第一次检查在锁外,是为了快速返回已有缓存,减少锁竞争
- 第二次检查在锁内,是真正防击穿的防线:确保只有第一个抢到锁的线程查库,其余人在锁里查一次缓存就直接返回
- 如果省略第二次检查,多个线程可能在「锁释放 → 缓存写入」这个极小时间窗口内先后获得锁,全部查库
锁 key 和业务 key 分离是硬性要求
锁的 key 必须和缓存 key 有明确区分,且带业务标识。常见错误是直接用 "product:"+id 当锁名 —— 这会导致不同业务互相干扰或锁粒度失控。
- 推荐格式:
"lock:product:"+id或"lock:cache:product:"+id - 绝对不要用固定锁名(如
"lock:product"),那会把整个商品模块串行化 - 也不要拼接用户 ID、Session ID 等动态敏感信息进锁名,除非你明确需要按用户隔离,否则增加 key 数量和内存压力
- Redisson 的锁 key 默认带前缀
redisson_lock:{xxx},你传进去的只是逻辑名,不用自己加前缀
真正容易被忽略的是锁 key 的生命周期管理:它不随业务逻辑结束而消失,只靠 leaseTime 自动清理。如果 leaseTime 设得太短,又没开启看门狗(比如用了低版本 Redisson 或禁用了),就可能在业务还没做完时锁就被释放,后续线程再次闯入 —— 这种竞态很难复现,但线上会偶发数据错乱或 DB 压力突增。










