分布式锁

4 阅读8分钟

分布式锁(Distributed Lock)是用于在分布式系统中协调多个节点对共享资源的访问,确保同一时刻只有一个节点可以执行特定操作的一种同步机制。它类似于单机系统中的互斥锁(Mutex),但实现更为复杂,因为需要跨网络、跨进程甚至跨机器进行协调。

一、为什么需要分布式锁?

在单机环境中,我们可以使用语言内置的锁(如 Java 的 synchronized、Python 的 threading.Lock)来保护临界区。但在分布式系统中:

  • 多个服务实例部署在不同机器上;
  • 各实例之间无法直接共享内存;
  • 操作可能并发修改同一个外部资源(如数据库记录、缓存、文件等);

如果不加控制,并发操作可能导致:

  • 数据不一致(如超卖、重复下单);
  • 资源竞争(如同时写入一个文件);
  • 业务逻辑错误(如重复发放优惠券)。

因此,需要一种跨节点可见、全局唯一、强一致的锁机制——即分布式锁。

二、分布式锁的核心要求

  1. 互斥性(Mutual Exclusion)
    任意时刻,只能有一个客户端持有锁。
  2. 安全性(Safety)
    锁不能被错误释放(例如 A 获取的锁不能被 B 释放)。
  3. 避免死锁(Deadlock-Free)
    即使持有锁的客户端崩溃,锁最终也能被释放(通常通过设置超时自动释放)。
  4. 高可用性(Availability)
    锁服务本身不能成为系统瓶颈或单点故障。
  5. 高性能(Performance)
    加锁/解锁操作应尽可能快,减少对业务性能的影响。
  6. 可重入性(可选)
    同一个客户端可以多次获取同一把锁(类似 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)机制,天然适合实现分布式锁。

实现方式(公平锁):

  1. 所有客户端在 /locks 下创建临时顺序节点(如 /locks/lock-0000000001);

  2. 客户端检查自己是否是最小序号的节点:

    • 是 → 获取锁;
    • 否 → 监听前一个序号的节点(Watcher 机制);
  3. 当前一个节点删除(释放锁或客户端宕机),当前客户端被唤醒并尝试获取锁。

优点:

  • 强一致性(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 加锁流程

客户端尝试获取锁的步骤如下:

  1. 记录开始时间 start_time

  2. 依次向 N 个 Redis 节点(串行或并行)发送加锁命令:

    SET resource_name my_random_value NX PX lock_validity_time
    
    • resource_name:锁名称;
    • my_random_value:唯一标识(如 UUID),用于安全释放;
    • lock_validity_time:建议设为 总锁超时时间(如 10s)。
  3. 统计成功加锁的节点数

  4. 计算总耗时total_time = current_time - start_time

  5. 判断是否获取锁成功

    • 成功节点数 > N/2(即多数派);
    • 且 total_time < lock_validity_time
  6. 若成功,则锁的实际有效时间为:

    text
    编辑
    real_lock_time = lock_validity_time - total_time
    
  7. 若失败,则向所有节点发送释放锁命令(即使某些节点未加锁成功)。

示例: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 等人提出):
  1. 依赖系统时钟:若某节点时钟跳跃(如 NTP 调整),可能导致锁提前过期;
  2. 网络延迟不确定性total_time 计算不可靠;
  3. 无法完全保证互斥性:在极端网络分区下仍可能失效;
  4. 性能开销大:需与多个节点通信,延迟较高。

因此,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 PXRedisson
可重入
自动续期✅(Watchdog)
公平锁
Redlock 支持需手动实现✅(内置 MultiLock)
易用性低(需写 Lua)高(API 封装)
语言支持通用Java 为主(也有 .NET、Node.js 等版本)