分布式锁RedLock争论

前言

RedLock 争议是 Redis 作者 Salvatore Sanfilippo(Antirez)与分布式系统研究者 Martin Kleppmann 围绕分布式锁安全性所展开的一场经典技术论战。

这场争论的核心并非仅针对 RedLock 算法本身,而是进一步延伸至一个更本质的问题:

在真实分布式环境中,是否能够仅依赖基于时间的租约(Lease)机制构建同时具备安全性(Safety)、活性(Liveness)以及高性能(Performance)的分布式锁服务。

从分布式理论视角来看,该争议实际上反映了工程实践与形式化模型之间对于系统假设(System Assumptions)和故障模型(Failure Model)的不同理解。


RedLock 方案概述

Redis 单实例锁通常采用如下实现:

1
SET resource_name value NX PX 30000

其中:

  • NX 保证锁获取操作的原子性(Atomic Acquisition);
  • PX 指定锁租约(Lease)的有效期;
  • value 为客户端生成的唯一随机标识,用于保证解锁操作的正确性。

然而,在主从复制架构下,该方案存在明显的安全隐患。

假设客户端已成功获取锁,但锁信息尚未完成主从复制,此时主节点发生故障并触发故障转移(Failover),新的主节点可能并未持有对应锁状态。

此时多个客户端可能同时认为自己成功获得锁,从而破坏互斥性(Mutual Exclusion)。

为解决上述问题,Antirez 提出了 RedLock 算法。

RedLock 的核心思想可以概括为:

  1. 客户端同时向多个相互独立的 Redis 实例发起加锁请求;
  2. 仅当成功获取多数派(Majority Quorum)节点的锁时,才认为加锁成功;
  3. 锁以租约(Lease)的形式存在,并在 TTL 到期后自动失效;
  4. 解锁时仅删除属于当前客户端的锁记录。

需要特别强调的是:

虽然 RedLock 同样采用了多数派原则(Majority Principle),但其并不具备共识协议(Consensus Protocol)的核心机制,包括:

  • Leader Election
  • Term / Epoch
  • Log Replication
  • Majority Commit
  • State Machine Replication

因此,RedLock 本质上并非共识算法,而是一种基于多数派租约(Majority Lease)的分布式锁实现。

安全性分析(Safety Analysis)

核心假设

RedLock 的安全性证明建立在以下两个关键前提之上。

假设一:有界网络延迟(Bounded Network Delay)

客户端与 Redis 实例之间的通信延迟存在可接受上界:

$$
T_2 - T_1 < \infty
$$

其中:

  • $T_1$ 表示开始获取锁的时刻;
  • $T_2$ 表示完成多数派加锁的时刻。

假设二:有界时钟漂移(Bounded Clock Drift)

各 Redis 实例之间虽然不存在严格意义上的全局同步时钟,但其时钟误差被认为处于可接受范围内:

$$
CLOCK_DRIFT \ll TTL
$$

在上述假设成立的条件下,RedLock 试图证明:

在锁租约有效期间内,不会存在两个客户端同时获得同一资源访问权的情况。

这对应于分布式锁所要求的互斥性(Mutual Exclusion)属性。


最小有效时间(MIN_VALIDITY)

RedLock 使用如下公式定义锁的理论安全窗口:

$$
MIN_VALIDITY=TTL-(T_2-T_1)-CLOCK_DRIFT
$$

其中:

  • $TTL$ 为锁租约时长;
  • $(T_2-T_1)$ 为获取多数派锁所消耗的时间;
  • $CLOCK_DRIFT$ 为时钟误差补偿项。

该公式所刻画的是:

多数派节点上的锁记录能够同时存在的最短时间区间。

若满足:

$$
MIN_VALIDITY > 0
$$

则在该时间窗口内,多数派锁记录必然同时存在。

由于新的客户端无法在多数派节点上再次成功执行 SET NX 操作,因此互斥性得以保证。


互斥性证明

互斥性的成立依赖于如下事实:

若某客户端已成功在多数派节点上建立锁记录,则在锁记录失效之前,任何其他客户端都无法再次获得新的多数派。

原因在于:

多数派集合之间必然存在交集(Quorum Intersection)。

只要交集节点上的锁记录仍然存在,则后续客户端无法在该节点完成加锁操作,从而无法形成新的多数派。

因此,在 MIN_VALIDITY 时间窗口内,系统满足:

$$
\forall t \in MIN_VALIDITY
\quad
|\text{Lock Owners}| \le 1
$$

即任意时刻最多只有一个客户端能够持有锁。


无效租约的处理

若客户端完成加锁所消耗的时间已经接近甚至超过 TTL:

$$
(T_2-T_1)\ge TTL
$$

则:

$$
MIN_VALIDITY \le 0
$$

此时虽然客户端可能已经获得多数派确认,但锁的有效窗口已经不存在。

因此 RedLock 要求客户端主动释放所有已获取的锁,并将本次加锁视为失败。

该机制用于避免客户端持有逻辑上已经失效的租约继续访问共享资源。


活性(Liveness)证明

原文:

1
2
3
4
5
6
7
8
9
The system liveness is based on three main features:

The auto release of the lock (since keys expire): eventually keys are available again to be locked.
The fact that clients, usually, will cooperate removing the locks when the lock was not acquired, or when the lock was acquired and the work terminated, making it likely that we don’t have to wait for keys to expire to re-acquire the lock.
The fact that when a client needs to retry a lock, it waits a time which is comparably greater than the time needed to acquire the majority of locks, in order to probabilistically make split brain conditions during resource contention unlikely.
However, we pay an availability penalty equal to TTL time on network partitions, so if there are continuous partitions, we can pay this penalty indefinitely. This happens every time a client acquires a lock and gets partitioned away before being able to remove the lock.

Basically if there are infinite continuous network partitions, the system may become not available for an infinite amount of time.

RedLock采用下面三种方法保证活性:

  1. 锁的自动释放:这是最根本的保障。所有锁 Key 都设置了 TTL,即使客户端彻底崩溃,锁也会在 TTL 后自动删除,系统总能恢复。

  2. 主动清理:客户端在加锁失败或完成任务后,会主动发起解锁操作(DEL 命令),这会加速锁的释放,不必每次都等待 TTL 到期。

  3. 重试退避:当客户端因冲突而重试加锁时,它会等待一个比通常加锁耗时更长的时间(例如几倍于网络往返时间)。这大大降低了多个客户端在发生网络分区(Split-Brain)时,仍然因同时重试而反复冲突的概率。

不过,文档也坦诚指出了代价:在网络分区期间,系统可用性会受到 TTL 时间的惩罚。 如果客户端在分区期间持有锁并与主网络隔离,无法主动释放,那么所有其他客户端都必须等待 TTL 到期才能再次获取锁。如果分区持续不断,这种不可用状态可能会无限延续。

性能、崩溃恢复与持久化

原文

1
2
3
4
5
6
7
8
9
10
11
12
13
Many users using Redis as a lock server need high performance in terms of both latency to acquire and release a lock, and number of acquire / release operations that it is possible to perform per second. In order to meet this requirement, the strategy to talk with the N Redis servers to reduce latency is definitely multiplexing (putting the socket in non-blocking mode, send all the commands, and read all the commands later, assuming that the RTT between the client and each instance is similar).

However there is another consideration around persistence if we want to target a crash-recovery system model.

Basically to see the problem here, let’s assume we configure Redis without persistence at all. A client acquires the lock in 3 of 5 instances. One of the instances where the client was able to acquire the lock is restarted, at this point there are again 3 instances that we can lock for the same resource, and another client can lock it again, violating the safety property of exclusivity of lock.

If we enable AOF persistence, things will improve quite a bit. For example we can upgrade a server by sending it a SHUTDOWN command and restarting it. Because Redis expires are semantically implemented so that time still elapses when the server is off, all our requirements are fine. However everything is fine as long as it is a clean shutdown. What about a power outage? If Redis is configured, as by default, to fsync on disk every second, it is possible that after a restart our key is missing. In theory, if we want to guarantee the lock safety in the face of any kind of instance restart, we need to enable in the persistence settings. This will affect performance due to the additional sync overhead.fsync=always

However things are better than they look like at a first glance. Basically, the algorithm safety is retained as long as when an instance restarts after a crash, it no longer participates to any currently active lock. This means that the set of currently active locks when the instance restarts were all obtained by locking instances other than the one which is rejoining the system.

To guarantee this we just need to make an instance, after a crash, unavailable for at least a bit more than the max TTL we use. This is the time needed for all the keys about the locks that existed when the instance crashed to become invalid and be automatically released.

Using delayed restarts it is basically possible to achieve safety even without any kind of Redis persistence available, however note that this may translate into an availability penalty. For example if a majority of instances crash, the system will become globally unavailable for TTL (here globally means that no resource at all will be lockable during this time).
  1. 性能优化策略
    为了达到高吞吐和低延迟,客户端需要使用多路复用(Multiplexing)。具体来说,是以非阻塞模式,同时向所有 Redis 节点发送加锁命令,然后一次性读取所有回复,而不是串行地一个接一个请求。

  2. 崩溃恢复与数据持久化
    这是一个关键的安全隐患。假设客户端 A 在 5 个节点中的 3 个上成功加锁。随后,其中一个节点发生故障并重启。

无持久化场景:重启节点丢失所有数据。客户端 B 随后可在该节点及其他 2 个节点上加锁成功,导致两个客户端同时持有锁。

为规避此风险,有两种权衡方案:

方案 机制 优点 劣点
强持久化 每次写入使用 fsync=always 节点重启后数据不丢失 性能大幅下降
延迟重启 节点离线后延迟重启,等待时间 $> \max(TTL)$ 高性能,自动恢复 节点长时间不可用

延迟重启的原理:节点离线时间超过所有旧锁的 TTL 后,重启时是”白纸”状态,不会包含有效锁信息,从而无法被新客户端利用破坏互斥性。

锁续期

文档提到了一个锁续期(Lock Extension) 机制。如果业务执行时间较长,客户端可以在锁快要过期时,通过一个 Lua 脚本检查锁的 Key 是否存在且 Value 是自己设置的随机值,如果是,则延长该 Key 的 TTL。这个机制必须同样向多数派节点发起,且必须在有效时间内完成。不过文档警告:必须限制续期次数,否则如果客户端永不释放锁,就会违背活性(Liveness)原则。


Kleppmann 的质疑:RedLock 是否安全?

Martin Kleppmann 在《How to do distributed locking》一文中指出,RedLock 依赖于一组非常强的系统假设,因此其安全性在实际分布式环境中存在缺陷。主要争议点包括:

1. 客户端暂停(Stop-the-World)与锁过期

RedLock 的实现依赖于客户端能够在锁的 TTL 内完成对资源的访问并释放锁。如果客户端在持锁期间遭遇长时间 GC 暂停或线程阻塞,并且该暂停时间超过了锁的 TTL,那么:

  • 服务器端可能已经将该锁视为过期并允许其他客户端获取;
  • 原客户端随后继续执行并访问资源,导致两个客户端同时持有该资源的访问权限。

这一问题体现的是:RedLock 使用基于时间的租约,而时间租约在完全异步环境下并不可靠。

2. 缺少 fencing token(栅栏令牌)

安全的分布式锁通常要求资源访问端具备“栅栏令牌”机制,即:

  • 每次成功获取锁时,客户端获得一个递增的令牌;
  • 资源管理方在处理写操作时检查令牌有效性;
  • 若客户端持有的令牌较旧,则拒绝其写操作。

如果仅依赖锁的 TTL,而没有 fencing token,即使客户端在锁过期后仍继续执行,它也无法被资源端可靠地拒绝,这会带来“过期锁仍可写”的危险。

3. 时间假设与异步系统的限制

Kleppmann 认为 RedLock 的安全性隐含如下假设:

  • 时钟漂移受限;
  • 网络延迟受限;
  • 客户端停顿时间不会无限延长。

然而,在真实系统中,时钟误差、网络波动、GC 暂停等因素都可能变得非常大,尤其是在云环境、虚拟化或容器平台上,这些因素不能简单地忽略。

因此,任何依赖严格时间界限的安全性证明都应保持谨慎。

这与 FLP 不可能性定理的精神类似:在完全异步模型下,不能通过时间来判断节点是否失败。


Antirez 的回应:RedLock 的假设与定位

Antirez 在《Is RedLock Safe》一文中反驳了 Kleppmann 的观点,强调 RedLock 的适用前提与设计目标。

1. 锁的必要性与资源约束

Antirez 指出,如果底层资源本身能够拒绝旧写入,那么强化锁本身的意义会减弱。也就是说,若资源层能够基于版本号或 idempotency 直接保证安全,那么锁服务可以退化为性能优化工具。

但在很多场景中,分布式锁仍然是必要的,因为它为不支持幂等写入或乐观并发控制的资源提供了一种统一的互斥语义。

2. 半同步系统假设(Semi-Synchronous System)

Antirez 认为 RedLock 并非在“完全异步”模型下设计,而是在一个“半同步”模型下成立:

  • 时钟存在漂移,但漂移在可接受范围内;
  • 网络延迟存在,但不会无限大;
  • 锁的 TTL 设置足够大,以覆盖常见延迟与抖动。

在这种半同步假设下,RedLock 可以被视为一种基于租约的分布式锁,其安全性依赖于这些延迟边界。

3. Redis 的定位

Antirez 还强调,Redis 本身定位为高性能 KV 存储,无法、也不应承担像 Raft、Paxos、etcd 这样的重型共识协议。

RedLock 的设计目标是为 Redis 提供一种轻量级分布式锁机制,而不是构造一个通用、强一致性的分布式协调系统。


争议的核心

RedLock 争议的核心并不是“RedLock 是否工作”,而是“RedLock 的安全性是否在现实条件下成立”。

  • 如果将 RedLock 视为一个在受限假设下工作的租约锁,它可以提供一定程度的可用性和互斥性;
  • 但如果强求它在完全异步、无界延迟、无界暂停的系统中也具备强一致性,那么它就不能满足这种要求。

从本质上讲,要构建一个真正的强一致性分布式锁,需要解决分布式共识问题;而解决共识问题通常会引入 Raft/Paxos、领导者选举、日志复制、任期和多数派提交等机制。

因此,RedLock 更像是一种“工程折衷”,而不是一个形式上完备的共识式锁服务。


应用场景与风险权衡

效率优先的锁

当业务能容忍偶尔的重复执行,且操作本身幂等或后果有限时,RedLock 这类基于租约的锁具有较高价值:

  • 定时任务调度;
  • 防止重复发送通知;
  • 避免重复缓存重建或重复计算。

在这些场景下,锁失效最多会导致“最多多执行一次”,且只要上层业务设计具备幂等性,风险可控。

正确性优先的锁

对于需要严格互斥的业务场景,RedLock 的时间假设风险可能导致严重后果:

  • 支付扣款;
  • 实时库存扣减;
  • 订单状态变更;
  • 分布式事务协调。

如果锁失效或客户端执行超过租约时间,可能引发数据不一致、资金错误或状态混乱。


结论

RedLock 的争议揭示了一个重要原则:

  • 任何基于时间租约的分布式锁都需要明确其系统假设与故障模型;
  • 在半同步系统中,RedLock 可被视为一种可行的工程方案;
  • 但在完全异步、无界暂停的系统语境下,它不能被视为强一致性锁。

最终,应根据业务需求在“效率优先”和“正确性优先”之间做出权衡,并选择适合的分布式锁或协调机制。

与其他分布式锁机制的对比

方案 实现基础 强一致性 性能 工程成本 适用场景
RedLock 多数派租约 否(半同步) 高性能、容忍偶发冲突
ZooKeeper Zab 共识 强一致性、配置中心
etcd Raft 共识 Kubernetes、服务发现
Chubby Paxos 共识 Google 内部、极高可靠性

参考资料



分布式锁RedLock争论
https://yicizhang00.github.io/posts/分布式/分布式锁/分布式锁争论/
作者
Yici Zhang
发布于
2026年6月17日
许可协议