分布式锁(Distributed Lock)是用于在分布式系统中协调多个节点对共享资源的访问,确保同一时刻只有一个节点可以执行特定操作的一种同步机制。它类似于单机系统中的互斥锁(Mutex),但实现更为复杂,因为需要跨网络、跨进程甚至跨机器进行协调。
一、为什么需要分布式锁?
在单机环境中,我们可以使用语言内置的锁(如 Java 的 synchronized、Python 的 threading.Lock)来保护临界区。但在分布式系统中:
- 多个服务实例部署在不同机器上;
- 各实例之间无法直接共享内存;
- 操作可能并发修改同一个外部资源(如数据库记录、缓存、文件等);
如果不加控制,并发操作可能导致:
- 数据不一致(如超卖、重复下单);
- 资源竞争(如同时写入一个文件);
- 业务逻辑错误(如重复发放优惠券)。
因此,需要一种跨节点可见、全局唯一、强一致的锁机制——即分布式锁。
二、分布式锁的核心要求
- 互斥性(Mutual Exclusion)
任意时刻,只能有一个客户端持有锁。 - 安全性(Safety)
锁不能被错误释放(例如 A 获取的锁不能被 B 释放)。 - 避免死锁(Deadlock-Free)
即使持有锁的客户端崩溃,锁最终也能被释放(通常通过设置超时自动释放)。 - 高可用性(Availability)
锁服务本身不能成为系统瓶颈或单点故障。 - 高性能(Performance)
加锁/解锁操作应尽可能快,减少对业务性能的影响。 - 可重入性(可选)
同一个客户端可以多次获取同一把锁(类似 ReentrantLock)。
常见实现方式
1. 基于 Redis 实现
Redis 因其高性能和原子操作(如 SET key value NX PX)常被用于实现分布式锁。
基本原理:
SET lock_key unique_value NX PX 30000
NX:仅当 key 不存在时才设置(保证互斥);PX 30000:设置 30 秒自动过期(防止死锁);unique_value:通常是客户端 ID 或 UUID,用于验证锁归属。
释放锁(需 Lua 脚本保证原子性):
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
防止误删其他客户端的锁。
缺陷:
- Redis 主从架构下可能发生锁丢失(主写成功但未同步到从,主宕机后从升主导致锁失效);
- 解决方案:使用 Redlock 算法(由 Redis 作者提出,但存在争议)或多副本强一致性存储。
2. 基于 ZooKeeper 实现
ZooKeeper 提供了临时顺序节点(Ephemeral Sequential Node)机制,天然适合实现分布式锁。
实现方式(公平锁):
-
所有客户端在
/locks下创建临时顺序节点(如/locks/lock-0000000001); -
客户端检查自己是否是最小序号的节点:
- 是 → 获取锁;
- 否 → 监听前一个序号的节点(Watcher 机制);
-
当前一个节点删除(释放锁或客户端宕机),当前客户端被唤醒并尝试获取锁。
优点:
- 强一致性(ZAB 协议);
- 自动释放(临时节点随会话断开而删除);
- 支持公平锁、可重入锁。
缺点:
- 性能低于 Redis;
- 运维复杂度高;
- 对网络抖动敏感。
3. 基于数据库实现
利用数据库的唯一索引或行级锁实现。
方式一:唯一索引
- 创建一张锁表:
lock_name VARCHAR(100) UNIQUE - 获取锁:
INSERT INTO locks (lock_name) VALUES ('order_lock') - 释放锁:
DELETE FROM locks WHERE lock_name = 'order_lock'
成功插入表示获取锁,失败则等待重试。
方式二:乐观锁(版本号)
适用于更新场景,如:
UPDATE orders SET status = 'paid', version = version + 1
WHERE id = 123 AND version = old_version;
若返回影响行数为 0,说明并发冲突。
缺点:
- 性能较差(频繁读写 DB);
- 死锁风险(需设置超时);
- 不支持自动过期(需额外定时任务清理)。
这里我们再补充介绍Redlock 算法和 Redisson 分布式锁
一、Redlock 算法详解
1. 背景与动机
标准 Redis 单实例分布式锁在主从架构下存在安全隐患:
- 客户端 A 在主节点成功加锁;
- 主节点尚未将锁同步到从节点就宕机;
- 从节点被提升为主节点;
- 客户端 B 可在新主节点上再次加锁 → 同一资源被两个客户端同时持有锁。
为解决此问题,Redis 作者 Antirez 提出了 Redlock(Redis Distributed Lock)算法,通过多个独立 Redis 实例实现高可用、高安全的分布式锁。
注意:Redlock 是一种算法思想,并非 Redis 内置功能。
2. Redlock 基本假设
- 使用 N 个相互独立的 Redis 主节点(通常 N = 5),无主从复制;
- 各节点之间不共享状态;
- 客户端可与任意节点通信;
- 系统时钟相对稳定(对时间敏感)。
3. Redlock 加锁流程
客户端尝试获取锁的步骤如下:
-
记录开始时间
start_time; -
依次向 N 个 Redis 节点(串行或并行)发送加锁命令:
SET resource_name my_random_value NX PX lock_validity_timeresource_name:锁名称;my_random_value:唯一标识(如 UUID),用于安全释放;lock_validity_time:建议设为 总锁超时时间(如 10s)。
-
统计成功加锁的节点数;
-
计算总耗时:
total_time = current_time - start_time; -
判断是否获取锁成功:
- 成功节点数 > N/2(即多数派);
- 且
total_time < lock_validity_time;
-
若成功,则锁的实际有效时间为:
text 编辑 real_lock_time = lock_validity_time - total_time -
若失败,则向所有节点发送释放锁命令(即使某些节点未加锁成功)。
示例:5 个节点,至少需在 3 个节点上成功加锁,且总耗时 < 锁有效期。
4. Redlock 释放锁
向所有 N 个节点发送 Lua 脚本释放锁(验证 value 一致):
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
5. Redlock 的安全性分析
✅ 优点:
- 避免单点故障;
- 不依赖主从复制,容忍部分节点宕机;
- 满足“大多数”原则,提高一致性。
❌ 争议与缺陷(Martin Kleppmann 等人提出):
- 依赖系统时钟:若某节点时钟跳跃(如 NTP 调整),可能导致锁提前过期;
- 网络延迟不确定性:
total_time计算不可靠; - 无法完全保证互斥性:在极端网络分区下仍可能失效;
- 性能开销大:需与多个节点通信,延迟较高。
因此,Redlock 适用于对一致性要求不是极端严苛、但希望避免单点故障的场景。对于金融级强一致场景,建议使用 ZooKeeper 或 etcd。
二、Redisson 分布式锁详解
Redisson 是一个基于 Redis 的 Java 客户端工具库,提供了丰富的分布式对象和服务,其中分布式锁是其核心功能之一。
1. 核心特性
| 特性 | 说明 |
|---|---|
| 可重入 | 同一线程可多次加锁,计数器递增 |
| 自动续期(Watchdog) | 默认 30s 过期,每 10s 自动续期(只要线程存活) |
| 公平锁 | 支持按请求顺序获取锁(基于 Redis List + Pub/Sub) |
| 多锁(MultiLock) | 可组合多个锁(如 Redlock 实现) |
| 异步 & Reactive 支持 | 支持 CompletableFuture 和 RxJava |
| 锁等待 & 超时 | tryLock(waitTime, leaseTime, unit) |
2. 基本使用示例(Java)
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 获取可重入锁
RLock lock = redisson.getLock("myLock");
try {
// 加锁(自动续期,默认30s过期)
lock.lock();
// 业务逻辑
System.out.println("执行关键操作...");
} finally {
// 解锁
lock.unlock();
}
带超时的尝试加锁:
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// do something
} finally {
lock.unlock();
}
}
3. Redisson 如何实现可重入与 Watchdog?
(1)锁结构(Redis 中存储)
myLock: {
"8743c9c0-0795-4907-87fd-6c719a6b4586:1" : 1
}
- Key:锁名称;
- Value:
threadId:count(线程 ID + 重入次数); - TTL:默认 30 秒。
(2)Watchdog 机制
- 加锁成功后,后台启动一个 watchdog 线程;
- 每隔
leaseTime / 3(默认 10s)检查线程是否仍持有锁; - 若是,则执行
PEXPIRE myLock 30000续期; - 线程 unlock 或 JVM 退出后,watchdog 停止。
⚠️ 如果你手动指定了
leaseTime(如lock.lock(10, TimeUnit.SECONDS)),则禁用 watchdog,锁将在 10s 后自动释放。
4. Redisson 对 Redlock 的支持
Redisson 提供了 RedissonMultiLock 来实现 Redlock:
RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {
boolean locked = multiLock.tryLock(100, 30, TimeUnit.SECONDS);
if (locked) {
// 执行业务
}
} finally {
multiLock.unlock();
}
此处
redisson1/2/3分别连接不同的 Redis 实例,构成 Redlock 所需的独立节点集群。
5. Redisson vs 原生 Redis 锁
| 对比项 | 原生 Redis SET NX PX | Redisson |
|---|---|---|
| 可重入 | ❌ | ✅ |
| 自动续期 | ❌ | ✅(Watchdog) |
| 公平锁 | ❌ | ✅ |
| Redlock 支持 | 需手动实现 | ✅(内置 MultiLock) |
| 易用性 | 低(需写 Lua) | 高(API 封装) |
| 语言支持 | 通用 | Java 为主(也有 .NET、Node.js 等版本) |