必须用Lua脚本原子校验value再删锁,因“GET+DEL”存在竞态窗口致误删;Redis事务无法实现条件删除;标准脚本需严格比对get结果与ARGV[1],且value须全局唯一不可预测。

解锁时“GET + DEL”两步操作必然导致误删
直接在客户端做 GET 判断再 DEL,看似逻辑清晰,实则存在不可修复的竞态窗口。只要锁在 GET 返回后、DEL 执行前过期,就可能被其他客户端抢占——此时你删的已不是自己的锁。
常见错误现象包括:库存超卖、重复发券、定时任务双跑。这类问题在线上往往表现为偶发、难复现、日志无报错,但后果严重。
- 客户端 A 执行
GET lock:order→ 得到自己的 value ✅ - 时间片切换 / GC 停顿 / 网络延迟 → 锁自动过期,Redis 删除 key
- 客户端 B 成功
SET lock:order B-uuid NX PX 30000✅ - 客户端 A 继续执行
DEL lock:order→ 实际删掉的是 B 的锁 ❌
Redis事务(MULTI/EXEC)也无法解决这个问题
MULTI 只能保证命令按序执行,不能根据中间结果做条件分支。你无法在事务里写 “如果 GET 结果等于我的 value,才执行 DEL” ——事务不支持 if 逻辑。
典型失败写法:
MULTI GET lock:xxx DEL lock:xxx EXEC
这段事务不管锁是不是你的,都会执行 DEL。它只是把两个非原子操作“打包”,并未赋予其原子语义。
- 事务中所有命令会被排队执行,但无运行时判断能力
- 即使
GET返回 nil 或他人 value,DEL仍会照常触发 - Redis 官方明确说明:事务 ≠ 原子条件操作
Lua 脚本在 Redis 单线程中是真正原子的执行单元
Redis 将整个 Lua 脚本当作一个不可分割的命令来处理:从读取、解析、执行到返回结果,全程独占主线程,期间不会被任何其他客户端命令打断。
标准解锁脚本长这样:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
-
KEYS[1]是锁 key,ARGV[1]是加锁时写入的唯一 value(如8f3a2b4c:123) - 整个 if-else 在服务端一次完成,不存在中间状态暴露
- 返回 1 表示删除成功,0 表示跳过(不删、不报错、不干扰)
- 注意:脚本里不能用
redis.call("expire", ...)这类阻塞命令,否则会卡住整个 Redis
不校验 value 的 Lua 脚本也等于没用
只用 Lua 不代表安全。见过有人写这样的脚本:
return redis.call("del", KEYS[1])
这跟客户端直连 DEL 没区别,只是把网络往返省了,但完全没解决“谁的锁谁删”这个核心问题。
真正关键的不是“用了 Lua”,而是脚本里是否严格比对了 redis.call("get", KEYS[1]) == ARGV[1]。漏掉这行校验,等于把分布式锁的互斥性拱手交出。
另外,value 必须全局唯一且不可预测——用 UUID 或 process_id + thread_id + timestamp 拼接,别用固定字符串或自增 ID。











