分布式锁(Distributed lock)

文章目录
- 分布式锁(Distributed lock)
-
- 1. 什么是分布式锁(Distributed lock)?
- 2. 分布式锁应具备的特性
- 3. 分布式锁的常见实现方式
-
- 3.1 基于关系型数据库
- 3.2 基于Redis
- 3.3 基于 ZooKeeper / etcd / Consul
- 3.4 基于其他存储系统
- 4. 实现分布式锁的挑战与应对
- 5. 各方案对比
- 6. 最佳实践与使用建议
- 7. 总结
1. 什么是分布式锁(Distributed lock)?
在单机环境中,多线程或多进程对共享资源的并发访问通常使用本地锁(如 synchronized、ReentrantLock)来保证数据一致性。但在分布式系统中,多个节点(服务实例)需要协同访问同一共享资源(如数据库记录、文件、缓存)时,本地锁无法跨进程生效,此时就需要分布式锁。
分布式锁是一种跨多个服务实例(通常运行在不同机器上)的互斥机制,它确保在任何时刻,只有一个客户端(服务实例)能够持有锁并访问受保护的共享资源,从而避免数据竞争和状态不一致。
2. 分布式锁应具备的特性
一个设计良好的分布式锁通常需要满足以下特性:
- 互斥性:在同一时刻,只有一个客户端可以持有锁。
- 可重入性(可选):同一个客户端在持有锁的情况下可以再次成功获取锁(避免死锁)。
- 锁超时释放:持有锁的客户端崩溃或网络分区时,锁能够自动释放,避免死锁。
- 高可用性与高性能:锁服务本身应具备高可用性,且获取/释放锁的操作应低延迟。
- 阻塞/非阻塞:支持阻塞式获取锁(获取不到则等待)或非阻塞式尝试获取。
- 公平性(可选):锁的获取顺序与请求顺序一致。
- 容错性:即使部分锁服务节点故障,也不影响锁的正确性。
3. 分布式锁的常见实现方式
实现分布式锁通常依赖一个外部协调服务或数据存储,下面介绍三种主流方案。
3.1 基于关系型数据库
利用数据库的唯一索引或行锁实现互斥。
-
实现方式:
- 创建一张锁表,包含锁名称、持有者标识、过期时间等字段。
- 获取锁:尝试插入一条记录(锁名称唯一约束)。插入成功则表示获取锁;失败则锁已被占用。
- 释放锁:删除对应记录。
- 为防止死锁,可增加过期时间,定时清理超时记录。
-
优点:
- 实现简单,不引入额外组件。
- 利用数据库事务可保证一致性。
-
缺点:
- 数据库性能瓶颈,频繁加锁可能拖垮数据库。
- 无法直接支持锁超时自动释放(需后台线程清理)。
- 数据库单点问题(可通过主从、分片缓解,但仍复杂)。

3.2 基于Redis
Redis 基于内存、单线程模型,适合实现高性能分布式锁。常见方案有:
3.2.1 单节点 Redis 锁(SETNX + EXPIRE)
早期使用 SETNX(Set if Not eXists)命令加锁,再通过 EXPIRE 设置超时。但这两个操作非原子性,可能导致死锁(SETNX 成功但 EXPIRE 失败)。改进方案使用 Lua 脚本或 Redis 2.6.12 后的 SET 命令原子操作:
SET lock_key unique_value NX PX 30000
- NX:键不存在时才设置。
- PX 30000:设置过期时间 30 秒。
- unique_value:客户端唯一标识,用于安全释放锁(释放时需校验是否自己持有,防止误删)。
释放锁时使用 Lua 脚本原子地比较并删除:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
3.2.2 RedLock 算法(多节点 Redis)
Redis 作者提出 RedLock 算法,用于在多个 Redis 主节点(通常 5 个)上获取锁,提高容错性。客户端依次向所有节点请求锁,计算获取锁的总耗时,若超过半数节点成功且总耗时小于锁有效时间,则认为获取锁成功。释放时向所有节点发送释放命令。
- 优点:性能高,适合高并发场景。
- 缺点:依赖时钟同步,存在一定争议(如 Martin Kleppmann 曾撰文批评其安全性)。实现复杂,需考虑网络延迟、节点故障等。
3.2.3 使用 Redisson 客户端
Redisson 是 Java 的 Redis 客户端,封装了分布式锁的高级实现,支持可重入、自动续期(看门狗机制)、红锁等多种模式,简化开发。

3.3 基于 ZooKeeper / etcd / Consul
这类协调服务基于 ZAB(ZooKeeper Atomic Broadcast)或 Raft 共识算法,提供强一致性、顺序节点和 Watcher 机制,天然适合实现分布式锁。
-
ZooKeeper 实现原理:
- 利用临时顺序节点:客户端在锁目录下创建临时顺序节点(EPHEMERAL_SEQUENTIAL)。
- 获取锁:检查自己创建的节点序号是否为最小,若是则获得锁;否则监听前一个节点的删除事件,进入等待。
- 释放锁:删除自身节点(客户端断开连接时临时节点自动删除)。
- 这种方式保证了锁的公平性,且无需设置超时时间,客户端崩溃后节点自动删除,避免死锁。
-
etcd / Consul:类似实现,基于 Raft 和 Lease 机制,提供事务和 watch 功能。
-
优点:
- 强一致性,可靠性高。
- 自带自动释放(会话过期),无需额外超时控制。
- 可公平锁,监听机制能有效唤醒等待客户端。
-
缺点:
- 性能相对 Redis 稍低(磁盘存储、一致性协议开销)。
- 客户端需要维护长连接,会话管理较复杂。

3.4 基于其他存储系统
- Chubby:Google 的分布式锁服务,Paxos 算法实现,不对外开源。
- Tair、Memcached 等也可以实现,但用得较少。
4. 实现分布式锁的挑战与应对
-
锁超时与自动续期:
- 若持有锁的客户端处理时间超过锁的过期时间,锁被自动释放,其他客户端可能获取锁,导致并发问题。
- 解决方案:客户端启动“看门狗”线程,在锁快过期时主动续期(如 Redisson 的 Watchdog)。也可在业务代码中估算合理超时,避免过长或过短。
-
锁的可重入性:
- 需要记录持有锁的客户端和重入次数。可通过 ThreadLocal 或 Redis 中的 hash 结构存储计数器。
-
锁的公平性与非公平性:
- ZooKeeper 的临时顺序节点天然支持公平锁;Redis 通常是非公平的,可通过队列实现公平锁,但复杂度高。
-
死锁预防:
- 数据库方案需设置过期时间并定期清理;Redis 设置过期时间;ZooKeeper 利用临时节点自动清理。
-
性能与可用性权衡:
- 高并发场景通常选择 Redis;对一致性要求极高的场景(如金融支付)倾向 ZooKeeper/etcd。
-
时钟跳跃问题(Redis):
- 若 Redis 节点时钟发生跳跃,可能导致锁意外过期。RedLock 假设各节点时钟基本同步,但仍需谨慎。
-
网络分区与脑裂:
- 若客户端与锁服务网络断开,可能丢失锁,导致多个客户端同时操作。需业务端做幂等或版本控制。
5. 各方案对比
| 一致性 | 最终一致(取决于数据库) | 最终一致(主从异步复制) | 强一致性 |
| 性能 | 低 | 高 | 中 |
| 自动释放 | 需额外实现 | 支持过期时间 | 临时节点(会话超时) |
| 可重入 | 需实现 | 需实现(Redisson 支持) | 需实现 |
| 公平锁 | 可实现 | 通常非公平,可改造 | 原生支持(顺序节点) |
| 适用场景 | 低并发、已有数据库 | 高并发、缓存场景 | 强一致性要求高的分布式协调场景 |
| 复杂性 | 低 | 中 | 高 |
6. 最佳实践与使用建议
7. 总结
分布式锁是分布式系统中协调共享资源访问的关键技术。根据应用场景的不同,可以选择基于数据库、Redis、ZooKeeper 等不同的实现方案。理解各种方案的原理、优缺点及潜在问题,结合实际业务需求进行选型和优化,才能构建可靠、高效的分布式锁服务。




