分布式锁是一种跨进程的互斥同步机制,核心目标是在分布式环境下保障同一时刻仅有一个节点可以访问共享资源。

本文先梳理分布式锁的通用设计准则和三种主流基础设施方案,再以 Redisson 为主线,逐层拆解其 Lua 原子化、可重入、看门狗续期、发布订阅唤醒和 RedLock 的实现逻辑,最后给出跨方案的选型框架。

1 分布式锁的设计准则

在单机场景下,多线程资源访问可以通过 synchronizedReentrantLock 解决。但在分布式架构中,不同进程、不同主机上的线程无法通过单机锁实现互斥,必须由分布式锁充当跨主机同步协调器。

一个生产级可用的分布式锁方案需要满足以下八项准则,这也是区分“基础可用”和“生产级可靠”的评判维度。

1. 互斥性

同一时刻,无论有多少个客户端实例请求锁,最终只能有一个客户端持有锁。这是分布式锁最核心的语义——没有互斥,后续所有机制都失去根基。

2. 防死锁

即使持有锁的客户端因进程崩溃、网络分区或主机宕机等异常无法主动释放,锁也必须能被回收,不能永久占用。防死锁的典型手段是锁超时自动过期——客户端加锁时必须设定合理的存活时间,作为异常场景的终极兜底。

3. 身份唯一性(谁加锁谁解锁)

锁只能由加锁时的同一客户端释放,禁止其他客户端误释放。实现上需要由加锁方生成全局唯一的锁持有者标识,解锁时先校验标识合法性,校验通过才执行释放。

4. 可重入性

同一线程在同一把锁上的多次加锁操作不应被自己阻塞。对应的,释放锁也需要执行相同次数的解锁,只有当重入计数归零后锁才被真正释放。这个语义适配的是业务代码中同步逻辑嵌套的场景,避免线程因自身持有锁而陷入死锁。

5. 高可用

锁服务本身必须具备容错能力。当锁服务的部分节点故障或出现网络分区时,只要多数节点仍在正常运行,锁服务就应该继续提供加锁/解锁服务,不存在单点故障。

6. 非阻塞语义

获取锁失败时应有明确的返回结果,客户端不会无限阻塞。这让业务层有机会实现重试、降级或其他异常处理逻辑,而不是挂死等待。

7. 原子性校验

所有涉及“锁状态校验 + 修改”的混合操作——例如校验锁是否存在、校验持有者身份、更新重入计数或过期时间——都必须是原子执行的。如果这些组合操作存在中间状态,并发请求就可能突破互斥约束。

8. 高性能

锁的获取和释放必须控制在业务可接受的延迟范围内,且能支撑预期的峰值并发量。锁服务的性能瓶颈会直接传导到业务层——如果每次加锁耗时几十毫秒,一个百 QPS 的接口就可能被锁拖垮。

2 三类主流实现方案

目前业界主流的分布式锁方案,技术底层均依赖三类开源分布式组件的核心能力。三者在一致性模型、性能特征和运维复杂度上存在显著差异。

2.1 基于 Redis

Redis 是高性能内存存储,天然支持 SETNXEX 等原子命令。单实例纯内存操作即可支撑 10 万+ QPS,是高并发场景下分布式锁的首选。

但 Redis 的主从复制是异步的,这会带来一致性风险:在主从切换时,新主节点可能未同步完旧主节点的锁写入数据,导致锁“丢失”——即两个客户端同时认为自己持有锁。

这是 Redis 作为 AP 系统在锁场景下的天然短板,Redisson 的 RedLock(见 3.6 节)正是针对这一风险的工程补救。

2.2 基于 ZooKeeper

ZooKeeper 基于 ZAB 一致性协议实现强一致性。其锁方案依赖两个核心能力:

  • 临时顺序节点:客户端在锁节点下创建临时顺序子节点,序号最小者获得锁。
  • Watcher 事件监听:未获得锁的客户端监听前一个序号节点的删除事件,前一个节点释放后自己被唤醒。

ZooKeeper 方案的优势在于强一致性和天然的公平性(按到达顺序排队),代价是性能开销较大、运维成本较高。每个锁操作都需要一次 ZAB 协议的写入,延迟远高于 Redis。

2.3 基于 etcd

etcd 依托 Raft 一致性算法实现强一致性,同时支持 Lease 租约和 CAS 原子操作两种核心原语。

从定位上看,etcd 介于 Redis 和 ZooKeeper 之间:它的强一致性和稳定性优于 Redis,运维复杂度低于 ZooKeeper,是云原生场景下的理想锁方案。但相比 Redis,etcd 的锁操作延迟更高,不适合极端高并发的场景。

此外,还有基于数据库唯一索引或行级锁的实现方案。这类方案的优点是实现简单、不额外引入中间件,但性能差且有死锁风险,通常仅适用于并发量低、无高可用要求的非核心业务。

3 Redisson 分布式锁

Redisson 是对 Redis 原生锁的一次高封装度工程化改进。它将使用 Redis 原生命令构建分布式锁时需要手动处理的“可重入性、自动续期、锁误释放、阻塞等待、集群高可用”等技术痛点全部在框架底层解决,让开发者可以用接近 ReentrantLock 的 API 使用生产级分布式锁。

3.1 五层核心优化

Redisson 并非重复造轮子,而是在 Redis 原生能力之上做了五层针对性优化:

原子性优化:将锁获取、锁释放的多条件多命令操作全部封装到一段 Lua 脚本中执行。Redis 的 Lua 脚本执行是原子性的——脚本执行期间不会被其他客户端的命令插入中断。这从底层保障了“锁状态校验 + 修改”整个混合操作的原子性,消除了多命令组合的并发安全风险。

可重入性优化:用 Redis 的 Hash 数据结构替代 String 来存储锁信息。Hash 的 key 是锁的唯一标识,field 是“客户端唯一标识 + 线程 ID”的组合,value 是重入次数计数器。这个设计天然支持可重入逻辑——同一线程多次加锁只需递增计数器,解锁时递减,归零后才真正释放。

阻塞等待优化:放弃客户端轮询重试,改用 Redis 的发布订阅机制。客户端获取锁失败后订阅该锁的释放事件而不是立即重试;锁被释放时,Redisson 通过 Pub/Sub 向所有等待客户端发送通知;收到通知后再重新发起获取锁的请求。这个设计将无效自旋产生的网络开销降低了一个数量级。

自动续期优化:引入看门狗机制,解决“业务执行时间超过锁默认过期时间导致锁提前释放”这一核心痛点。详细逻辑见 3.5 节。

集群高可用优化:针对 Redis Cluster 场景做了两层适配——客户端根据锁 key 计算哈希槽,将锁操作直接路由到对应主节点,避免跨节点通信;同时缓存 Lua 脚本的 SHA-1 摘要,通过 EVALSHA 命令执行,减少网络传输开销。

3.2 加锁流程拆解

Redisson 加锁流程的核心,是原子化地校验锁存在性和持有者身份,再根据校验结果执行加锁或重入。整个流程从客户端 API 调用到 Redis 节点命令执行形成闭环,可以拆解为以下六个步骤:

  1. 路由定位:如果是 Redis Cluster 场景,客户端先根据锁 key 计算哈希槽,将请求直接路由到对应主节点;单节点场景跳过此步。
  2. 发送 Lua 脚本:客户端向 Redis 节点发送加锁 Lua 脚本。
  3. 分支判断:Redis 执行脚本,根据 key 的存在状态走三个分支:
    • 锁不存在:创建 Hash 记录,field 设为当前客户端标识,value 设为 1,同时设置默认过期时间(30 秒),返回加锁成功。
    • 锁存在且持有者是当前客户端:field 对应的 value 原子递增 1(重入),刷新过期时间为 30 秒,返回加锁成功。
    • 锁存在且持有者是其他客户端:返回锁的剩余存活时间(PTTL),客户端据此计算重试间隔。
  4. 退避重试:客户端收到剩余存活时间后,以此作为 sleep 时长,等待结束后重新发起加锁请求。
  5. 发布订阅阻塞:重试指定次数后仍未获取到锁,客户端订阅该锁的释放事件并进入阻塞状态,直到收到释放通知。
  6. 启动看门狗:加锁成功后,立即启动一个后台续期定时任务,定期刷新锁的过期时间。

加锁 Lua 脚本的核心逻辑:

-- KEYS[1]: 锁的名称
-- ARGV[1]: 锁的过期时间(毫秒)
-- ARGV[2]: 客户端唯一标识+线程ID(锁持有者标识)

if ((redis.call('exists', KEYS[1]) == 0)
    or (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;
return redis.call('pttl', KEYS[1]);

为提升执行效率,Redisson 还引入了一层脚本摘要缓存优化:客户端预计算并缓存内置 Lua 脚本的 SHA-1 摘要,首次执行时发送完整脚本,后续优先发送摘要(EVALSHA),Redis 根据摘要直接调用已缓存的脚本。若缓存未命中,客户端会自动退回发送完整脚本。

3.3 可重入实现原理

可重入性是 Redisson 支撑复杂业务同步逻辑的关键。它的实现依赖两个基础构件:Hash 数据结构Lua 脚本的原子性

Redis 中存储的锁信息结构如下:

维度内容含义
Keyredisson_lock:stock:1001锁的唯一标识
FieldUUID:threadId锁持有者的唯一标识
Value3当前线程的重入次数

三种场景下的处理逻辑:

  • 初次加锁:锁 key 不存在,创建 Hash 记录,field 设为当前客户端标识,value 设为 1,设置过期时间。
  • 重入加锁:锁 key 存在且 field 匹配当前客户端,value 原子递增 1,刷新过期时间。
  • 解锁:field 对应的 value 原子递减 1。递减后若 value > 0,说明线程仍有未执行的同步逻辑,锁不释放,仅刷新过期时间;若 value == 0,删除整个锁的 Hash key,真正释放锁。

选择 Hash 而非 String 的理由:String 只能存储单一的持有者标识,无法记录重入次数;Hash 天然支持对不同 field 的原子增减操作,能同时覆盖“识别持有者”和“记录重入次数”两个需求。更关键的是,Hash 的增删改逻辑都可以封装进 Lua 脚本原子执行,满足“校验 + 增减”混合操作的原子性要求。

3.4 释放锁与唤醒机制

释放锁是整个锁生命周期的收尾环节。操作目标是“安全、可靠地将锁状态重置为可获取”,同时杜绝锁被其他客户端误释放。Redisson 的释放流程同样通过 Lua 脚本保证整个操作的原子性:

  1. 校验持有者身份:脚本先检查锁的 Hash 结构中是否存在与当前客户端标识匹配的 field。若不存在,说明当前客户端不是合法持有者,直接返回失败,不执行后续操作。
  2. 递减重入计数:校验通过后,将对应 field 的 value 原子递减 1。
  3. 判断是否真正释放
    • 若递减后 value = 0:删除整个锁的 Hash key,锁被真正释放。
    • 若递减后 value > 0:说明线程还有同步逻辑未执行完,仅刷新过期时间,锁继续持有。
  4. 通知等待客户端:锁被释放后,Redisson 通过 Redis 的 Pub/Sub 向该锁的释放事件频道发送通知。所有订阅了该事件的等待客户端收到通知后,重新发起获取锁请求,进入下一轮竞争。

释放锁用 Lua 脚本实现的核心原因,是为了保障“校验持有者 + 递减计数 / 删除锁”整个组合操作的原子性。如果由客户端分步发送多条 Redis 命令,在分布式环境下就可能出现“校验通过后锁被其他客户端提前释放、当前客户端又误删了新持有者的锁”的并发安全问题。

3.5 看门狗续期机制

看门狗是 Redisson 解决“业务执行超时导致锁提前释放”这一痛点的核心机制。在分布式锁的使用场景中,有一个典型的死亡三角:

  • 锁过期时间设得太短 → 业务未执行完锁就释放 → 临界区失去保护,出现并发问题。
  • 锁过期时间设得太长 → 客户端崩溃后锁长时间不释放 → 整个系统被一个死锁阻塞。
  • 业务执行时间不可预知 → 无法准确设定过期时间 → 上述两个问题必然命中其一。

看门狗的工作方式是通过自动续期打破这个三角:

  1. 触发条件:客户端成功获取锁后,立即启动一个基于 Netty 时间轮的定时任务。注意,看门狗仅在开发者未显式指定锁过期时间时启动——如果调用 tryLock()lock() 时传入了过期时间,Redisson 认为开发者已有自己的超时策略,不再启动续期。
  2. 续期频率:每隔 defaultLockWatchdogTimeout / 3 执行一次,默认即每 10 秒续期一次。
  3. 续期逻辑:续期是一段 Lua 脚本——先校验当前锁的持有者是否仍是当前客户端线程,若是,则将锁的过期时间重置为默认 30 秒;若不是,停止续期。
  4. 终止条件:两条路径——业务完成,客户端主动释放锁,续期任务立即取消;客户端崩溃,续期任务随进程停止,锁最终被 Redis 自动过期回收。

关键点在于,续期只是“尽力而为”的优化,过期时间才是兜底保障。即使在极端情况(长 GC 暂停、网络分区)下续期任务未能按时执行,锁也会在过期后被自动释放,不会永久占用。

3.6 RedLock 红锁及其争议

上述 Hash + Lua + 看门狗的组合方案,针对的是单节点 Redis 场景。引入主从复制后,会出现一个典型的一致性风险:

sequenceDiagram
    participant Client1
    participant Master
    participant Slave
    participant Client2

    Client1->>Master: Lock
    Master-->>Client1: OK
    
    Note over Master: Master is dead
    Note over Master,Slave: Slave is promoted as new Master
    
    Master-->>Slave: Replicate is failed due to Master death
    
    Client2->>Slave: Lock
    Slave-->>Client2: OK

时序很清楚:客户端 1 加锁成功后 Master 宕机,锁 key 尚未异步复制到 Slave;主从切换后 Slave 升级为新 Master,但新 Master 上没有锁数据;客户端 2 此时请求同一把锁,可以成功获取——结果就是两个客户端同时认为自己持有锁。

Redis 作者 Antirez 针对这个风险提出了 RedLock 算法,核心思想是通过多个独立 Redis 节点的多数写入来保障高可用。算法分四步:

  1. 初始化:客户端初始化 N 个完全独立(不存在主从关系)的 Redis 节点上的锁对象,通常 N = 5。
  2. 多节点加锁:按顺序向所有独立节点发送加锁请求,加锁逻辑与单节点场景完全一致,同时记录每个节点的加锁开始时间。
  3. 结果校验:统计加锁成功的节点数。只有当超过半数节点(5 个中至少 3 个)加锁成功,且整个操作的耗时小于锁的有效存活时间时,才判定加锁成功。否则向所有节点发送释放请求,回退已完成的加锁操作。
  4. 解锁:向所有独立节点发送释放请求,不校验响应结果——这是避免因部分节点异常导致锁无法正常释放。

RedLock 在业界存在较大争议。分布式系统专家 Martin Kleppmann 曾专门撰文质疑其安全性,核心批评集中在三点:

  • 网络分区:网络分区场景下,不同客户端可能在分区的两侧分别与不同节点集合交互,导致双方都认为自己获得了多数节点支持,出现脑裂的情况。
  • 时钟漂移:不同机器的系统时间存在微小差异,影响锁过期时间的一致判断。RedLock 依赖各个 Redis 节点的本地时钟来判定锁是否过期,时钟漂移可能导致不同节点对同一把锁的“已过期”判断不一致。
  • 本质问题仍未解决:RedLock 牺牲了 Redis 的核心优势(高性能),换来的是一个仍有争议的一致性保障,不如直接使用 ZooKeeper 或 etcd 这类 CP 系统。

目前业界对 RedLock 使用的普遍态度是:如果业务场景对一致性要求很高,优先考虑 ZooKeeper 或 etcd,而不是通过 RedLock 将 AP 系统强行改造成 CP 语义。

4 选型框架

三种主锁方案在一致性模型、性能和运维成本上的差异,本质是 CAP 权衡的结果。不存在绝对完美的方案,选择取决于场景优先级:

维度Redis / RedissonZooKeeperetcd
一致性模型AP(弱一致)CP(强一致)CP(强一致)
锁操作延迟极低(亚毫秒级)较高(毫秒级)中等
吞吐量极高(10 万+ QPS)中等中等
运维复杂度
公平性不保证天然支持支持
适用场景高并发、可容忍短暂不一致数据一致性要求极高云原生、一致性 + 运维平衡

推荐选型优先级

  1. 从现有基础设施出发:已有 Redis 集群且对一致性容忍度较高,选 Redisson;已有 ZooKeeper 集群且对一致性要求极高,选 ZooKeeper;云原生架构且对两方面都有较高要求,选 etcd。
  2. 开发侧封装:封装统一的分布式锁工具类,收敛重试逻辑、异常释放逻辑和边界兜底逻辑,避免业务代码中散落锁操作细节。
  3. 监控侧支撑:重点监控加锁成功率、锁等待时长、持锁时长、续期次数和释放失败次数等核心指标,提前识别潜在风险。
  4. 业务侧兜底:无论选择哪种锁方案,业务层都应增加幂等性校验作为最后一道防线。即使在锁服务异常的极端情况下,也能避免数据出现不一致。

5 总结

Redisson 本质是对 Redis 原生锁的一次工程级补全——它没有发明新的分布式锁算法,而是系统性地将 Lua 原子化、Hash 可重入、Pub/Sub 唤醒、看门狗续期和 RedLock 多节点保障这些工程细节封装在框架底层,让开发者用极简 API 获得生产级锁能力。

ZooKeeper 和 etcd 的强一致性保障,是通过共识协议的网络交互和磁盘持久化开销换来的。它们天然适配对一致性要求极高的核心业务场景,但在高并发下需要更多资源投入。

理解三类方案在 CAP 轴上的位置以及 Redisson 在 Redis AP 模型之上的工程补偿策略,是在实际项目中做出正确选型判断的前提。