首页/数据/数据库速查/分布式锁:安全释放(Lua 比对后删除)
Redis 运维与场景124 条

分布式锁:安全释放(Lua 比对后删除)

释放锁必须「先确认还是自己的锁、再删除」,且两步要原子;Lua 脚本把 GET 比对与 DEL 合成一次执行,避免两步之间锁过期被他人获得后误删。

数据库速查数据更新于 2026-10-13

安全释放锁是什么

释放分布式锁必须先确认「锁还是自己的」再删除,而且这两步要原子完成。否则会出现经典事故:业务 A 持锁期间任务超时,锁自动过期被业务 B 拿到,A 处理完后直接 DEL,把 B 的锁删掉。Lua 脚本把 GET 比对与 DEL 合成一次服务端执行,天然原子,是标准解法。

基本信息

适用版本EVAL 需 Redis 2.6+,全版本通用
锁值内容客户端唯一 token,如 UUID 加线程号
调用形式EVAL script 1 key token,KEYS 与 ARGV 分离传参
返回值1=删除成功(确属自己);0=已不是自己的锁
常见实现Redisson 等客户端库内置等价 Lua

典型使用场景

  • 订单防重复提交:多实例部署时保证同一订单只被处理一次
  • 定时任务互斥:多台机器上的同一定时任务只允许一轮执行
  • 库存扣减保护:配合原子脚本在锁内完成校验与扣减
  • 资源初始化互斥:保证缓存预热、索引构建只跑一次
  • 自研锁接替客户端库:把加锁与解锁都收敛到同一套 token 逻辑
  • 故障复盘:确认历史事故是否源于误删他人锁

Lua 安全释放与调用

Lua 安全释放与调用
# 加锁:值必须是本次调用独有的 token
SET lock:order:42 6f1d2c4e-9a1b NX PX 5000

# 释放:只有值等于自己的 token 才删除
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:order:42 6f1d2c4e-9a1b
# 返回 1 = 删除成功(锁确实是自己的)
# 返回 0 = 锁已过期或已被他人持有,不做任何动作

# 续期:同样要校验 token,避免给别人的锁续命
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end" 1 lock:order:42 6f1d2c4e-9a1b 5000

返回值读法

返回值含义后续动作
1token 匹配并删除成功正常释放,业务可继续
0锁已不属于自己(过期或他人持有)记录告警,排查任务是否超时
nil脚本执行异常或语法错误检查脚本引号转义与参数个数
客户端库(Redisson 等)都用等价 Lua 实现解锁;自研锁照抄这段即可,KEYS 与 ARGV 要通过参数传,不要拼进脚本文本。
安全释放只解决误删,不解决锁本身在主从切换瞬间可能被两个客户端同时持有的问题;对互斥性要求极高的场景应选用带共识的组件,并接受其性能与复杂度成本。

易错点

  • 解锁直接 DEL 不校验 token,删掉他人的锁,互斥性彻底失效
  • 用固定字符串当锁值(如 1 或实例名),多线程多进程之间无法区分
  • 把 token 拼进 Lua 脚本文本,命中脚本缓存后不同调用看到同一个值
  • 锁过期时间短于任务耗时且不做看门狗续期,业务未完成锁已释放但无人察觉
  • 认为 Lua 原子就万事大吉:加锁前的业务读取仍可能在锁外发生,需要把所有互斥逻辑放进锁内

数据来源与参考

  • Redis 官方文档: 分布式锁
  • Redis 官方文档: EVAL

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌