Redis 分布式锁为什么要 value 唯一并用 Lua 删除?
简化版
锁的 value 要为每次获取生成唯一 owner token,释放时只有 token 相同的客户端才有权删除。比较 value 与 DEL 必须放在同一段 Lua 脚本原子执行,否则锁可能在两条命令之间过期并被别人重新获得,旧客户端就会误删新锁。
详细版
获取使用一条 SET lockKey ownerToken NX PX ttl,把不存在判断、写 value 和 TTL 原子完成;释放用 Lua 先比较 value,再删除。不能使用固定实例 ID 代替每次唯一 token,也不能先 GET、回到客户端判断后再 DEL。
这套做法解决获取原子性和误删问题,但不能阻止租约过期后的旧客户端继续写业务资源。长任务需安全续期,数据安全要求高时还要用 Fencing Token、幂等与数据库约束;Redis 故障转移下的复制语义也必须纳入选型。
完整版教学
一、误删是怎样发生的
A 获得 10 秒锁后暂停 15 秒,Redis 已让 key 过期;B 随后设置同名锁。A 恢复后若直接 DEL key,删除的是 B 的锁,C 便能再次进入临界区。问题不在 key,而在 A 无法证明当前记录仍属于自己。
记忆钩子:key 标识被锁资源,value 标识这一代所有者;删除前必须同时核对两者。
二、获取为什么使用一条 SET
早期写法先 SETNX 再 EXPIRE,若进程在两条命令之间宕机,会留下没有 TTL 的永久锁。现代 Redis 用单条 SET 同时表达不存在才写、保存 owner 和设置毫秒过期。
SET order:123 8f3c... NX PX 30000
OK -> 获得锁
nil -> 锁已存在
TTL 是故障回收机制,不代表客户端到期时会被停止。
三、唯一 value 怎样生成
value 至少在同一 key 的不同获取代次之间不重复,可用安全随机 UUID,或 instanceId + processEpoch + requestId。只用线程 ID 会在重启后复用,只用实例 ID 无法区分同实例先后两次获取。token 应保存在锁句柄内,不让业务方仅凭 key 解锁。
| Value | 能区分每次获取 | 主要问题 |
|---|---|---|
| 固定字符串 | 否 | 任意客户端可删 |
| IP/线程号 | 不可靠 | 复用与并发冲突 |
| 每次随机 token | 是 | 需随句柄保存 |
| 单调 fencing token | 是 | 还需资源侧校验 |
四、为什么 GET 再 DEL 不安全
客户端先 GET 看见自己的 token,随后发生暂停;此时 key 过期,B 创建新锁;A 再执行 DEL 仍会误删。Lua 在 Redis 单线程命令执行模型中让比较与删除不可被其他命令插入。
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
返回 0 表示锁已不属于调用者,不应再尝试无条件删除。
五、续期也要遵循相同原则
看门狗续期不能直接 PEXPIRE key ttl,否则旧客户端可能替新 owner 延长租约。续期脚本同样先比较 token,再原子刷新 TTL。业务结束时先停止本地续期调度,再条件删除,处理好在途续期与释放的竞态。
若续期失败或剩余 TTL 进入危险区,业务线程应停止新的副作用;已经发出的请求仍需资源侧保护。
六、这套方案的安全边界
owner token 只能防错误管理锁记录,不能判断两个 owner 谁更新。A 的锁过期、B 获锁后,A 仍可能向数据库写入;数据库看不到 Redis value。关键写入要携带单调 fencing token,或用数据库版本/CAS、幂等状态机约束。
Redis 主从异步复制还可能在主节点写锁后、复制前宕机,使新主上看不到锁。若业务需要严格互斥,要评估一致性更强的协调服务或让最终资源执行条件校验。
七、常见误区与追问
- 误区:设置 TTL 后直接 DEL 就安全。 旧客户端可能删除过期后新建的锁。
- 误区:GET 相等后再 DEL 没有问题。 两条命令之间存在 ABA 竞态。
- 误区:value 使用固定实例 ID 足够。 同实例不同请求和重启代次无法区分。
- 误区:Lua 解锁能解决所有锁安全问题。 它不能阻止过期 owner 继续写资源。
- 追问:Lua 脚本返回 0 怎么处理? 记录 owner mismatch,并停止依赖该锁的后续副作用。
- 追问:续期为什么也要 Lua? 比较 owner 与延长 TTL 同样必须原子。
- 追问:脚本执行超时怎么办? 先按结果未知处理并读取当前 token 辅助判断;若仍是自己的 token,可重试同一段条件删除脚本,否则视为已经失去所有权,任何情况下都不能无条件
DEL。
八、加强记忆
Redis 锁的最小正确闭环是“一条 SET 获取、每次唯一 token、Lua 比较后删除、续期也校验 owner”。它修复的是锁记录生命周期中的原子性与误删;租约过期后的迟到业务写、Redis 故障转移和业务重复执行,仍要靠 fencing、存储约束与幂等另行解决。