redis的分布式锁的进化过程
整个进化路线大致是:SETNX 原始锁 → SET NX PX 原子锁 → Lua 安全解锁 → 锁续期(看门狗) → 多实例 Redlock。
第一阶段:原始时代——SETNX
这是最朴素的思路:利用 Redis 的单线程命令 setnx(SET if Not exists)来争抢锁。
-
加锁:
setnx lock_key unique_value-
返回 1,说明 key 不存在,设置成功,得到锁。
-
返回 0,说明 key 已存在,抢锁失败。
-
-
解锁:
del lock_key
致命缺陷:死锁
如果持有锁的客户端进程崩溃,或服务器宕机,来不及执行 DEL,这个锁就会永远存在,所有其他客户端都会因为抢不到锁而永久阻塞。
第二阶段:原子加锁与过期——SET NX PX
为了解决死锁,直观的想法是给锁加一个“保质期”。但简单的先 SETNX 再 EXPIRE 是非原子操作,若在两个命令之间发生故障,死锁问题依旧。
于是,Redis 2.6.12 版本引入了扩展的 SET 命令,将加锁和设置超时合并为一个原子操作,彻底解决了死锁难题。
- 加锁:
SET lock_key unique_value NX PX 30000 - 语义:仅当
lock_key不存在(NX)时,设置其值为unique_value并在 30000 毫秒(PX)后过期。
进化成果:解决了死锁问题。
新问题:
-
锁过期释放导致任务并发:业务执行时间超过了锁的过期时间,锁自动释放,第二个客户端拿到锁。此时第一个客户端任务执行完,两个客户端就会同时操作共享资源。
-
释放了别人的锁:客户端A 任务超时,锁自动过期。客户端B 拿到锁。此时客户端A完成任务,执行
DEL lock_key,却把 客户端B 持有的锁给删除了。
第三阶段:安全解锁——Lua 脚本
为了解决"释放别人锁"的问题,解锁时不能再盲目地 DEL,而是先核对自己的身份标识(unique_value),确认是自己的锁,再删除。为了保证 核对 和 删除 两步的原子性,必须使用 Lua 脚本。
- 安全解锁脚本
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
客户端调用时传入 lock_key 和 unique_value。Redis 会原子地执行这个脚本。
进化成果:解决了锁被它人误删的问题。
悬而未决的问题:
锁过期时间过长或过短的矛盾依然存在,即业务超时导致多个客户端并发持有锁的根本问题还没解决。
第四阶段:自动续期——看门狗机制
核心原理:
1、客户端加锁时,设置一个相对较短的初始过期时间(例如30秒)。
2、同时启动一个后台守护线程(看门狗,Watchdog)。
3、只要客户端还在运行且锁未被显式释放,这个看门狗会定期(例如每10秒)向Redis发送命令,将锁的过期时间重新 续期 到初始值(30秒)。
4、当业务执行完毕,客户端显式释放锁,并告诉看门狗停止续期。
5、如果客户端机器突然宕机,看门狗随之消亡,锁在初始过期时间后自然失效,不会死锁。
进化成果:完美解决了"业务执行时间不确定"和"死锁"之间的矛盾。只要客户端活着,锁就一直是有效的。
代表实现:Redisson 是这一模式的典型代表,它将看门狗机制封装在内部,对开发者透明。
第五阶段:高可用下的脑裂——Redlock
上述所有方案都建立在单个 Redis 实例是绝对可靠的假设之上。但在主从架构中,如果主节点刚写入锁就立即宕机,而数据还未同步到从节点,新晋升的主节点会丢失这把锁,导致其他客户端再次获得锁,出现锁丢失。
为了解决主从切换带来的安全性问题,Redis 作者 Antirez 提出了 Redlock(红锁)算法。
核心思想:不在一个实例上排队,而是去中心化地在大半个集群中"投票"。
执行步骤:
1、客户端获取当前毫秒时间戳。
2、依次向 N 个完全独立的 Redis 主节点(通常是 5 个)请求加锁。每次请求有极短的超时时间(远小于锁有效期)。
3、客户端计算总耗时。当且仅当在 大多数节点((N/2+1))上成功加锁,且总耗时小于锁的有效期时,才认为最终加锁成功。
4、锁的真正有效期 = 初始有效期 - 总耗时。
5、如果加锁失败,客户端会向所有节点发送解锁请求。
争议与现状:
Redlock 一经提出便引发了巨大争议,分布式系统专家 Martin Kleppmann 认为它"不安全",其依赖系统时钟的论证存在缺陷。尽管如此,Redlock 的思想仍是主流方案之一,部分 Java 客户端(如 Redisson)也提供了 RedissonRedLock 的实现。

浙公网安备 33010602011771号