连载中 2/20

从 SETNX 到 SET NX PX:一条命令的二十年进化

2026-06-25 · 4101 阅读 · 0 评论 · 0 赞

最朴素的开始:SETNX

解决两店同时扣款,老王的第一版方案朴素得可爱:Redis 里放个 key,谁的请求能把这个 key 写进去,谁就拿到锁。

SETNX lock:card:8801 1        # key 不存在才写入,返回 1 表示抢到锁
# ……执行扣款业务……
DEL lock:card:8801            # 干完活,释放锁

SETNX(SET if Not eXists)的妙处在于「判断存在」和「写入」是一条命令,Redis 单线程执行命令,天然原子——不存在「两个请求同时看到 key 不存在、然后都写入」的竞态。互斥这一关,过了。但这套代码上线第二天就炸了:A 实例抢到锁之后,执行扣款时恰好碰上老王的服务器断电重启,DEL 没来得及执行。这把锁就这么永远留在 Redis 里,后续所有扣款请求都在 SETNX 上碰壁,整张卡的业务停摆——锁是防死锁的,结果自己成了死锁。

第一代坑:加过期时间,却拆成了两步

解决「持有人失联」的直觉答案是加过期时间,锁超时自动释放。于是代码变成了:

SETNX lock:card:8801 1        # 抢锁
EXPIRE lock:card:8801 10      # 设 10 秒过期

两步写法,原子性碎了。SETNX 成功之后、EXPIRE 执行之前,客户端崩了——锁写进去了但没设过期,永生锁死灰复燃。这不是抬杠,进程被 kill -9、网络抖断、GC 停顿,任何一种都能让程序死在两行代码之间。根治要等 Redis 2.6.12:SET 命令支持一条顶三条——

SET lock:card:8801 8f3a9c NX PX 10000
# NX:不存在才设置(互斥);PX 10000:10 秒过期(防死锁)
# 返回 OK 拿到锁,返回 nil 被别人抢了

一条命令,原子地把「互斥」和「防死锁」两个指标同时拿下。顺手把 value 从固定值 1 换成了随机字符串 8f3a9c——这是为下一个坑埋的伏笔。

第二代坑:DEL 删了别人的锁

设想过这样一个时间线:A 抢到锁,执行扣款;碰上数据库抖动,业务跑了 12 秒;锁 10 秒到期自动释放;B 顺势抢到锁开始干活;A 终于跑完了,很自觉地去 DEL——删掉的是 B 的锁。C 又抢到锁,A、B、C 三个「持有人」同时在临界区里跑,互斥碎了一地。锁过期本身没错,错在 A 释放锁的时候根本没看这把锁还是不是自己的。这就是第一篇说的「可辨识」:value 里存唯一标识(UUID 或机器号加线程号),删除之前先核对——

// 错误示范:校验和删除分两步,照样有竞态
if (redis.get(lockKey).equals(myId)) {   // 校验:还是我的
    redis.del(lockKey);                  // ← 就在这一瞬间,锁过期,B 抢到
}

校验通过到 DEL 执行之间,锁可能恰好过期、B 可能恰好抢到——校验的是「过去」,删除的是「现在」,两步之间隔着无限可能。GET 和 DEL 必须原子完成,Redis 单线程能原子执行的是「一条命令」,那就把两步打包成一条——Lua 脚本在 Redis 端被当作一个整体执行:

-- 安全释放:身份匹配才删,整个脚本原子执行
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

三代写法一张表

写法解决的问题留下的坑
SETNX + DEL互斥持有人失联,锁永生
SETNX + EXPIRE锁超时自动释放两步不原子,永生锁复活
SET NX PX + UUID + Lua 删除互斥、防死锁、可辨识全齐业务时长没有上限,锁先过期怎么办

第三行最后一格是全系列最深的一个坑:锁的过期时间必须大于业务执行时长,但业务时长理论上没有上限——数据库就是可以抖 30 秒,GC 就是可以停 20 秒(分布式事务系列第 14 篇讲过三态结果,这里的根源一样)。过期时间设多短都赌不赢,正确的姿势是「锁快过期了,持有人还在干活,就给它续命」——这就是大名鼎鼎的看门狗机制,下一篇的主角。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#分布式锁#SETNX#SET NX PX#Lua脚本#误删锁

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞