缓存击穿本质是单个热点key过期瞬间大量并发请求穿透至数据库,需用原子命令set key value EX seconds NX加key级细粒度锁,并配合双重检查、合理超时与空值缓存来防控。

缓存击穿在 Spring Cloud 场景下,本质是「单个热点 key 过期瞬间,大量并发请求同时穿透到数据库」。它不是分布式锁用不用的问题,而是锁怎么用、用在哪、会不会反向拖垮系统的问题。
为什么不能只靠 setnx 加锁?
很多方案直接拿 setnx 做锁,但忽略两个致命细节:
-
setnx不带超时,如果业务线程在查库后崩溃或卡住,锁永远不释放,后续所有请求全被阻塞 - Spring Cloud 微服务通常是多实例部署,
setnx只作用于单个 Redis 节点,无法跨实例保证互斥(除非你用的是单节点 Redis,但生产环境几乎不会)
所以必须用原子命令:set key value EX seconds NX —— 一条命令完成「加锁 + 设置过期」,避免死锁。
锁粒度要细到 key 级,而不是 service 级
常见错误是全局一把锁,比如对整个商品服务加一个 lock:shop,结果所有商品请求都排队——这等于把高并发变成了串行,吞吐量断崖下跌。
正确做法是锁和缓存 key 对齐:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 缓存 key 是
cache:shop:1001,锁 key 就该是lock:shop:1001 - 每个热点数据独立竞争锁,互不影响
- 锁超时时间建议设为
db.query.time * 2 + 1s,比如查库平均耗时 300ms,锁设为 700ms 即可,既防卡死又不过长
必须做双重检查(Double Check),且顺序不能错
加锁后不能直接查库,否则仍可能多个线程同时重建缓存。标准流程是:
- 读缓存 → 空
- 尝试加锁 → 成功
- 再次读缓存 → 仍空才查库写缓存
- 释放锁
伪代码关键逻辑:
String cacheKey = "cache:shop:" + id;
String lockKey = "lock:shop:" + id;
<p>String shopJson = stringRedisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isNotBlank(shopJson)) {
return JSONUtil.toBean(shopJson, Shop.class);
}</p><p>// 注意:这里用 setIfAbsent + expire 分两步是错的!必须原子
Boolean isLocked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 700, TimeUnit.MILLISECONDS);</p><p>if (Boolean.TRUE.equals(isLocked)) {
try {
// 再次检查缓存,防刚加锁时别人已写入
shopJson = stringRedisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isBlank(shopJson)) {
Shop shop = shopMapper.selectById(id);
if (shop != null) {
stringRedisTemplate.opsForValue()
.set(cacheKey, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES);
} else {
// 空值也缓存,防穿透
stringRedisTemplate.opsForValue()
.set(cacheKey, "", 2, TimeUnit.MINUTES);
}
}
} finally {
stringRedisTemplate.delete(lockKey); // 锁必须释放,哪怕异常
}
} else {
// 没抢到锁,等 50ms 后重试(避免自旋消耗 CPU)
Thread.sleep(50);
return queryById(id);
}Spring Cloud 环境下容易被忽略的坑
微服务架构里,问题往往藏在链路协同上:
- 网关层(如 Spring Cloud Gateway)默认不透传请求头,如果用了基于 header 的限流或熔断,缓存逻辑可能失效
- Feign 客户端默认超时是 1s,而锁等待 + DB 查询可能超时,导致 fallback 触发却没清锁,引发连锁阻塞
- Redis 连接池配置不当(如
max-active=8),高并发下连接耗尽,setIfAbsent调用本身就会失败或超时,锁机制形同虚设
真正起作用的不是“用了分布式锁”,而是锁的生命周期是否严格匹配业务耗时、是否与网关/客户端/连接池形成配合闭环。

















