Redlock方案
前提:
- 不需要再部署从库和哨兵实例,只部署主库
- 主库要部署多个,官方推荐至少5个实例
整体流程:
- 客户端先获取 当前时间戳time1
- 客户端依次向这5个Redis实例发起加锁请求,而且每个请求会设置超时时间,注意这里的超时时间要远远的小于锁的有效时间,如果某一个实例加锁失败(包括网络超时、锁被其他人持有等各种异常情况),向下一个Redis实例申请加锁
- 如果客户端从 大于等于3个以上Redis实例加锁成功,则再次获取 当前时间戳time2 ,如果$time2-time1 < ddl$,其中ddl为锁的过期时间,认为客户端加锁成功,因为锁还未过期;否则,加锁失败
- 加锁成功,操作共享资源
- 加锁失败,向全部节点发起释放锁的请求
重点在于:
- 客户端必须在多个独立的Redis实例上加锁
本质是为了容错,因为如果部分实例宕机的话,剩余的实例加锁成功了,整个锁服务依旧可用
- 必须保证大多数的节点都能加锁成功
多个Redis实例构成了一个分布式系统,如果只存在故障节点,只要大多数节点正常,整个系统依旧是可以提供正确服务
- 大多数节点加锁的总耗时,小于锁设置的过期时间
操作的是多个节点,耗时会比单个实例的耗时多,而且网络层面也有延迟、丢包、超时等情况发生。所以如果大多数的节点加锁情况,但是加锁的累计耗时超过了锁的过期时间,有些实例的锁可能已经失效了,这个锁也没有意义
- 释放锁,要向全部节点发起释放请求
清理节点遗留的锁
对Relock的质疑
分布式专家Martin对Relock提出了质疑
分布式锁的目的是什么?
有两个目的:
一是效率,使用分布式锁的互斥功能,避免不必要地做同样地两次工作,例如一些昂贵地计算任务,而锁失效,并不会带来恶性的后果,例如发了2次邮件,问题不大
二是正确性,锁用来防止并发进程互相干扰,如果锁失效,会造成多个进程同时操作同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题。
如果是为了效率,使用单机版Redis就可以了,即使偶尔宕机/主从切换导致锁失效,都不会产生严重的后果。而Redlock依旧存在锁失效的问题,正确性也无法保证
锁在分布式系统中遇到的问题
分布式系统的异常场景
- N:
Network Delay
网络延迟 - P:
process Pause
进程暂停 GC - C:
Clock Drift
时钟偏移
下面用一个进程暂停的例子来看Redlock的安全性问题
- 客户端1请求锁定节点A~E
- 客户端1拿到所以后,进入GC,时间维持久
- 所有Redis节点上的锁都过期了
- 客户端2获取到了A~E上的锁
- 客户端1的GC结束了,也认为自己成功获取到锁了
- 客户端2也同样认为 发生了冲突
假设时钟正确并不合理
当多个Redis节点时钟发生问题时,也会导致Redlock锁失效
- 客户端1获取节点A~C上的锁,但是因为网络问题,无法访问D和E
- 节点C上的时钟向前跳跃,导致锁到期
- 客户端2获取节点C~E上的锁,因为网络问题,无法访问A和B
- 客户端1和客户端2都觉得自己加锁成功
机器的时钟发生错误,主要有以下情况
- 系统管理员手动修改了机器时钟
- 机器时钟在同步NTP时间时,发生了大的跳跃
fencing token方案
Martin 提出一种被叫作 fencing token 的方案,保证分布式锁的正确性。
流程如下:
- 客户端在获取锁时,锁服务可以提供一个递增的 token
- 客户端拿着这个 token 去操作共享资源
- 共享资源可以根据 token 拒绝后来者的请求
这样是建立在异步模型上的,无论哪种情况发生,都可以保证安全性。
他还表示,一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的性能,而不会影响它的正确性