标签:阻塞 val 策略 可靠 间隔 系统 nes 服务 star
在多线程共享临界资源的场景下,分布式锁是一种非常重要的组件。
许多库使用不同的方式使用redis实现一个分布式锁管理。
其中有一部分简单的实现方式可靠性不足,可以通过一些简单的修改提高其可靠性。
这篇文章介绍了一种指导性的redis分布式锁算法RedLock,RedLock比起单实例的实现方式更加安全。
在介绍RedLock算法之前,我们列出了一些已经实现了分布式锁的类库供大家参考。
Redlock-rb (Ruby 实现).
Redlock-py (Python 实现)
Redlock-php (PHP 实现)
PHPRedisMutex (further PHP 实现)??
Redsync.go (Go 实现)
Redisson (Java 实现)
Redis::DistLock (Perl 实现)
Redlock-cpp (C++ 实现)
Redlock-cs (C#/.NET 实现)
RedLock.net (C#/.NET 实现
ScarletLock (C# .NET 实现)
node-redlock (NodeJS 实现)
我们将从三个特性的角度出发来设计RedLock模型:
故障切换(failover)实现方式的局限性
通过Redis为某个资源加锁的最简单方式就是在一个Redis实例中使用过期特性(expire)创建一个key, 如果获得锁的客户端没有释放锁,那么在一定时间内这个Key将会自动删除,避免死锁。
这种做法在表面上看起来可行,但分布式锁作为架构中的一个组件,为了避免Redis宕机引起锁服务不可用, 我们需要为Redis实例(master)增加热备(slave),如果master不可用则将slave提升为master。
这种主从的配置方式存在一定的安全风险,由于Redis的主从复制是异步进行的, 可能会发生多个客户端同时持有一个锁的现象。
此类场景是非常典型的竞态模型:
如何正确实现单实例的锁
在单redis实例中实现锁是分布式锁的基础,在解决前文提到的单实例的不足之前,我们先了解如何在单点中正确的实现锁。
如果你的应用可以容忍偶尔发生竞态问题,那么单实例锁就足够了。
我们通过以下命令对资源加锁
SET resource_name my_random_value NX PX 30000
SET NX 命令只会在Key不存在的时给key赋值,PX 命令通知redis保存这个key 30000ms。
my_random_value必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性。
通过下面的脚本为申请成功的锁解锁:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
如果key对应的Value一致,则删除这个key。
通过这个方式释放锁是为了避免client释放了其他client申请的锁。
例如:
通过执行上面脚本的方式释放锁,Client的解锁操作只会解锁自己曾经加锁的资源。
官方推荐通从 /dev/urandom/中取20个byte作为随机数或者采用更加简单的方式, 例如使用RC4加密算法在/dev/urandom中得到一个种子(Seed),然后生成一个伪随机流。
也可以用更简单的使用时间戳+客户端编号的方式生成随机数,
这种方式的安全性较差一些,但是对于绝大多数的场景来说也已经足够安全了。
PX 操作后面的参数代表的是这key的存活时间,称作锁过期时间。
通过上面的两个操作,我们可以完成获得锁和释放锁操作。如果这个系统不宕机,那么单点的锁服务已经足够安全,接下来我们开始把场景扩展到分布式系统。
RedLock算法介绍
下面例子中的分布式环境包含N个Redis Master节点,这些节点相互独立,无需备份。这些节点尽可能相互隔离的部署在不同的物理机或虚拟机上(故障隔离)。
节点数量暂定为5个(在需要投票的集群中,5个节点的配置是比较合理的最小配置方式)。获得锁和释放锁的方式仍然采用之前介绍的方法。
一个Client想要获得一个锁需要以下几个操作:
RedLock能保证锁同步吗?
这个算法成立的一个条件是:即使集群中没有同步时钟,各个进程的时间流逝速度也要大体一致,并且误差与锁存活时间相比是比较小的。实际应用中的计算机也能满足这个条件:各个计算机中间有几毫秒的时钟漂移(clock drift)。
失败重试机制
如果一个Client无法获得锁,它将在一个随机延时后开始重试。使用随机延时的目的是为了与其他申请同一个锁的Client错开申请时间,减少脑裂(split brain)发生的可能性。
三个Client同时尝试获得锁,分别获得了2,2,1个实例中的锁,三个锁请求全部失败。
一个client在全部Redis实例中完成的申请时间越短,发生脑裂的时间窗口越小。所以比较理想的做法是同时向N个Redis实例发出异步的SET请求。
当Client没有在大多数Master中获得锁时,立即释放已经取得的锁时非常必要的。(PS.当极端情况发生时,比如获得了部分锁以后,client发生网络故障,无法再释放锁资源。
那么其他client重新获得锁的时间将是锁的过期时间)。
无论Client认为在指定的Master中有没有获得锁,都需要执行释放锁操作。
我们将从不同的场景分析RedLock算法是否足够安全。首先我们假设一个client在大多数的Redis实例中取得了锁,
那么:
于是,最先被SET的锁将在TTL-(T2-T1)-CLOCK_DIRFT后自动过期,其他的锁将在之后陆续过期。
所以可以得到结论:所有的key这段时间内是同时被锁住的。
在这段时间内,一半以上的Redis实例中这个key都处在被锁定状态,其他的客户端无法获得这个锁。
分布式锁系统的可用性主要依靠以下三种机制
如果一直持续的发生网络故障,那么没有客户端可以申请到锁。分布式锁系统也将无法提供服务直到网络故障恢复为止。
用户使用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长时间不可用。
标签:阻塞 val 策略 可靠 间隔 系统 nes 服务 star
原文地址:https://www.cnblogs.com/ExMan/p/12189759.html