缓存击穿是热点key过期瞬间并发重建导致的数据库穿透,需用redisson rlock实现带看门狗的互斥锁,并加随机ttl、二次检查及合理超时控制。

缓存击穿的本质是热点 key 过期瞬间的并发重建
商品详情页击穿不是“查不到”,而是“刚好过期时上百请求同时涌向数据库”。比如 product:1001 设置了 30 分钟 TTL,凌晨两点整过期,此时 200 个用户刷 iPhone 15 页面,全部穿透——这不是 Redis 慢,是业务逻辑没兜住。
关键判断:只要你的缓存 key 有明确过期时间(set(key, value, 30, MINUTES)),又服务高并发商品查询,就存在击穿风险。别信“QPS 不高不用管”,促销前预热失败、爬虫扫库存 ID 都可能触发。
用 Redisson 的 RLock 实现安全的互斥锁重建
别手写 SETNX + EXPIRE 组合,原子性难保证,超时释放也容易出错。Spring Boot 3 推荐直接上 Redisson,它封装了看门狗机制,锁自动续期,避免业务执行慢导致锁提前释放。
-
RLock lock = redissonClient.getLock("lock:product:" + id):锁 key 必须带业务标识,不能只用"lock",否则不同商品互相阻塞 -
lock.tryLock(10, 15, TimeUnit.SECONDS):最多等 10 秒抢锁,拿到后持有 15 秒(看门狗会自动续) - 抢锁失败时,**不要立即递归重试**,否则线程栈爆或雪崩;建议
Thread.sleep(50)后再查一次缓存,命中就走,不命中再试锁 - 加锁后必须做**二次检查**:
redisTemplate.opsForValue().get(cacheKey),因为可能在你等待锁时,别的线程已写入缓存
空值缓存与布隆过滤器要分场景用
击穿关注的是“存在但过期”的热点数据;而空值(如 product:-999)属于穿透范畴,混在一起处理反而拖慢正常路径。
- 对确定存在的商品 ID(比如前端传参来自搜索结果页),**不做空值缓存**,避免污染有效 key 空间
- 如果商品下架后 ID 仍被高频请求(如分享链接未及时失效),才在击穿流程里补一句:
if (product == null) { stringRedisTemplate.opsForValue().set(cacheKey, "", 2, MINUTES); } - 布隆过滤器(
RBloomFilter)适合全量预热场景,比如启动时加载百万 SKU;但对动态上架商品,实时更新过滤器有延迟,不能替代锁机制
过期时间设置和锁粒度直接影响稳定性
很多人把 TTL 设成固定 30 分钟,结果发现流量高峰总在整点附近抖动——这是过期时间扎堆导致的隐性击穿。
- 给 TTL 加随机偏移:
30 + ThreadLocalRandom.current().nextInt(5)分钟,打散过期峰 - 锁 key 必须精确到具体商品:
"lock:product:" + id,而不是"lock:product"全局一把锁,否则一个商品卡住,整个详情页不可用 - 数据库查询必须设超时(如 MyBatis 的
timeout属性),防止锁持有期间 DB 挂掉,锁永远不释放
真正难的不是写对一行 tryLock,而是想清楚:这个锁保护的是“重建缓存”这件事,不是“读取缓存”。一旦混淆,就会在缓存命中的路径里也去抢锁,白白增加 Redis 压力。











