连载中 2/20

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

2026-06-25 · 4099 阅读 · 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)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞