Redlock红锁总结

 redis常用的方式有单节点、主从模式、哨兵模式、集群模式。

单节点在生产环境基本上不会使用,因为不能达到高可用,且连RDB或AOF备份都只能放在master上,所以基本上不会使用。
另外几种模式都无法避免两个问题:
1、异步数据丢失。
2、脑裂问题。

所以redis官方针对这种情况提出了红锁(Redlock)的概念。
假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。

RedLock算法介绍
下面例子中的分布式环境包含N个Redis Master节点,这些节点相互独立,无需备份。这些节点尽可能相互隔离的部署在不同的物理机或虚拟机上(故障隔离)。
节点数量暂定为5个(在需要投票的集群中,5个节点的配置是比较合理的最小配置方式)。获得锁和释放锁的方式仍然采用之前介绍的方法。
一个Client想要获得一个锁需要以下几个操作:
得到本地时间
Client使用相同的key和随机数,按照顺序在每个Master实例中尝试获得锁。在获得锁的过程中,为每一个锁操作设置一个快速失败时间(如果想要获得一个10秒的锁, 那么每一个锁操作的失败时间设为5-50ms)。
这样可以避免客户端与一个已经故障的Master通信占用太长时间,通过快速失败的方式尽快的与集群中的其他节点完成锁操作。
客户端计算出与master获得锁操作过程中消耗的时间,当且仅当Client获得锁消耗的时间小于锁的存活时间,并且在一半以上的master节点中获得锁。才认为client成功的获得了锁。
如果已经获得了锁,Client执行任务的时间窗口是锁的存活时间减去获得锁消耗的时间。
如果Client获得锁的数量不足一半以上,或获得锁的时间超时,那么认为获得锁失败。客户端需要尝试在所有的master节点中释放锁, 即使在第二步中没有成功获得该Master节点中的锁,仍要进行释放操作。

RedLock能保证锁同步吗?
这个算法成立的一个条件是:即使集群中没有同步时钟,各个进程的时间流逝速度也要大体一致,并且误差与锁存活时间相比是比较小的。实际应用中的计算机也能满足这个条件:各个计算机中间有几毫秒的时钟漂移(clock drift)。
失败重试机制
如果一个Client无法获得锁,它将在一个随机延时后开始重试。使用随机延时的目的是为了与其他申请同一个锁的Client错开申请时间,减少脑裂(split brain)发生的可能性。
三个Client同时尝试获得锁,分别获得了2,2,1个实例中的锁,三个锁请求全部失败。
一个client在全部Redis实例中完成的申请时间越短,发生脑裂的时间窗口越小。所以比较理想的做法是同时向N个Redis实例发出异步的SET请求。
当Client没有在大多数Master中获得锁时,立即释放已经取得的锁时非常必要的。(PS.当极端情况发生时,比如获得了部分锁以后,client发生网络故障,无法再释放锁资源。
那么其他client重新获得锁的时间将是锁的过期时间)。
无论Client认为在指定的Master中有没有获得锁,都需要执行释放锁操作。

RedLock算法安全性分析
我们将从不同的场景分析RedLock算法是否足够安全。首先我们假设一个client在大多数的Redis实例中取得了锁,
那么:
每个实例中的锁的剩余存活时间相等为TTL。
每个锁请求到达各个Redis实例中的时间有差异。
第一个锁成功请求最先在T1后返回,最后返回的请求在T2后返回。(T1,T2都小于最大失败时间)
并且每个实例之间存在时钟漂移CLOCK_DRIFT(Time Drift)。
于是,最先被SET的锁将在TTL-(T2-T1)-CLOCK_DIRFT后自动过期,其他的锁将在之后陆续过期。
所以可以得到结论:所有的key这段时间内是同时被锁住的。
在这段时间内,一半以上的Redis实例中这个key都处在被锁定状态,其他的客户端无法获得这个锁。

锁的可用性分析(Liveness)
分布式锁系统的可用性主要依靠以下三种机制
锁的自动释放(key expire),最终锁将被释放并且被再次申请。
客户端在未申请到锁以及申请到锁并完成任务后都将进行释放锁的操作,所以大部分情况下都不需要等待到锁的自动释放期限,其他client即可重新申请到锁。
假设一个Client在大多数Redis实例中申请锁请求所成功花费的时间为Tac。那么如果某个Client第一次没有申请到锁,需要重试之前,必须等待一段时间T。T需要远大于Tac。 因为多个Client同时请求锁资源,他们有可能都无法获得一半以上的锁,导致脑裂双方均失败。设置较久的重试时间是为了减少脑裂产生的概率。
如果一直持续的发生网络故障,那么没有客户端可以申请到锁。分布式锁系统也将无法提供服务直到网络故障恢复为止。
性能,故障恢复与文件同步
用户使用redis作为锁服务的主要优势是性能。其性能的指标有两个
加锁和解锁的延迟
每秒可以进行多少加锁和解锁操作
所以,在客户端与N个Redis节点通信时,必须使用多路发送的方式(multiplex),减少通信延时。
为了实现故障恢复还需要考虑数据持久化的问题。
我们还是从某个特定的场景分析:
<code>
Redis实例的配置不进行任何持久化,集群中5个实例 M1,M2,M3,M4,M5
client A获得了M1,M2,M3实例的锁。
此时M1宕机并重启。
由于没有进行持久化,M1重启后不存在任何KEY
client B获得M4,M5和重启后的M1中的锁。
此时client A 和Client B 同时获得锁
</code>
如果使用AOF的方式进行持久化,情况会稍好一些。例如我们可以向某个实例发送shutdown和restart命令。即使节点被关闭,EX设置的时间仍在计算,锁的排他性仍能保证。
但当Redis发生电源瞬断的情况又会遇到有新的问题出现。如果Redis配置中的进行磁盘持久化的时间是每分钟进行,那么会有部分key在重新启动后丢失。
如果为了避免key的丢失,将持久化的设置改为Always,那么性能将大幅度下降。
另一种解决方案是在这台实例重新启动后,令其在一定时间内不参与任何加锁。在间隔了一整个锁生命周期后,重新参与到锁服务中。这样可以保证所有在这台实例宕机期间内的key都已经过期或被释放。
延时重启机制能够保证Redis即使不使用任何持久化策略,仍能保证锁的可靠性。但是这种策略可能会牺牲掉一部分可用性。
例如集群中超过半数的实例都宕机了,那么整个分布式锁系统需要等待一整个锁有效期的时间才能重新提供锁服务。
使锁算法更加可靠:锁续
如果Client进行的工作耗时较短,那么可以默认使用一个较小的锁有效期,然后实现一个锁续约机制。
当一个Client在工作计算到一半时发现锁的剩余有效期不足。可以向Redis实例发送续约锁的Lua脚本。如果Client在一定的期限内(耗间与申请锁的耗时接近)成功的续约了半数以上的实例,那么续约锁成功。
为了提高系统的可用性,每个Client申请锁续约的次数需要有一个最大限制,避免其不断续约造成该key长时间不可用。

posted @ 2021-04-16 10:47  郭慕荣  阅读(1642)  评论(0编辑  收藏  举报