分布式锁实现:Redis、Zookeeper 与红锁

FreeGuideOnline 最新 2026-06-16

分布式锁入门

在单机应用中,锁机制(如 synchronizedReentrantLock)可以保证多线程并发安全。但在分布式系统下,多个进程分散在不同机器上,传统的 JVM 锁就失效了。此时需要一种跨进程、跨主机的锁机制来协调共享资源的访问——这就是分布式锁

分布式锁需要满足几个核心特性:

  • 互斥性:同一时刻只有一个客户端持有锁。
  • 防死锁:即使持有锁的客户端崩溃或发生网络分区,锁能自动释放。
  • 容错性:锁服务本身应当具备一定的可用性,不能因为单点故障导致系统不可用。
  • 可重入性(可选):同一个客户端可以重复获取同一把锁。

本教程将详细介绍三种主流的分布式锁实现方案:基于 Redis基于 ZooKeeper 以及 Redis 红锁(Redlock)


基于 Redis 的分布式锁

Redis 因为高性能和丰富的数据结构,是实现分布式锁最常用的技术之一。实现方法从简单到完善大致经历了两个阶段。

基础实现:SETNX + 过期时间

使用一条原子命令即可尝试获取锁:

SET lock_key unique_value NX PX 30000
  • NX:只有当键不存在时才设置,保证互斥。
  • PX 30000:锁的过期时间为 30000 毫秒,防止死锁。
  • unique_value:客户端生成的唯一标识(UUID),用于释放锁时验证身份。

释放锁时,不能简单地执行 DEL lock_key,必须判断值是否和自己设置的一致:

-- Lua 脚本保证原子性
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

将这一段 Lua 脚本交给 Redis 原子执行,避免“判断属于自己”与“删除锁”之间出现竞态条件。

这种实现方式简单,但存在一个严重隐患:如果业务执行时间超过了锁的过期时间,锁会被自动释放,其他客户端就有可能拿到锁,导致并发安全问题。

生产级方案:Redisson 的看门狗机制

Redisson 是一个 Redis 客户端,它提供了完善的分布式锁实现——RLock,并内置了**看门狗(Watch Dog)**机制。

工作原理

  1. 客户端尝试获取锁,若成功会启动一个后台线程(看门狗)。
  2. 看门狗每隔 10 秒(默认锁超时 30 秒的三分之一时间)检查是否仍持有锁,若持有则自动续期,将过期时间重新设置为 30 秒。
  3. 当客户端主动解锁或进程崩溃后,看门狗停止续期,锁最终自然过期。

这样即使业务执行较久,只要客户端活着,锁就不会意外释放。

获取锁示例(Java)

RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认超时 30 秒,看门狗自动续期
try {
    // 执行业务代码
} finally {
    lock.unlock();
}

Redisson 内部使用 Hash 结构存储锁信息,键为锁名称,字段为客户端连接 ID,值表示重入次数,从而天然支持可重入锁。

优点:使用简单,性能高,自动续期,支持可重入、公平锁等。
缺点:依赖于 Redis 主从复制,在故障转移场景下可能出现锁安全问题(下文会展开)。


基于 ZooKeeper 的分布式锁

ZooKeeper(ZK)是一个分布式协调服务,基于 ZAB(原子广播)协议保证一致性,天然适合实现分布式锁。

原理:临时顺序节点

ZK 实现分布式锁的核心是利用临时顺序节点

获取锁流程

  1. 所有客户端在指定路径(如 /locks/mylock)下创建一个临时顺序节点,例如 lock-00000001
  2. 客户端获取 /locks/mylock 下所有子节点,并按序号排序。
  3. 如果自己创建的节点是序号最小的,则认为获取锁成功。
  4. 如果不是最小,则对自己前一个节点注册一个 Watcher 监听,进入等待状态。

释放锁

  • 主动删除自己创建的临时节点。
  • 客户端会话结束(进程退出或网络断开),临时节点会被 ZK 自动删除,ZooKeeper 会通知下一个客户端开始重新争锁。

这种机制避免了“惊群效应”(Herd Effect):每次锁释放只唤醒排在后面的一个客户端,而不是所有客户端都去竞争。

实现示例(Curator 框架)

InterProcessMutex lock = new InterProcessMutex(curatorClient, "/locks/mylock");
if (lock.acquire(5, TimeUnit.SECONDS)) {
    try {
        // 业务逻辑
    } finally {
        lock.release();
    }
}

Apache Curator 封装了上述复杂逻辑,开发者仅需几行代码即可使用。

优点

  • 强一致性,可靠性高。
  • 自动释放,没有过期时间困扰。
  • 支持可重入、公平锁。

缺点

  • 性能不如 Redis,因为 ZK 基于磁盘和一致性协议,频繁的创建/删除节点开销较大。
  • 依赖 ZK 集群的稳定性,维护成本相对较高。

Redis 红锁(Redlock)算法

当使用单实例 Redis 或 Redis 主从模式时,主从切换可能导致锁丢失。例如:

  1. 客户端 A 在 Master 上获得了锁。
  2. Master 宕机,尚未将锁数据同步到 Slave。
  3. Slave 提升为新 Master,此时锁信息丢失。
  4. 客户端 B 向新 Master 请求同一把锁,成功获取,导致并发冲突。

Redis 作者 Antirez 提出的 Redlock 算法正是为了解决这种场景下的可靠性问题。

Redlock 核心流程

Redlock 要求部署 N 个完全独立的 Redis 实例(推荐 N=5,通常 N 为奇数)。获取锁时会向每个实例依次尝试 SET NX PX

  1. 客户端生成一个全局唯一的随机字符串作为锁值。
  2. 依次向 N 个 Redis 实例请求获取锁,对每个实例使用相对较小的超时时间(如几毫秒),避免长时间等待某个故障节点。
  3. 计算获取锁总共消耗的时间。只有当 获取到锁的实例数 >= N/2 + 1(即多数派),并且总消耗时间小于锁的有效期,才认为锁获取成功。
  4. 锁的实际有效时间为 锁原定有效期 - 获取锁消耗的时间
  5. 如果获取失败,客户端会向所有实例(包括之前成功的)发送释放锁的请求。

释放锁类似基础版,需要向所有实例执行 Lua 删除脚本。

优势与争议

Redlock 提供了比单实例或主从切换更高的容错性,即使少数 Redis 实例宕机,锁服务仍然可用。但它并非绝对安全,学术界与工业界对 Redlock 有过激烈讨论(如 Martin Kleppmann 的文章),主要问题集中在:

  • 系统时钟跳变可能影响锁有效期。
  • 垃圾回收(GC)导致进程暂停,导致锁在业务执行超时后仍被其他客户端获取。

因此 Redlock 适用于对正确性要求“非常高”但可以接受极少概率错误的场景;对于需要绝对一致性的场景,ZooKeeper 或 etcd 更合适。

工程化使用

Redisson 实现了 RedissonRedLock,只需提供多个 RedissonClient

RLock lock1 = redissonClient1.getLock("myLock");
RLock lock2 = redissonClient2.getLock("myLock");
RLock lock3 = redissonClient3.getLock("myLock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();
try {
    // 业务逻辑
} finally {
    redLock.unlock();
}

方案对比与选型建议

特性 Redis (简单版) Redis (Redisson) ZooKeeper Redlock
互斥性 是(多数派)
自动续期 是(看门狗) 无需(会话保持) 看门狗(单实例)无全局续期
锁释放安全性 依赖过期 续期 + 过期 会话断开即释放 多数派释放 + 过期
性能 极高 中等 较低(多实例通信)
一致性模型 最终一致 最终一致 强一致 多数派(较一致)
实现复杂度
典型适用场景 低并发或可重试 高并发、长时间任务 金融、一致性优先 跨地域/高可靠需求

选型建议

  • 绝大多数场景:使用 Redis + Redisson,性能好,代码少,看门狗机制能覆盖常见超时问题,配合哨兵或集群模式保证可用性。
  • 要求强一致性、不容忍任何锁丢失:选择 ZooKeeper 或 etcd。
  • 需要极高容错且无法接受主从切换风险:可以考虑 Redlock,但要充分评估时钟和 GC 的影响,并做好业务幂等。

无论采用哪种方案,都不能完全依赖分布式锁解决并发问题,必须在业务层面结合幂等性设计,才能构建真正健壮的分布式系统。