根本原因是DEL操作不认人,必须用Lua脚本保证“校验value+删除key”原子执行:先通过redis.call('GET', KEYS[1])获取锁值,仅当等于ARGV[1](当前线程唯一标识)时才执行redis.call('DEL', KEYS[1]),否则返回0,彻底杜绝误删。

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

















