我先简单的介绍一下如何使用redis单节点部署分布式锁,并说明可能存在的问题,然后引入Redlock,并主要通过分布式大神Martin和Redis作者antirez的争论来说明Redlock算法存在的问题
目前能够实现分布式锁的方案还是比较多的,例如基于数据库、zookeeper、redis等等。这里主要记录一下基于redis实现分布式锁的情况。
其实和单机上的无锁控制并发差不多,只不过锁变量需要由一个共享存储系统来维护。
主要两种方式:
用 SETNX 和 DEL 命令组合来实现加锁和释放锁操作
SETNX这个命令在执行时会判断键值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。
// 加锁
SETNX lock_key 1
// 业务逻辑
DO THINGS
// 释放锁
DEL lock_key
存在的两个问题:
如果没有设置过期时间,可能会导致某个客户端一直持有锁不释放。(例如在执行业务逻辑时发生异常)
解决办法:设置过期时间
如果设置了过期时间,那么可能会导致某个客户端持有的锁被误删(执行业务逻辑时超时,其它客户端获取锁然后删除)
解决办法:设置客户端唯一标识,然后删除时进行逻辑判断(具体实现如下一方法)
在SET指令后加上NX来代替SETNX的效果,并且SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。
执行下面的命令时,只有 key 不存在时,SET 才会创建 key,并对 key 进行赋值。另外,key 的存活时间由 seconds 或者 milliseconds 选项值来决定。
SET key value [EX seconds | PX milliseconds] [NX]
// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000
释放步骤由于是要保证多个命令的原子性,所以必须的使用Lua脚本:
//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
如果现在只用了一个 Redis 实例来保存锁变量,如果这个 Redis 实例发生故障宕机了,那么锁变量就没有了。此时,客户端也无法进行锁操作了,这就会影响到业务的正常执行。所以,我们在实现分布式锁时,还需要保证锁的可靠性。
为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
Redisson中实现了Redis分布式锁,且支持单点模式和集群模式。在集群模式下,Redisson就使用了Redlock算法。
Redlock 算法的实现需要有 N 个独立的 Redis 实例,它的流程主要可分为以下三个步骤:
客户端获取当前时间。
客户端按顺序依次向 N 个 Redis 实例执行加锁操作。
这里客户端向N个节点获取实例的操作和上面描述的单节点部署分布式锁中的第二种方法的思路一样。但需要保证的是如果某个节点获取失败,需要设置一定的超时时间(小于锁的有效时间)让客户端去获取下一个Redis实例的锁。
一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时,用来判断这个锁是否合理以及更新锁的有效时间
客户端只有在满足下面的这两个条件时,才能认为是加锁成功:
如果满足上述的两个条件,那么就会把当前锁的有效时间设置为(锁的初始有效时间 - 获取锁的时间),加锁成功!
如果没有满足,那么会向所以Redis实例发出释放锁的操作(由于可能存在网络等原因,导致在Redis实例上已经加锁成功,但是没有 发给客户端,为了保证这次情况能够正常释放锁,所以需要向所有实例发送释放锁的请求)!
上面已经介绍完Redlock的大致流程,但是这个分布式算法并不是完美的,是存在一些问题还没有解决的。
下面我对两篇文章基于Redis的分布式锁到底安全吗(上)和基于Redis的分布式锁到底安全吗(下)))中提到的一些问题做一个概述,这两篇文章描述了 分布式大神Martin 和 Redis作者 对Redlock安全性的一个争论。
Martin提出的问题1:客户端崩溃重启引发Redlock失效
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:
antirez的反驳:
antirez提出了延迟重启(delayed restarts)的概念。
也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
Martin提出的问题2:Redlock对系统记时(timing)的过分依赖,如果系统时钟发生跳跃,那么会导致Redlock失效
Martin在提到时钟跳跃的时候,举了两个可能造成时钟跳跃的具体例子:
antirez的反驳:
从antirez的回答来看,antirez是大致同意大的系统时钟跳跃会造成Redlock失效的。在这一点上,他与Martin的观点的不同在于,他认为在实际系统中是可以避免大的时钟跳跃的。当然,这取决于基础设施和运维方式。
Martin提出的问题3:客户端GC pause 或 网络延迟引发Redlock失效
Martin的blog中认为带有自动过期功能的分布式锁,必须提供某种fencing机制来保证对共享资源的真正的互斥保护。Redlock提供不了这样一种机制。
fencing机制:就是在客户端向服务器申请锁的时候,锁服务器能够给客户端按照顺序分配一个序号,在访问资源服务器时,会进行一定的判断来保证只有当当前客户端的序号大于以已访问资源服务器的最大序号时,才能够正常访问。通过这种方法来保证对共享资源的正常访问,
下面是Martin画的图:其中fencing token即为分配的序号
下面是Martin假设的时序图:
在Martin举的例子中,GC pause或网络延迟,实际发生在客户端收到请求结果之前(未获取到锁)。我们可以看一下上面Redlock的工作流程,会发现在这种情况下,它并不会满足客户端获取锁的总耗时没有超过锁的有效时间这一条件,redlock就不会认为这个锁是合理的,也就不会进行加锁,所以redlock没有失效。
但是,我们可以想象客户端1如果 GC pause 发生在客户端加锁成功后,如果我们的业务逻辑执行太长的时间,导致锁已经过期(但客户端1认为它还获取着锁)。而客户端2又获取了锁,那么他们都同时对共享资源进行操作,那么一定会存在问题。
antirez的反驳:
首先,关于fencing机制。antirez对于Martin的这种论证方式提出了质疑:既然在锁失效的情况下已经存在一种fencing机制能继续保持资源的互斥访问了,那为什么还要使用一个分布式锁并且还要求它提供那么强的安全性保证呢?另外,antirez也提到了redlock会提供 unique_value 来保证客户端的唯一性,并且提供类似于CAS的方法来原子性的访问共享资源。(这里我认为antirez也没有提供依据来保证redlock在这一方面的正确性,只是能够说明在争取锁的时候存在的并发问题)
最后:antirez和Martin也达成了一致,认为redlock解决了客户端和锁服务器之间的消息延迟(即获取锁之前产生的网络延迟等,上面已经描述如何解决),但是对于客户端和资源服务器(访问共享资源的过程)之间的延迟,antirez承认所有的分布式锁的实现,包括Redlock,是没有什么好办法来应对的。
参考资料:
Redis核心技术与实战