正确释放Redis分布式锁必须用Lua脚本原子校验value后再DEL,预加载SCRIPT LOAD+ EVALSHA,配合唯一value、TTL续期及高可用Redis部署。

释放锁时用 DEL 会误删别人持有的锁
Redis 分布式锁的核心风险不在加锁,而在释放——如果用 DEL key 直接删,只要锁过期或客户端崩溃,后续任意客户端都能删掉当前持有者的锁,彻底破坏互斥性。
正确做法是:释放前校验锁的 value 是否属于自己。标准方案是用 Lua 脚本原子执行「判断 + 删除」:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
其中 KEYS[1] 是锁名(如 "order:123"),ARGV[1] 是客户端生成的唯一标识(如 UUID 或随机字符串)。
- 必须用 Lua 脚本,不能拆成
GET+DEL两步——中间可能被其他客户端抢占 - value 不能用时间戳或进程 ID,必须是全局唯一、不可预测的字符串,否则可能被猜中伪造
- 脚本返回
1表示成功释放,0表示锁不属于当前客户端,不要当成错误重试
EVAL 脚本要预加载,别每次传完整 Lua
高频调用时反复传输 Lua 脚本会增加网络开销和 Redis 解析负担,还可能触发 maxmemory 策略下的淘汰风险。
生产环境应使用 SCRIPT LOAD 预加载,再用 EVALSHA 执行:
SCRIPT LOAD "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"
得到 SHA1 值(如 "a1b2c3...")后,后续释放都用:
EVALSHA a1b2c3... 1 order:123 550e8400-e29b-41d4-a716-446655440000
- 首次加载失败(如 Redis 重启)需自动 fallback 到
EVAL,但不应默认每次都走EVAL - 客户端 SDK(如 Jedis、redis-py)通常封装了自动加载逻辑,确认是否启用
evalShaEnabled或类似配置 - 脚本内容变更后必须更新 SHA 缓存,否则执行旧逻辑导致释放失效
锁续期(renew)不是可选功能,而是安全前提
业务执行时间不确定,而 Redis 锁必须设 TTL(否则宕机不释放),这就导致「锁提前过期」和「业务未结束」的竞态——此时别的客户端可能拿到锁,造成并发写入。
解决方式是:在加锁时启动独立看门狗线程/协程,定期用相同 value 对锁 TTL 做 EXPIRE 延长(注意仍是原子判断:只延长自己持有的锁)。
- 续期间隔建议设为 TTL 的 1/3(如锁 30s,每 10s 续一次),留出网络抖动余量
- 续期也必须校验 value,否则可能把别人刚抢到的锁又续上
- 客户端断连后看门狗应自动停止,避免僵尸续期;可用
SET key val PX 30000 NX加锁时记录 client ID 到本地状态
Redlock 不解决单 Redis 故障,反而增加复杂度
很多人听说 Redis 单点故障,就直接上 Redlock(向多个独立 Redis 实例加锁),但它的安全模型依赖严格的时间同步和半数以上节点存活,实际部署中 NTP 漂移、GC 暂停、网络分区都会打破假设。
对绝大多数服务,更稳妥的做法是:用单个高可用 Redis(如哨兵或 Cluster),配合合理的锁 TTL、value 校验、续期机制。
- Redlock 的
quorum判断逻辑在客户端实现容易出错,且无法防止脑裂场景下的双主写入 - 如果你的 Redis 已经是云厂商托管的高可用实例(如 AWS ElastiCache、阿里云 Tair),Redlock 带来的收益远小于运维成本
- 真正需要跨数据中心强一致?那该换用 ZooKeeper 或 etcd,而不是硬套 Redis

















