基于本文回答
0
评论

如何使用Redis实现一个高可用的分布式锁?

知识点图片

使用 Redis 实现一个高可用的分布式锁,不能仅仅依靠简单的 SETNX 命令。在生产环境中,需要考虑进程崩溃、网络延迟、Redis 节点宕机、主从切换导致的数据丢失等多种复杂情况。

一个完整且高可用的 Redis 分布式锁演进过程可以分为以下几个阶段。在 Java 生态中,通常直接使用 Redisson 框架来实现,但理解其底层原理至关重要。


第一阶段:单节点的基础实现(标准命令 + Lua)

在单节点 Redis 下,实现分布式锁必须保证加锁解锁的原子性,同时防止死锁。

1. 加锁:使用 SET 命令的扩展参数

必须将“判断是否存在”和“设置过期时间”合并为一条命令,防止客户端刚拿到锁就崩溃导致死锁。

bash
# resource_name: 锁的名称
# my_random_value: 客户端生成的唯一标识(UUID+线程ID),用于解锁时防误删
# NX: 只有 key 不存在时才设置(Not eXists)
# PX 30000: 设置过期时间为 30000 毫秒(30秒)
SET resource_name my_random_value NX PX 30000

2. 解锁:使用 Lua 脚本保证原子性

解锁时,必须判断这把锁是不是自己加的(通过对比 my_random_value),如果是才能删除。获取和删除必须是原子操作,因此需要用 Lua 脚本:

plaintext
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

第二阶段:解决业务执行时间 > 锁过期时间(看门狗机制)

痛点:如果业务逻辑执行需要 40 秒,而锁的过期时间是 30 秒。第 30 秒时锁自动释放,其他线程就会拿到锁,导致并发安全问题。

解决方案:Watchdog(看门狗)自动续期机制

  1. 加锁成功后,启动一个后台守护线程(看门狗)。
  2. 看门狗每隔一段时间(例如锁过期时间的 1/3,即 10 秒)检查一次:
    • 如果当前客户端还持有这把锁(业务没执行完),就使用 Lua 脚本重置锁的过期时间(例如重新续到 30 秒)。
  3. 当业务执行完毕主动解锁,或者客户端宕机(看门狗线程随之死亡),续期停止,锁最终会自动过期释放。

(注:Redisson 框架默认自带且开启了 Watchdog 机制)


第三阶段:解决 Redis 宕机问题(高可用方案)

前面两步在单机 Redis 上运行良好。但为了“高可用”,Redis 通常会部署主从架构(Master-Slave)或哨兵/集群模式。

痛点(主从切换导致锁丢失)

  1. 客户端 A 在 Master 节点成功申请了一把锁。
  2. Master 还没来得及将这把锁同步给 Slave 就宕机了(Redis 的主从同步是异步的)。
  3. 哨兵将 Slave 提升为新 Master。
  4. 客户端 B 来新 Master 申请同一把锁,因为新 Master 上没有这个 key,申请成功。
  5. 结果:客户端 A 和 B 同时持有了同一把分布式锁,系统崩溃。

解决方案:Redlock(红锁)算法

为了解决主从异步复制导致的锁丢失问题,Redis 作者 Antirez 提出了 Redlock 算法。它不再依赖主从复制,而是依赖多个完全独立的 Redis Master 节点(通常为 5 个)。

Redlock 的加锁步骤

  1. 客户端记录当前时间戳 T1
  2. 客户端尝试向 5 个独立的 Redis Master 节点发起加锁请求(使用相同的 key 和 random_value,且请求超时时间要远小于锁过期时间,例如 50ms)。
  3. 如果客户端在多数节点(>= 3 个)上加锁成功,且再次获取当前时间戳 T2
  4. 计算获取锁的总耗时 T_cost = T2 - T1
  5. 如果 T_cost < 锁的有效时间,则认为加锁成功
    • 锁的真正剩余有效时间 = 初始有效时间 - T_cost
  6. 如果加锁失败(成功节点 < 3,或者耗时太长),客户端必须向所有 5 个节点发起解锁请求(即使刚才没加锁成功的节点也要发,防止由于网络延迟导致实际上加锁成功了但客户端没收到响应)。

第四阶段:生产级最佳实践(使用 Redisson)

在 Java 开发中,强烈不建议手写上述复杂的逻辑,直接使用 Redisson 是行业标准。

1. 引入依赖

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.x.x</version>
</dependency>

2. 基本使用(自带可重入、看门狗)

java
RLock lock = redisson.getLock("myResourceLock");

try {
    // 尝试加锁,最多等待100秒,上锁以后30秒自动解锁(如果不传时间,默认采用看门狗机制续期)
    // lock.lock(); // 阻塞式等待,默认开启看门狗
    boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
        // 执行业务逻辑
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    // 必须判断当前是否还持有锁,并且当前线程是加锁的线程
    if (lock.isLocked() && lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

3. Redisson 中的联锁(MultiLock)替代 Redlock

(注意:由于 Redlock 算法在面对时钟跳跃、长时间 GC 时存在理论上的争议,Redisson 官方近期已废弃了 RedissonRedLock,推荐使用 RedissonMultiLock)

java
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

// 将多个锁合并为一个大锁
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);

// 只有当所有的锁都加锁成功时,才算成功
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

总结与技术选型建议

实现一个高可用的 Redis 分布式锁,你的架构应当具备:

  1. 防死锁:必须设置 TTL 过期时间。
  2. 防误删:Value 必须是客户端唯一标识(UUID+ThreadId),解锁用 Lua 脚本校验。
  3. 防超时:引入 Watchdog 看门狗机制,后台自动续期。
  4. 高可用:在极端严苛的场景下,摒弃主从复制,使用多个独立 Master 节点进行 MultiLock 操作。

架构师视角的权衡(Redis vs Zookeeper):

  • Redis 分布式锁:本质上是 AP 模型(高可用、高性能),在极端情况下(如时钟发生回拨、超长 GC 导致看门狗假死、Redis 集群脑裂)仍然有极小概率出现并发冲突。适用于追求极致性能,且业务允许万分之一概率出现锁失效的场景(可通过数据库兜底处理)。
  • Zookeeper / etcd 分布式锁:本质上是 CP 模型(强一致性),利用临时顺序节点和监听机制,安全性更高,不会出现脑裂导致锁失效,但性能和吞吐量不如 Redis。适用于对数据一致性要求极其严苛(如核心金融交易)的场景。
右滑查看面试常问