redis分布式锁必须用lua脚本实现加锁和解锁以保证原子性,因setnx+del存在竞态漏洞,expire与setnx间有窗口,且解锁需校验owner并原子执行。

Redis 分布式锁必须用 Lua 脚本实现加锁和解锁,否则无法保证原子性——哪怕只差毫秒级的竞态,也会导致锁失效或误删。
为什么 SETNX + DEL 组合一定出问题
单纯用 SETNX 加锁、再用 DEL 解锁,看似简单,实则存在经典竞态漏洞:客户端 A 成功加锁,但执行业务逻辑超时,锁自动过期;此时客户端 B 获取到锁;A 接着执行完,无脑调用 DEL 删除 key,结果把 B 的锁删了。
- 根本原因:加锁和删锁是两个独立命令,中间没有原子屏障
-
EXPIRE和SETNX之间也存在窗口(即使用事务也不行,因为 Redis 事务不支持条件执行) - Redis 6.0+ 的
SET key value EX seconds NX可以解决加锁原子性,但删锁仍需判断 owner
解锁必须校验锁拥有者,且整个操作不可拆分
解锁不是“删掉 key 就完事”,而是“只有当前线程(通过唯一 value 标识)才能删自己设的锁”。这个判断+删除必须在一个 Lua 脚本里完成,由 Redis 串行执行。
典型安全解锁脚本:
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(如"order:lock:123") -
ARGV[1]是加锁时写入的随机唯一值(如"8f4b3a2e-1d5c-4a9f-b0e1-7c8d9a0b1c2d"),防止误删 - 返回
1表示成功释放,0表示非持有者调用,拒绝操作
加锁脚本要兼顾唯一性、过期时间、重入安全(可选)
基础加锁脚本只需一行:SET key value EX seconds NX。但生产中建议封装为 Lua,便于扩展逻辑(比如记录线程 ID、支持有限重入)。
最小可行加锁 Lua 示例:
if redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2], "NX") then
return 1
else
return 0
end
-
ARGV[2]是 TTL(单位秒),必须设置,避免死锁 - 不要用毫秒级
PX除非客户端时钟严格同步,否则可能因系统时钟回拨导致锁提前失效 - value 必须全局唯一(推荐 UUID v4 或加密随机字符串),不能用进程 PID 或时间戳
- 若需重入,得额外维护计数器(例如 hash 结构存 owner + count),但会显著增加复杂度和开销
实际使用时最容易被忽略的三个细节
很多人写了 Lua 脚本,却在线上出问题,往往栽在这三点:
- 客户端没做
EVALSHA回退逻辑:首次用EVAL发送脚本,后续应缓存 SHA1 值并用EVALSHA调用,否则网络和解析开销翻倍 - 锁过期时间远小于业务执行时间,又没配自动续期(watchdog):Redisson 等库的
lockWatchdogTimeout就是干这个的,自己实现需另起守护线程定期EXPIRE - 没处理连接中断场景:加锁成功但网络断开,客户端不知道结果,不敢继续业务,也不敢直接重试(怕重复加锁)。稳妥做法是加锁前生成唯一请求 ID,业务幂等落库,再配合锁状态查询
分布式锁不是“加了就稳了”,它的可靠性取决于你对边界条件的掌控程度,而不是脚本是否用了 Lua。










