跳转到内容

分布式锁:租约、续期与旧执行者怎样处理

分布式锁分配一段有期限的执行权,不能证明旧进程已经停止。 Redis 原子加锁、持有者校验和 Redisson 自动续期减少常见错误;真正怕并发写坏数据的资源,还要校验业务唯一键、状态版本或 Fencing Token。

不是所有多实例任务都需要锁。单条数据库条件更新、唯一约束或队列串行化能解决时,先比较这些方案。锁服务、资源端和业务幂等要分别设计。

假设锁有效 10 秒,进程 A 停顿后到第 12 秒才继续;B 已在第 10 秒后获得新锁。

动作风险防护
A 恢复后继续写A、B 可能同时操作资源资源拒绝旧执行序号,业务状态条件更新
A 直接 DEL可能删除 B 的锁原子校验持有者,再删除
A 先 GET 再 DEL两步之间可过期、被接管校验和删除放在一个原子操作里

Redis 的常见加锁命令是 SET key token NX PX ...,不是带过期参数的 SETNX。token 对应这次持有者,解锁需校验它。Redis 锁模式

机制能做什么不能保证什么
持有者与原子脚本判断哪个客户端线程持锁,减少误解锁不能让已发出的资源写入自动撤销
Watchdog 续期未指定 leaseTime 时延长锁有效期停顿、网络故障或客户端退出仍可能错过续期
指定 leaseTime到期自动释放业务不会因此自动停止,时长要匹配任务

Redisson 的 watchdog 默认超时为 30 秒,可用 lockWatchdogTimeout 调整;只有锁持有线程能解锁,实际行为按使用版本核对。Redisson Lock 文档

// redisson 与 processOrder 是项目提供的客户端和业务方法
RLock lock = redisson.getLock("my_lock");
lock.lock(); // 不指定 leaseTime,使用 watchdog
try {
processOrder();
} finally {
lock.unlock();
}

生产链路还要限定等待时间并处理抢锁失败,不把无界阻塞当默认。Lua 的“原子执行”表示其他请求不能在脚本中间插入,不表示运行报错会回滚此前写入。Redis Lua 执行语义

故障切换和进程停顿是两类问题

Section titled “故障切换和进程停顿是两类问题”

Redis 异步复制中,锁写入尚未复制到副本时主节点故障,新主节点可能没有这条锁,另一个客户端可再次获得执行权。这是故障切换丢锁窗口,不能只靠 Watchdog 消除。Redis 复制与锁边界

即便锁服务没有丢记录,旧持有者也可能停顿超过租约后恢复。这时要由资源端拒绝过期执行者。Kleppmann:How to do distributed locking

每次领取执行权获得递增序号,写入携带该序号,资源端原子拒绝小于已接受序号的操作。旧 A 带 7,接管后的 B 带 8;资源已接受 8 后,A 的 7 不能覆盖它。

条件为什么
序号在资源范围内单调、不回退普通异步 Redis 的 INCR 也可能在切换时回退,不能直接当可靠 fencing
资源实际校验序号仅在客户端检查锁状态,检查后仍可能失去租约
副作用可去重fencing 防旧写,不自动消除同一操作的重复付款或发信

外部 API 不支持这种校验时,需要业务操作 ID、状态查询或人工核对,不能只靠客户端锁宣称安全。

ZooKeeper 的临时顺序节点可组织锁;会话过期会删除节点,但暂停的客户端仍可能恢复执行,资源保护依然需要。ZooKeeper 锁配方

Redlock 使用多个独立 Redis 实例,也有时钟、暂停和故障假设;Redis 作者的方案与 Kleppmann 的批评需要一起评估,不能只看“用了多数节点”。

要守住的结果验收方式
不误删新锁旧持有者恢复并尝试解锁
不接受旧写租约过期、接管后旧进程继续提交
不重复业务动作相同操作 ID 并发、超时后重试
故障能收敛锁服务切换、失联、续期失败、进程停顿

选择取决于锁失效的后果与资源是否支持校验,不能用“Redis 是 AP、ZooKeeper 是 CP”替代端到端正确性论证。接口幂等设计