分布式锁的四种实现方案深度剖析
一、 原生 Redis (SETNX + Lua) 实现方案
这是最基础的轻量级实现方案,主要依赖 Redis 的单线程执行特性。
1. 加解锁流程
- 加锁: 客户端向 Redis 发送
SET key value NX EX seconds命令。NX保证只有当 Key 不存在时才能设置成功(互斥性)。EX设置物理过期时间(防死锁兜底)。value通常设置为当前客户端的唯一标识(如 UUID + 线程 ID)。
- 解锁: 客户端必须先
GET获取 Key 的值,判断是否与自己的唯一标识一致。如果一致,再执行DEL删除 Key。为了保证 GET 和 DEL 的原子性,这一步必须使用 Lua 脚本。
2. 底层逻辑与源码特征
原生方案的底层数据结构极其简单,就是最基础的 String(SDS) 结构。
解锁的 Lua 脚本源码逻辑:
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
为什么必须用 Lua? 因为单机 Redis 在执行 Lua 脚本时具有排他性。它会将整个脚本作为一个原子操作放入执行队列,期间不会被其他客户端的命令打断,这就避免了“判断通过后、删除执行前”由于线程切换导致的锁状态变化(典型的 CAS 思想在分布式环境的应用)。
3. 优缺点(CAP 视角)
单点 Redis 或主从 Redis 架构本质上是一个 AP(可用性 + 分区容错性) 模型。
- 优点: * 极致性能: 纯内存操作,加上 String 结构的极低开销,并发吞吐量极大。
- 轻量简单: 不需要引入额外的重型客户端依赖。
- 缺点:
- 锁过期与业务执行时间的矛盾: 无法精确预估业务执行时间。如果业务阻塞,锁过期自动释放,其他线程就会乘虚而入,导致并发安全被破坏。
- 不可重入: 基础实现不支持同一个线程多次获取同一把锁,容易引发死锁。
- 主从异步复制丢失锁: 在 Sentinel 或 Cluster 模式下,如果 Master 节点在客户端加锁成功后、但未将该 Key 同步到 Slave 前发生宕机,Slave 升级为新 Master,此时锁数据丢失,其他客户端可以再次加锁成功,违背了互斥性。
4. 使用场景
适用于并发量大、对极小概率的数据不一致有一定容忍度、且业务执行逻辑较短的场景。例如:限制用户某项操作的频率、缓存击穿时的单点重建拦截等。
二、 Redisson 实现分布式锁
Redisson 是目前 Java 生态中工业级分布式锁的标杆。它不仅是一个 Redis 客户端,更是一个驻留在内存中的数据网格。
1. 加解锁流程
Redisson 的 API 设计高度贴合 Java J.U.C 包中的 ReentrantLock。
- 加锁: 客户端调用
lock.tryLock()。Redisson 会向 Redis 发送一段极其复杂的 Lua 脚本,并在内部启动一个 Watchdog(看门狗)后台线程。如果加锁失败,客户端不会一直盲目自旋,而是通过 Pub/Sub 订阅该锁的释放事件。 - 解锁: 客户端调用
lock.unlock()。执行 Lua 脚本将重入次数减 1。如果减到 0,则真正删除该 Key,并通过 Pub/Sub 发布一条消息,唤醒其他阻塞等待的客户端。最后,销毁看门狗任务。
2. 源码实现逻辑深度剖析
A. Hash 结构与可重入性
Redisson 放弃了简单的 String 结构,转而使用 Hash 结构。
KEY(Hash 的表名):分布式锁的名称(如lock:seckill:1001)。FIELD(Hash 的键):客户端 UUID + 线程 ID(精确到具体哪个机器的哪个线程)。VALUE(Hash 的值):重入次数(整数)。
加锁核心 Lua 源码逻辑解析:
-- 1. 如果锁不存在,说明没人占用
if (redis.call('exists', KEYS[1]) == 0) then
-- 使用 hincrby 初始化 field 的 value 为 1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置过期时间(默认 30s)
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 2. 如果锁存在,且 field(UUID:ThreadId)和当前线程一致,说明是重入
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 重入次数 + 1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 重新刷新过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 3. 锁被别人占用,返回剩余生存时间(TTL),用于客户端阻塞等待
return redis.call('pttl', KEYS[1]);
B. Watchdog(看门狗)机制
当未显式指定锁的 leaseTime(租期)时,Redisson 会激活看门狗。
- 底层基于 Netty 的
HashedWheelTimer(时间轮算法)实现定时任务。 - 默认锁超时时间为 30 秒(
lockWatchdogTimeout)。 - 加锁成功后,立即注册一个延时任务,在
30 / 3 = 10秒后执行。该任务会向 Redis 发送 Lua 脚本重新将 TTL 刷新为 30 秒。 - 只要客户端所在的 JVM 没有崩溃,该线程没有执行完业务,锁就会被无限期续命,彻底解决了业务执行时间长于锁过期时间的问题。
C. Pub/Sub 与阻塞唤醒
基于 Redis 的发布订阅(Pub/Sub)和 Java 的 Semaphore(信号量)实现。抢锁失败的线程会订阅特定的 Channel,然后调用 Semaphore.tryAcquire 阻塞。当占有锁的线程释放锁并在 Channel 发布消息时,阻塞的线程被唤醒,再次尝试执行 Lua 脚本抢锁,有效避免了 CPU 的空转消耗。
3. 优缺点(CAP 视角)
Redisson 本质上依然建立在 Redis 的 AP 架构之上。
- 优点: * 工程化程度极高,完美解决了可重入、锁续期、阻塞等待唤醒等复杂协同问题。
- API 极其友好,开发者心智负担小。
- 支持公平锁、读写锁、RedLock 等多种高级锁原语。
- 缺点: * 底层依然无法绝对避免主从切换带来的丢锁问题。虽然官方提供了 RedLock(红锁)算法的实现试图向 CP 靠拢(要求向过半数的独立 Master 节点加锁),但 RedLock 算法本身在学术界(如 Martin Kleppmann 的分析)存在巨大争议,且运维成本极高,工业界极少直接使用。
4. 使用场景
这是高并发互联网系统的绝对主力方案。非常适合 O2O 系统的秒杀防超卖核心校验、分布式定时任务的单机执行防重复、电商交易订单状态的并发状态机流转等高并发且对性能要求严苛的工程场景。
二、 Redisson (基于单节点的完善封装)
Redisson 驻留在内存中的数据网格,是 Java 生态中最完善的 Redis 分布式锁客户端,它解决了原生 Redis 的续期和重入问题。
1. 加解锁流程
-
加锁: 客户端调用
lock.tryLock(),Redisson 向 Redis 发送一段复杂的 Lua 脚本。若失败,客户端不盲目自旋,而是通过 Pub/Sub 订阅该锁的释放事件。成功获锁后,内部启动 Watchdog(看门狗) 线程。 -
解锁: 调用
lock.unlock(),执行 Lua 脚本将重入次数减 1。减到 0 时,删除 Key,并在 Channel 中发布一条消息唤醒阻塞的客户端,同时销毁看门狗。
2. 源码实现逻辑深度剖析
A. Hash 结构与可重入性
Redisson 抛弃了 String,使用 Hash 结构:
-
KEY: 锁名称(lock:order:1001)。 -
FIELD: 客户端标识(UUID:ThreadId)。 -
VALUE: 重入次数。
加锁核心 Lua 逻辑:
Lua
-- 1. 不存在则创建,初始化重入次数为 1,并设置过期时间
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 2. 存在且 Field 一致,说明是重入,次数 +1,重置过期时间
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 3. 被别人占用,返回剩余生存时间(TTL)
return redis.call('pttl', KEYS[1]);
B. Watchdog (看门狗) 机制
底基于 Netty 的 HashedWheelTimer 时间轮实现。默认锁超时 30 秒。加锁成功后,立即注册一个延时任务,在 30÷3=1030 \div 3 = 1030÷3=10 秒后执行。该任务会重新将 Redis 中的锁 TTL 刷新为 30 秒。只要 JVM 没挂且业务没完,锁就无限期续命。
3. 优缺点(CAP 视角:AP 模型)
-
优点: API 极其友好,完美解决重入和锁续期问题;阻塞等待避免 CPU 空转;性能极高。
-
缺点: 依然无法解决 Redis 主从异步复制引发的 “Master 宕机丢锁” 物理漏洞。
三、 Redis RedLock(红锁算法)
为了解决上述“主从切换丢锁”问题,Redis 作者 antirez 亲自设计了 RedLock 算法。它试图用多个完全独立的 Redis 节点来提供一种更接近 CP(强一致性)的保障。
1. 加解锁流程
RedLock 强烈要求舍弃主从架构,部署 NNN 个(通常为 5 个)完全独立的 Redis Master 节点。
-
加锁流程:
-
获取当前系统毫秒时间 T1T_1T1。
-
客户端使用相同的 Key 和随机 Value,顺序向 5 个独立的节点尝试加锁。每次网络请求必须设置极短的超时时间(如 50ms),防止某个节点挂掉导致整个流程长期阻塞。
-
客户端计算获锁总耗时 ΔT=T2−T1\Delta T = T_2 - T_1ΔT=T2−T1。
-
判断成功条件: 必须在多数派节点(M≥N2+1M \ge \frac{N}{2} + 1M≥2N+1,即至少 3 个)上加锁成功,且 ΔT\Delta TΔT 必须小于锁的有效时间。
-
真正有效时间: 原始有效时间减去加锁耗时 ΔT\Delta TΔT。
-
-
解锁流程: 客户端必须向所有 5 个节点发送解锁 Lua 脚本(即便当时在某个节点上加锁失败了也要发,防止发生网络丢包导致的幽灵锁)。
2. 源码实现逻辑
在 Redisson 中,对应的实现是 RedissonRedLock(由于算法本身的争议性,现已被官方标记为 Deprecated,推荐使用更通用的 RedissonMultiLock,但底层多节点协同思想一致)。
-
底层不再有统一的集群路由,而是维护多个独立的 Redis Connection。
-
内部使用
CountDownLatch或异步并发模型向多个节点发射加锁命令。 -
强依赖于本地机器的时钟校准。
3. 优缺点与学术争议(试图突破 CAP,但饱受争议)
-
优点: 彻底拔掉了主从复制带来的隐患,容忍少数节点宕机,可用性极高。
-
缺点(致命争议点):
剑桥大学分布式系统专家 Martin Kleppmann 曾发文猛烈抨击 RedLock,指出了它的理论漏洞:
-
时钟跳跃假设: RedLock 强依赖服务器的物理时钟。如果某台 Redis 发生 NTP 时间同步跳跃(向未来跳跃),它的锁会提前过期,导致另一个客户端加锁成功,违背互斥性。
-
超长 GC 暂停: 如果客户端加锁成功后,发生长达数秒的 Full GC。等 GC 醒来,RedLock 早就过期了,但客户端仍以为自己持有锁,继续执行业务。
-
性能严重倒退: 一次加锁要和 5 个节点发生网络 I/O,完全丧失了 Redis 高性能的初衷。
-
4. 使用场景
非常尴尬的位置。对于绝大多数公司而言:如果可以容忍丢锁,直接用单节点 Redisson;如果绝对不能容忍丢锁,不如直接用 ZooKeeper。RedLock 部署成本高且依然存在理论漏洞,工业界极少直接落地。
四、 ZooKeeper 实现分布式锁
ZooKeeper(ZK)是一个分布式协调服务,其底层基于 Zab(ZooKeeper Atomic Broadcast)共识算法,天生就是一个为了保证强一致性而生的系统。
1. 加解锁流程
- 加锁: 客户端在指定的锁节点(父节点)下,请求创建一个 临时顺序节点 (Ephemeral Sequential Node)。创建后,获取父节点下所有的子节点列表并排序。如果自己创建的节点序号是最小的,则认为加锁成功;否则,找到排在自己前面的那个节点,对它注册一个 Watcher (监听器),然后当前线程进入阻塞。
- 解锁: 业务执行完毕后,客户端显式删除自己创建的临时顺序节点。如果客户端宕机,Session 断开,ZK Server 会自动删除该临时节点。节点删除后,排在后面的那个客户端绑定的 Watcher 被触发,收到通知后再次判断自己是否成为最小节点,若是,则获取锁。
2. 源码实现逻辑(以 Apache Curator 为例)
Curator 封装了原生的 ZK API,其实现核心类为 InterProcessMutex。
- 本地重入计数: Curator 在客户端本地的 JVM 内存中维护了一个
ConcurrentMap<Thread, LockData>。加锁前先查本地 Map,如果有当前线程的记录,直接将重入次数+1即可返回,无需再次发起网络请求去 ZK 交互。 - 单向链式监听(避免羊群效应): 早期的 ZK 锁是所有未获锁的客户端都监听父节点。一旦锁释放,所有客户端被同时唤醒发起请求,导致网络风暴(羊群效应)。Curator 优化了这一逻辑,强行规定:每个节点只监听比自己序号恰好小一位的那个节点。这样每次锁释放,只会有精确的一个后继节点被唤醒,将 O(N)O(N)O(N) 的唤醒代价降到了 O(1)O(1)O(1)。
- 临时节点与 Session: ZK 的 Server 端维护了客户端的 Session 租约。只要客户端活着,就会维持心跳。一旦 JVM 崩溃,心跳丢失超时,ZK 就会主动将该 Session 创建的所有 Ephemeral 节点抹除,这是 ZK 天然防死锁的底层物理机制,无需像 Redis 那样依赖复杂的 Watchdog。
3. 优缺点(CAP 视角)
ZK 集群是一个典型的 CP(一致性 + 分区容错性) 模型。
- 优点:
- 强一致性,绝对安全: 由于 Zab 协议的保证,每次创建节点的写请求都必须经过超半数节点(多数派)刷盘确认后才向客户端返回成功。即使 Leader 节点在返回成功后瞬间宕机,新选出的 Leader 也一定包含了这把锁的数据,绝对不会发生主从切换导致的丢锁。
- 完美的故障恢复机制: 依赖底层的 TCP 长连接与 Session 机制,客户端宕机自动释放锁,时效性与安全性俱佳。
- 缺点:
- 性能瓶颈显著: 所有的写请求(加锁、释放锁)都必须由 Leader 节点串行处理,且需要等待超半数节点网络通信及磁盘 I/O 完成。在高并发洪峰下,TPS 远不如纯内存的 Redis。
- 极端网络抖动导致的“假死”问题: 如果客户端发生了较长时间的 STW(Stop-The-World GC)或者瞬时的网络割裂,导致心跳未能及时送达 ZK Server。ZK Server 认为客户端已死,删除了临时节点(锁被释放)。此时其他客户端成功加锁,而原客户端恢复运转,依然认为自己持有锁,从而导致两方同时操作临界区资源(这类似于 Redis Watchdog 续期失败的极端情况)。
4. 使用场景
不适用于极端的高并发 C 端场景(如大流量秒杀),但非常适用于对数据安全性、一致性要求极其苛刻的后端核心控制链路。例如:B 端金融系统的账户资金清算调度、跨库的分布式事务协调、全局严格单调递增流水号的生成等。
五,总结对比矩阵
| 维度 | 原生 Redis (Lua) | Redisson | ZooKeeper (Curator) |
|---|---|---|---|
| 底层原理 | String + Lua + EX | Hash + Lua + Pub/Sub | 临时顺序节点 + Watcher |
| 一致性模型 | AP (最终一致性) | AP (最终一致性) | CP (强一致性) |
| 锁续期防死锁 | 靠过期时间兜底(易失效) | 后台 Watchdog 定时续期 | TCP Session 机制天然保证 |
| 阻塞与唤醒 | 客户端主动自旋轮询 | 订阅释放 Channel,信号量阻塞 | 监听前置节点,异步事件回调 |
| 性能吞吐量 | 极高 | 非常高 | 中等偏下 |
| 主从切换丢锁风险 | 存在 | 存在(除非硬抗 RedLock) | 绝对不会发生 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)