Redis分布式锁问题
通过SET原子操作来设置key和过期时间
// 加锁
// 如果key不存在,那么设置它的值,否则什么也不做
SETNX lock 1
// 10s后自动过期
EXPIRE lock 10
// 2者合一,一条命令保证原子性执行
SET lock 1 EX 10 NX
问题1:无法评估准确的加锁时间(自动续期)
问题2:客户端1释放了客户端2持有的锁(保存和判断加锁者信息)
RedLock
Redis一般采用主从集群+哨兵的模式部署,好处在于,当主库异常宕机时,哨兵可以实现故障自动切换,把从库提升为主库。
主从切换场景
客户端1在主库上加锁成功
主库异常宕机,SET命令还未同步到从库上(主从复制是异步的)
从库被哨兵提升为新主库,这个锁在新的主库上丢失了
Redlock解决方案
只部署主库,主库部署多个,推荐至少5个实例,它们之间没有任何关系
步骤:
1.客户端先获取当前时间戳T1
2.客户端依次向这5个Redis实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败,就立即向下一个Redis实例申请加锁
3.如果客户端从>=3个(大多数)以上Redis实例加锁成功,则再次获取当前时间戳T2,如果T2-T1<锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
4.加锁成功,之后向全部节点发起释放锁请求
加锁失败,向全部节点发起释放锁请求
4 个重点
1. 客户端在多个 Redis 实例上申请加锁
2. 必须保证大多数节点加锁成功
3. 大多数节点加锁的总耗时,要小于锁设置的过期时间
4. 释放锁,要向全部节点发起释放锁请求
1) 为什么要在多个实例上加锁?
为了容错,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
2) 为什么大多数加锁成功,才算成功?
多个 Redis 实例一起来用,组成了一个分布式系统,只要大多数节点正常,那么整个系统依旧是可以提供服务的。
3) 为什么步骤3加锁成功后,还要计算加锁的累计耗时?
因为操作的是多个节点,所以即使大多数节点加锁成功,但如果加锁的累计耗时已经超过了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
对于Redlock的质疑
分布式系统会遇到的三座大山:NPC
N:Network Delay,网络延迟
P:Process Pause,进程暂停(GC)
C:Clock Drift,时钟漂移
Martin 用一个进程暂停(GC)的例子,指出了 Redlock 安全性问题:
1.客户端 1 请求锁定节点 A、B、C、D、E
2.客户端 1 的拿到锁后,进入 GC(时间比较久)
3.所有 Redis 节点上的锁都过期了
4.客户端 2 获取到了 A、B、C、D、E 上的锁
5.客户端 1 GC 结束,认为成功获取锁
客户端 2 也认为获取到了锁,发生「冲突」
GC 可能发生在程序的任意时刻,而且执行时间是不可控的。
当多个 Redis 节点时钟发生问题时,也会导致 Redlock 锁失效。
1. 客户端 1 获取节点 A、B、C 上的锁,但由于网络问题,无法访问 D 和 E
2. 节点 C 上的时钟「向前跳跃」,导致锁到期
3. 客户端 2 获取节点 C、D、E 上的锁,由于网络问题,无法访问 A 和 B
4. 客户端 1 和 2 现在都相信它们持有了锁(冲突)
提出 fecing token 的方案,保证正确性
1. 客户端在获取锁时,锁服务可以提供一个递增的 token
2. 客户端拿着这个 token 去操作共享资源
3. 共享资源可以根据 token 拒绝后来者的请求
反驳质疑
如果在 1-3 发生了网络延迟、进程 GC 等耗时长的异常情况,那在第 3 步 T2 - T1,是可以检测出来的,如果超出了锁设置的过期时间,那这时就认为加锁会失败,之后释放所有节点的锁就好了!
如果发生网络延迟、进程 GC 是在步骤 3 之后,也就是客户端确认拿到了锁,去操作共享资源的途中发生了问题,导致锁失效,那这不止是 Redlock 的问题,任何其它锁服务例如 Zookeeper,都有类似的问题。
1. 客户端在拿到锁之前,无论经历什么耗时长问题,Redlock 都能够在第 3 步检测出来
2. 客户端在拿到锁之后,发生 NPC,那 Redlock、Zookeeper 都无能为力
Redlock 在保证时钟正确的基础上,是可以保证正确性的。
质疑 fencing token 机制
既然服务器都有了互斥能力,那还要分布式锁干什么?
ZooKeeper分布式锁优缺点
优点
1. 不需要考虑锁的过期时间
2. 加锁失败后,可以watch等待锁释放
缺点
1. 性能不如 Redis
2. 部署和运维成本高
3. 客户端与ZooKeeper的长时间失联,锁被释放问题