如何使用Redis实现一个高可用的分布式锁?
使用 Redis 实现一个高可用的分布式锁,不能仅仅依靠简单的 SETNX 命令。在生产环境中,需要考虑进程崩溃、网络延迟、Redis 节点宕机、主从切换导致的数据丢失等多种复杂情况。
一个完整且高可用的 Redis 分布式锁演进过程可以分为以下几个阶段。在 Java 生态中,通常直接使用 Redisson 框架来实现,但理解其底层原理至关重要。
第一阶段:单节点的基础实现(标准命令 + Lua)
在单节点 Redis 下,实现分布式锁必须保证加锁和解锁的原子性,同时防止死锁。
1. 加锁:使用 SET 命令的扩展参数
必须将“判断是否存在”和“设置过期时间”合并为一条命令,防止客户端刚拿到锁就崩溃导致死锁。
# 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 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
第二阶段:解决业务执行时间 > 锁过期时间(看门狗机制)
痛点:如果业务逻辑执行需要 40 秒,而锁的过期时间是 30 秒。第 30 秒时锁自动释放,其他线程就会拿到锁,导致并发安全问题。
解决方案:Watchdog(看门狗)自动续期机制
- 加锁成功后,启动一个后台守护线程(看门狗)。
- 看门狗每隔一段时间(例如锁过期时间的 1/3,即 10 秒)检查一次:
- 如果当前客户端还持有这把锁(业务没执行完),就使用 Lua 脚本重置锁的过期时间(例如重新续到 30 秒)。
- 当业务执行完毕主动解锁,或者客户端宕机(看门狗线程随之死亡),续期停止,锁最终会自动过期释放。
(注:Redisson 框架默认自带且开启了 Watchdog 机制)
第三阶段:解决 Redis 宕机问题(高可用方案)
前面两步在单机 Redis 上运行良好。但为了“高可用”,Redis 通常会部署主从架构(Master-Slave)或哨兵/集群模式。
痛点(主从切换导致锁丢失):
- 客户端 A 在 Master 节点成功申请了一把锁。
- Master 还没来得及将这把锁同步给 Slave 就宕机了(Redis 的主从同步是异步的)。
- 哨兵将 Slave 提升为新 Master。
- 客户端 B 来新 Master 申请同一把锁,因为新 Master 上没有这个 key,申请成功。
- 结果:客户端 A 和 B 同时持有了同一把分布式锁,系统崩溃。
解决方案:Redlock(红锁)算法
为了解决主从异步复制导致的锁丢失问题,Redis 作者 Antirez 提出了 Redlock 算法。它不再依赖主从复制,而是依赖多个完全独立的 Redis Master 节点(通常为 5 个)。
Redlock 的加锁步骤:
- 客户端记录当前时间戳
T1。 - 客户端尝试向 5 个独立的 Redis Master 节点发起加锁请求(使用相同的 key 和 random_value,且请求超时时间要远小于锁过期时间,例如 50ms)。
- 如果客户端在多数节点(>= 3 个)上加锁成功,且再次获取当前时间戳
T2。 - 计算获取锁的总耗时
T_cost = T2 - T1。 - 如果
T_cost < 锁的有效时间,则认为加锁成功!- 锁的真正剩余有效时间 =
初始有效时间 - T_cost。
- 锁的真正剩余有效时间 =
- 如果加锁失败(成功节点 < 3,或者耗时太长),客户端必须向所有 5 个节点发起解锁请求(即使刚才没加锁成功的节点也要发,防止由于网络延迟导致实际上加锁成功了但客户端没收到响应)。
第四阶段:生产级最佳实践(使用 Redisson)
在 Java 开发中,强烈不建议手写上述复杂的逻辑,直接使用 Redisson 是行业标准。
1. 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.x.x</version>
</dependency>
2. 基本使用(自带可重入、看门狗)
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)
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 分布式锁,你的架构应当具备:
- 防死锁:必须设置
TTL过期时间。 - 防误删:Value 必须是客户端唯一标识(UUID+ThreadId),解锁用 Lua 脚本校验。
- 防超时:引入 Watchdog 看门狗机制,后台自动续期。
- 高可用:在极端严苛的场景下,摒弃主从复制,使用多个独立 Master 节点进行 MultiLock 操作。
架构师视角的权衡(Redis vs Zookeeper):
- Redis 分布式锁:本质上是 AP 模型(高可用、高性能),在极端情况下(如时钟发生回拨、超长 GC 导致看门狗假死、Redis 集群脑裂)仍然有极小概率出现并发冲突。适用于追求极致性能,且业务允许万分之一概率出现锁失效的场景(可通过数据库兜底处理)。
- Zookeeper / etcd 分布式锁:本质上是 CP 模型(强一致性),利用临时顺序节点和监听机制,安全性更高,不会出现脑裂导致锁失效,但性能和吞吐量不如 Redis。适用于对数据一致性要求极其严苛(如核心金融交易)的场景。