在分布式系统中,确保数据在并发访问下的一致性是一个永恒的挑战。分布式锁,作为实现互斥(Mutual Exclusion)的关键工具,是每个后端工程师都必须掌握的核心技能。Redis因其高性能和丰富的原子命令,成为了实现分布式锁的首选。然而,一个看似简单的SETNX,在复杂的分布式环境下却隐藏着诸多陷阱。
本文将带你从最基础的Redis锁出发,剖析其在主从架构下的缺陷,深入理解Redis官方曾经推荐的Redlock(红锁) ,并探讨其背后的争议,以及为什么它在今天不再是分布式锁的首选方案。
为何基于主从复制的锁并不可靠?
实现Redis分布式锁最直观的方式,是在实例中创建一个唯一的键(Key):
SET my:lock random_value NX PX 30000-
NX:确保只有当键不存在时才能设置成功,保证了锁的原子性获取。 -
PX 30000:为锁设置一个过期时间(例如30秒),防止客户端因崩溃而无法释放锁,导致死锁。 -
random_value:一个唯一的客户端标识,用于在释放锁时验证身份,防止误删他人的锁。
这套方案在单机Redis上运行良好。但为了保证高可用性,我们通常会引入主从复制(Master-Slave)架构。问题也随之而来:这套方案在主从故障转移(Failover)面前,不堪一击。
致命的竞争条件
由于Redis的主从复制是异步的,数据从Master同步到Slave存在延迟。这就为数据不一致创造了条件,从而可能破坏锁的互斥性。
考虑以下场景:
- 客户端A 在Master节点上成功获取了锁
my:lock。 - 在Master将这个新的键值对同步到Slave之前,Master节点突然崩溃宕机。
- 哨兵(Sentinel)或集群机制触发故障转移,将一个Slave节点提升为新的Master。
- 客户端B 尝试在新Master上获取锁
my:lock。由于数据同步延迟,新Master上根本不存在这个键。 - 客户端B成功获取了锁。

结果是灾难性的:客户端A和客户端B同时持有了本应是互斥的锁。 这种安全违规会导致数据错乱、重复执行等严重问题。我们对锁最基本的要求——在任何时刻只允许一个持有者——被轻易地打破了。
Redlock算法:为分布式锁建立多数派共识
为了解决主从异步复制带来的锁失效问题,Redis的作者Antirez提出了一个更复杂的分布式锁算法——Redlock。
Redlock的核心思想是放弃主从复制,转而采用N个完全独立的Redis Master节点。这些节点之间不进行任何数据同步。客户端必须从大多数节点上成功获取锁,才能被认为真正持有了该锁。
Redlock加锁流程

假设我们有N=5个独立的Redis Master实例。客户端获取锁的操作如下:
记录开始时间:客户端以毫秒为单位,获取当前时间戳
t1。顺序尝试获取:客户端使用相同的键名(e.g.,
my:lock)和唯一的随机值,依次向所有N个Redis实例发送加锁请求。- 设置短暂超时:在向每个实例请求时,客户端需要设置一个远小于锁总有效期的网络超时(例如,锁有效期10秒,超时设为50毫秒)。这可以防止客户端因等待一个宕机的节点而长时间阻塞。
计算结果并校验:当客户端从所有N个实例都请求完毕后,记录当前时间戳
t2。此时,客户端必须同时满足以下两个条件,才能认为加锁成功:- 条件1:获取多数派支持:客户端成功从超过半数(
N/2 + 1,在此例中是5/2 + 1 = 3个)的实例上获取了锁。 - 条件2:耗时小于有效期:获取锁的总耗时
t2 - t1必须小于锁的初始有效时间。
- 条件1:获取多数派支持:客户端成功从超过半数(
重算锁的有效时间:如果加锁成功,锁的真实有效时间应为初始设置的有效时间减去加锁过程的耗时。例如,初始设置30秒,加锁花了2秒,那么锁的剩余生命周期只有28秒。
失败则全部释放:如果客户端未能满足上述任一条件(例如,只从2个实例获取到锁,或加锁耗时过长),则必须向所有N个Redis实例(包括那些加锁失败的)发送解锁命令,以清理残留的锁。
为什么是奇数个节点?

Redlock依赖于“多数派”共识。N = 2X + 1 是一个经典的容错公式,其中 X 是系统能够容忍的失效节点数。
- 若部署
N=3台 (2*1 + 1),系统能容忍X=1台节点宕机。剩下的2台仍能构成多数派 (3/2 + 1 = 2)。 - 若部署
N=5台 (2*2 + 1),系统能容忍X=2台节点宕机。剩下的3台仍能构成多数派 (5/2 + 1 = 3)。
使用奇数个节点可以在满足同等级别容错能力的情况下,节约资源。例如,3台和4台都只能容忍1台节点失效,但3台成本更低。
Redisson实现:看门狗与锁续期
理论虽好,但在实际应用中,还需要一个可靠的客户端库来封装这些复杂的逻辑。Redisson 就是一个实现了 Redlock 算法的优秀 Java Redis 客户端。
当你配置 Redisson 使用多个 Redis 节点时,它内部会构造一个 RedissonRedLock 对象,该对象聚合了多个普通的 RLock 对象,每个 RLock 对应一个独立的 Redis Master 实例。当你调用 RedissonRedLock 的 lock() 方法时,它会严格按照上述 Redlock 算法的流程,向多个实例广播加锁命令,并校验结果。
除了忠实实现 Redlock 算法,Redisson 还解决了另一个棘手的问题:如果锁的过期时间到了,但业务逻辑还没执行完,怎么办? 这就是它著名的 “看门狗”(Watchdog)机制。
看门狗 (Watchdog) 机制
当你使用Redisson的lock()方法(不带租约时间参数)获取锁时,如果加锁成功,Redisson会启动一个后台线程,即“看门狗”。
- 默认锁期:Redisson中的锁,默认过期时间是30秒。
- 定时续命:看门狗是一个定时任务,它会每隔10秒(即默认锁过期时间的1/3)检查一次持有锁的客户端线程是否依然存在。
- 自动续期:如果线程还活着(即业务还在执行),看门狗就会将该锁的过期时间重置为30秒。



这个机制确保了只要业务逻辑没有执行完毕,锁就不会因为超时而提前释放,极大地增强了锁的可靠性。当然,当业务执行完毕,客户端调用unlock()方法时,这个续期定时器会被取消。
加锁与解锁
Redisson的锁操作是通过执行Lua脚本来保证原子性的。
加锁(可重入) :


- 使用
HEXISTS判断锁是否存在。如果不存在,则通过HSET存入当前线程ID和计数1,并设置过期时间。 - 如果锁已存在,且
HGET出的线程ID是当前线程ID,则证明是锁重入。只需将计数加1即可。 - 否则,返回锁的剩余过期时间,加锁失败。
- 使用
解锁:
- 检查锁的持有者是否为当前线程。如果不是,则抛出异常。
- 如果是,则将重入计数减1。
- 如果计数仍大于0,则仅更新哈希值;如果计数变为0,则通过
DEL命令彻底删除该键,释放锁。

Redlock 的争议:为何它不再是首选方案?
尽管 Redlock 的设计初衷是为了解决主从锁的缺陷,并且 Redisson 提供了强大的实现,但该算法自提出以来就受到了分布式系统专家的激烈批评,其中最著名的是 Martin Kleppmann 的文章《How to do distributed locking》。
如今,社区的普遍共识是:Redlock 并非一个足够安全的分布式锁算法,不应在对安全性有严格要求的场景下使用。
其核心缺陷在于它对系统时间和网络延迟做了过于乐观的假设。
致命缺陷:对时钟的依赖
Redlock 的安全性依赖于一个关键假设:系统中没有会大幅影响锁有效期的意外延迟。然而,在真实的分布式环境中,这种延迟无处不在,例如:
- 长时间的 GC 停顿(Stop-The-World GC) :客户端A获取了锁,然后其进程发生了一次长时间的Full GC,停顿时间可能长达数秒甚至数十秒。
- 网络延迟或虚拟机迁移等。
一个典型的失效场景:
- 客户端A 成功从5个节点中的3个获取了锁,锁有效期为10秒。
- 客户端A的进程发生了长时间的GC停顿,持续了12秒。
- 在这12秒内,它在3个Redis节点上持有的锁已经因超时而自动释放。
- 客户端B 此时发起请求,成功从另外3个节点(或包括之前A持有的节点)获取了锁。
- 客户端A的GC结束,恢复执行,它仍然认为自己持有锁,并开始操作共享资源。
结果再次是灾难性的:客户端A和客户端B同时持有了锁,打破了互斥性。Redisson的看门狗机制虽然能缓解此问题,但它本身也是一个定时任务,同样会受GC等停顿的影响,无法从根本上解决这个问题。
Martin Kleppmann 指出,一个安全的分布式锁,不应该依赖于对时间的假设。它应该基于一个能提供严格一致性保证的系统。
结论与现代方案
分布式锁的实现远非一个简单的SETNX命令。从单机锁到不可靠的主从锁,再到基于多数派共识的 Redlock 算法,我们看到的是为了在不可靠的网络和硬件之上构建可靠系统所付出的巨大努力。
Redlock 是解决主从异步复制问题的一次重要尝试,它通过牺牲性能和增加架构复杂性,试图换取更高的容错性。然而,由于其对系统时钟和进程停顿的脆弱性,它无法提供严格的安全性保证。
因此,在构建现代分布式应用时,我们的选择应该是:
- 首选方案:使用强一致性系统
对于需要严格保证互斥性的关键业务(如订单、库存、支付),应使用基于 ZooKeeper 或 etcd 实现的分布式锁。这类系统基于 Raft/Paxos 等共识算法,提供了严格的线性一致性,不会出现 Redlock 的时钟问题。 - 次选方案:接受风险,简化架构
如果业务能容忍极小概率的锁失效,且不希望引入 ZK/etcd 等重型组件,那么一个配置了持久化和哨兵(Sentinel) 的单机版 Redis 锁,在大多数情况下已经足够。此时,我们必须清楚地认识到它的局限性(主从切换瞬间可能失效),并在业务设计上做好兜底或容错。