Redis分布式锁
Redis 分布式锁的底层实现经历了从简单到复杂、从不可靠到相对可靠的演进过程。其核心思想是:在分布式系统中,用一个共用的 Redis服务来充当一个公证人,所有客户端通过在这个公证人那里占坑的方式来获取锁,通过删坑来释放锁。
最基础的实现:SETNX + DEL
这是最原始的想法,利用 Redis 的 SETNX(Set if Not eXists)命令。
- 加锁:
SETNX lock_key my_random_value- 如果返回 1,说明设置成功,客户端获取到锁。
- 如果返回 0,说明 key 已存在,锁被其他客户端持有,当前客户端获取失败。
- 解锁:
DEL lock_key- 删除这个 key,释放锁供其他客户端使用。
存在的问题:
- 死锁:如果获取锁的客户端宕机,无法执行
DEL命令,那么这个锁将永远无法被释放,其他客户端再也无法获得锁。 - 误释放:客户端 A 获取锁后,如果业务执行时间过长,锁超时被 Redis 自动释放(假设设置了过期时间),客户端 B 获取到了锁。此时客户端 A 执行完,调用
DEL命令,就会错误地释放了客户端 B 的锁。
改进版:SETNX + EXPIRE + Lua 脚本
为了解决死锁问题,引入了锁的超时时间。
- 加锁:
SETNX lock_key my_random_value- 如果成功,紧接着执行
EXPIRE lock_key 10(设置 10 秒后自动过期)
- 解锁:使用 Lua 脚本保证原子性。
1
2
3
4
5if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end- 脚本的作用是:只有当前 key 对应的 value 是自己设置的那个随机值,才执行删除操作。这解决了误释放的问题。
仍然存在的问题:SETNX 和 EXPIRE 是两条命令,不是原子操作。如果在执行完 SETNX 后客户端宕机,EXPIRE 没有执行,依然会造成死锁。
官方推荐的标准实现:SET 命令扩展
Redis 2.6.12 之后,SET 命令增加了扩展参数,可以一次性完成set和expire的操作,彻底解决了原子性问题。
加锁命令:
1 | SET lock_key my_random_value NX PX 10000 |
NX:等同于SETNX的效果,只有 key 不存在时才设置。PX 10000:设置 key 的过期时间为 10000 毫秒。
解锁命令:
依然使用上述的 Lua 脚本,保证判断 value 和删除 key 这两个操作的原子性。
这个方案是目前单节点 Redis 分布式锁最成熟、最广泛的实现方案。
为什么要用随机值 my_random_value?
这个值通常是一个随机生成的 UUID 或客户端 ID。它最大的作用是标识锁的归属。防止其他客户端错误地释放了不属于自己的锁。解锁时通过 Lua 脚本校验 value 是否匹配,只有匹配才删除。
高可用问题与 RedLock 算法
上面的方案在单节点 Redis 下工作是没问题的。但在主从复制(Sentinel)或集群(Cluster) 模式下,可能会遇到问题:
场景:
- 客户端 A 从 Master 节点获取了锁。
- 在锁被同步到 Slave 节点之前,Master 节点宕机。
- Sentinel 集群选举出一个新的 Master(原来的一个 Slave)。
- 新的 Master 节点上并没有客户端 A 持有的锁(因为还没同步过来)。
- 客户端 B 向新的 Master 节点申请锁,成功获取。此时,客户端 A 和客户端 B 都认为自己持有锁,导致分布式锁失效。
为了解决这个问题,Redis 的作者 Antirez 提出了 RedLock(Redis Distributed Lock) 算法。
RedLock 核心思想:
它不依赖单个 Redis 实例,而是同时与多个(通常是奇数个,如 5 个)独立的 Redis 主节点(注意:不是主从集群,而是互相独立的主节点)进行交互。
获取锁的步骤:
- 获取当前时间(T1)。
- 客户端依次向 N 个独立的 Redis 实例 发送加锁命令(
SET key random_value NX PX timeout)。 - 只有当客户端从超过半数(N/2 + 1) 的节点上成功获取到锁,并且总的获取时间小于锁的有效时间(TTL),才认为加锁成功。
- 总获取时间 = 当前时间(T2) - T1。
- 锁的有效时间 = 最初设置的 TTL - 总获取时间。
- 如果获取锁失败(要么成功节点数未过半,要么总耗时超过了 TTL),客户端会向所有 Redis 实例发送解锁命令(即使它认为某个实例加锁失败)。
优缺点:
- 优点:在存在节点宕机的情况下,提供了更高的安全性。
- 缺点:
- 实现复杂,需要维护多个独立的 Redis 实例。
- 性能开销大。
- 存在争议:Martin Kleppmann 等人曾撰文质疑其安全性,认为它依赖于一个不安全的系统模型(比如依赖不可靠的时钟)。社区对此有广泛讨论。
建议:除非你的业务对分布式锁的可靠性有极高的要求(比如金融核心资产),并且愿意付出高昂的运维和性能成本,否则一般不建议使用 RedLock。对于绝大多数场景,基于单实例或主从复制的分布式锁已经足够。
现成的轮子:Redisson
在实际项目中,我们几乎不会自己从头实现上述逻辑,而是使用成熟的客户端库。在 Java 领域,Redisson 是最佳选择。
Redisson 的分布式锁实现:
- 加锁:它内部使用了 Lua 脚本,保证了原子性。其加锁命令就是我们上面提到的标准命令:
SET lock_name my_uuid NX PX timeout。 - 看门狗(Watchdog)机制:这是 Redisson 的一大亮点。它会在你获取锁成功后,创建一个定时任务(看门狗),每隔一段时间(比如锁过期时间的 1/3)就去延长锁的过期时间。只要客户端还活着(JVM 没挂),并且锁还在被持有(没解锁),这个续期操作就会一直进行,从而避免了因为业务执行时间过长而导致锁提前过期的问题。
- 解锁:同样使用 Lua 脚本原子性地释放锁。同时会取消看门狗的定时任务。
- 可重入性:Redisson 的锁是可重入的,它通过在 Redis 中存储 Hash 结构来实现,key 是锁名,field 是客户端 ID,value 是重入次数。
- 等待机制:提供了
tryLock等方法来支持获取锁的等待,内部使用了 Pub/Sub 机制来订阅锁释放的消息,避免无效的轮询,减少 Redis 压力。
总结
| 方案 | 核心命令/机制 | 优点 | 缺点 |
|---|---|---|---|
| 基础版 | SETNX + EXPIRE |
简单 | 非原子性,易死锁;易误释放 |
| 标准版 | SET key value NX PX timeout + Lua 脚本 |
原子性加锁和设置超时;安全释放 | 单节点/主从模式有故障转移问题 |
| RedLock | 向多个独立节点申请锁,遵循多数派原则 | 高可用,更高可靠性 | 实现复杂,性能差,存在争议 |
| 生产推荐 | 使用 Redisson 库(内置看门狗、可重入、Pub/Sub等待) | 功能完善,生产级可靠性,开箱即用 | 需要引入第三方库 |
最佳实践建议:
对于绝大多数应用,使用 单主节点 Redis + Redisson 客户端 来实现分布式锁就已经非常可靠和实用了。除非有极端要求,否则应避免使用复杂度极高的 RedLock 算法。


