根本原因是DEL操作不认人,必须用Lua脚本保证“校验value+删除key”原子执行:先通过redis.call('GET', KEYS[1])获取锁值,仅当等于ARGV[1](当前线程唯一标识)时才执行redis.call('DEL', KEYS[1]),否则返回0,彻底杜绝误删。
为什么 Redis 会释放别人的锁
根本原因就一条:
操作不认人。只要键存在,
就直接删,不管这个锁是谁设的、值是多少。常见误删场景是:线程 A 的业务执行超时,Redis 自动过期释放了锁;线程 B 立即抢到锁并写入自己的标识(比如
);线程 A 此时终于执行完,还拿着自己老的
去
判断,发现键已不存在(或刚好被 B 写入),但它的逻辑没做兜底,直接执行
—— 实际上删的是 B 刚设的锁。
用 UUID 标识锁持有者但不用 Lua 的风险
只靠
+
再
是不够的。问题出在“判断”和“删除”之间存在时间窗口:
线程 A
返回
,确认是自己的锁
此时锁恰好过期,Redis 自动删除该 key
线程 B 成功
线程 A 执行
,删掉的是线程 B 的锁
这个窗口哪怕只有几毫秒,在高并发或 GC 暂停场景下也极易触发。UUID 只解决了“谁设的”问题,没解决“判断与删除必须一气呵成”这个原子性要求。
为什么必须用 Lua 脚本做比对删除
Redis 执行 Lua 脚本是原子的:整个脚本要么全执行,要么全不执行,中间不会被其他客户端命令插入。典型脚本如下:
Redis 8.2.3
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
下载
关键点:
是锁 key(如
)
是当前线程的 UUID(如
)
和
在同一个原子上下文中完成
返回
表示删除成功,
表示不是自己的锁,不做任何操作
实际使用中容易忽略的细节
很多人写了 Lua 脚本却还是出问题,常踩的坑有:
没校验脚本执行返回值 ——
返回
时,仍当成删除成功继续后续逻辑
UUID 生成方式不合理 —— 用
每次都新生成,导致重入失败;应在线程首次获取锁时生成并复用
锁 key 设计没带业务上下文 —— 比如统一用
,多个业务互相干扰;应按资源粒度隔离,如
过期时间设得太短 —— 业务耗时波动大时,锁提前释放,Lua 脚本再严谨也防不住竞争
最要命的一点:以为加了 UUID + Lua 就万事大吉,结果忘了锁本身没做可重入设计,同一线程重复获取锁时直接失败,反而引发更隐蔽的并发问题。
DELDEL lock_key"uuid-b""uuid-a"GETDELSET lock_key "uuid-a" NX EX 30GET lock_keyDELGET lock_key"uuid-a"SET lock_key "uuid-b" NX EX 30DEL lock_keyif redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
endKEYS[1]"lock:order:123"ARGV[1]"a1b2c3-...-d4e5"redis.call('get', ...)redis.call('del', ...)10execute(...)0UUID.randomUUID()"lock""lock:stock:1001"