redis缓存击穿不能只靠setnx加锁,因其非原子性易致死锁,且锁粒度耦合业务;应使用set nx ex原子命令实现dcl,并配合lua脚本安全删锁。

为什么 Redis 缓存击穿不能只靠 SETNX 加锁
缓存击穿本质是:某个热点 key 过期瞬间,大量并发请求同时发现缓存缺失,全部穿透到数据库,造成瞬时压力激增。单纯用 SETNX 加锁看似能串行化重建缓存,但存在两个致命问题:锁未释放(进程崩溃/超时)导致死锁,以及锁粒度与业务逻辑耦合,容易漏加或错加。更关键的是,SETNX 本身不带自动过期,必须搭配 EXPIRE,而这两步不是原子的——中间若服务宕机,锁就永远卡住。
Redis 实现 DCL 的核心:用 SET 命令的 NX EX 原子组合
真正可行的 DCL 必须满足:第一次检查缓存缺失 → 尝试原子获取锁 → 成功则重建缓存并释放锁 → 失败则短暂等待后重试。其中“尝试获取锁”这一步,必须用单条原子命令完成,否则无法避免竞态。Redis 2.6.12+ 的 SET 支持 NX(仅当 key 不存在时设置)和 EX(设置过期时间)组合:
SET lock:goods:1001 "random_value" NX EX 10
这个命令同时完成“加锁 + 设置 10 秒过期”,杜绝了 SETNX + EXPIRE 的非原子风险。注意:random_value 是必须的,用于后续校验锁归属,防止误删他人锁。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 锁的 value 不能写死(如
"1"),否则 A 拿到锁后超时释放,B 又拿到锁,A 结束时用 DEL 删除,会把 B 的锁也干掉 - 推荐用 UUID 或线程安全的随机字符串(如 Java 的
UUID.randomUUID().toString()) - 锁过期时间要明显大于缓存重建耗时(比如重建最多 2s,锁设 5–10s),留出安全余量
Java 中完整 DCL 流程的关键代码片段
以 Jedis 为例,重点不在“怎么写”,而在“哪里容易错”:
String key = "cache:goods:1001";
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
// 第一次检查:缓存是否存在
String cached = jedis.get(key);
if (cached != null) {
return cached;
}
// 第二次检查:尝试加锁(原子)
String result = jedis.set(lockKey, lockValue, "NX", "EX", 10);
if ("OK".equals(result)) {
try {
// 真正查库、重建缓存
String dbData = loadFromDB(1001);
jedis.setex(key, 60, dbData); // 缓存设 60s 过期
return dbData;
} finally {
// 必须用 Lua 脚本删锁,确保“判断 value == 自己的 + DEL”原子执行
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
}
} else {
// 加锁失败,短暂休眠后重试(避免密集轮询)
Thread.sleep(50);
return getCacheWithDCL(key); // 递归或循环重试
}
- 删锁不用
jedis.del(lockKey),必须用 Lua 脚本,否则可能删错锁 - 重试不能无限循环,建议加最大重试次数(如 3 次)或指数退避
- 如果业务允许,可在锁竞争期间返回旧缓存(哪怕已过期),比直接阻塞更友好
比 DCL 更轻量的替代方案:逻辑过期 + 后台刷新
对高并发读、低频更新的场景(比如商品详情页),DCL 的锁开销和复杂度其实没必要。更常用的是“逻辑过期”:缓存 value 里额外存一个 expireTime 字段,而不是依赖 Redis 的 TTL。读取时先判断逻辑时间是否过期;若过期,则用 SETNX 尝试标记“我来刷新”,成功者异步重建缓存,失败者直接返回旧值。
- 避免了所有加锁、等待、重试逻辑,吞吐更高
- 要求业务能容忍短暂脏读(即返回过期但未被覆盖的旧数据)
- 需确保后台刷新任务幂等,且失败时有告警机制,否则缓存会永久陈旧
真正难的从来不是写出 DCL 代码,而是判断当前业务到底需要强一致性(选 DCL),还是可用性优先(选逻辑过期)。很多团队一上来就堆锁,结果压测时发现 80% 的耗时花在了锁等待上,却没意识到缓存本身根本不需要实时精确。










