欢迎关注我的同事犀利豆。
我们知道,锁的本质是互斥,即任何时候一个锁最多只能被一个客户端持有。用Redis来实现一个分布式锁,最简单的方法就是在创建一个键值,释放锁的时候,将键值删除。这个原理看似非常简单,但其实实现起来的细节还是非常多的,下面我们就来具体看看。
基于Redis的分布式锁
我们直接基于Redis的SETNX实现一个简单的锁。
锁的获取
SET resource_name your_random_value NX PX timeout
锁的释放
if redis.call("get", KYES[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这个简单的分布式锁中,有如下几个细节需要注意:
-
使用SETNX,保证查询和写入这两个步骤是原子的。
-
获取锁的时要设置超时时间,这是为了防止客户端在取得锁之后崩溃,或者网络出现问题,这样锁会被一直持有,就是死锁了。而且设置超时时间要和SETNX合为一起的原子操作,防止SET之后没有来得及设置超时时间,客户端就挂了。
-
在释放的时候要判断GET key == ARGV[1],这是为了保证锁只能是被它的持有者释放。我们假设不进行这一步校验:
- 客户端A获取锁之后,线程挂起了,时间大于锁的过期时间。
- 锁过期后,客户端B又获取了锁。
- 客户端A苏醒后,处理完相关事件,向Redis发起del命令,锁被释放(释放的是客户端B持有的锁)。
- 客户端C获取锁。这个时候,系统中就有B、C两个客户端同时持有锁。
造成这个问题的关键,就是客户端B持有的锁被客户端A释放了。
-
锁的释放必须使用lua脚本,保证操作的原子性。锁的释放包含了GET、判断、DEL三个步骤,如果不能保证三步的原子性,分布式锁就会有并发问题。
注意了以上细节,一个单点Redis的分布式锁就算完成了。
在这个分布式锁中,使用的是一个单点Redis,Redis是master-slave架构的,master出现故障的时候切换到slave就好。可Redis的数据同步却是异步的……
对于分布式锁这种高频修改的数据来说,就会有可能出现以下情况:
- 客户端A在master上拿到了锁。
- 在master将数据同步到slave之前,master宕机。
- 客户端B就从slave上又一次拿到了锁,这样又有两个客户端同时持有锁了。
这样由于master宕机,又造成了同时多人持有锁。如果你的系统可以接受短时间内多人持有锁,那么这个简单的方案就可以解决问题。
如果一定要解决这个问题,Redis官方提供了一个RedLock解决方案。
RedLock介绍
Redis的作者antirez为了解决单点Redis锁的问题,提出了RedLock这样一个非常巧妙又简单的解决方案。
RedLock的思想就是分布式一致性算法的核心概念——多数派思想。过程如下:
- 我们需要N个Redis节点(N为大于2的奇数),这些Redis节点相互之间是完全独立,没有数据同步的。
- 客户端使用上文提到的方法,尝试依次从所有Redis节点中获取锁,
- 如果获取到了半数以上节点的锁(>=N/2+1),并且获取锁所消耗的时间小于锁的有效时间,则认为客户端成功获取了锁,锁的真实过期时间=有效时间-获取锁所消耗的时间。
- 如果获取到锁的数量不足半数以上,或是在锁的有效时间内没有获取到足够多的锁,则认为获取失败,释放掉所有已获取到的锁。
- 获取到锁后的释放就很简单了,依次对所有节点释放即可。
同时需要注意以下几个细节:
- 获取锁的重试时间应该是一个范围内的随机时间间隔,而非固定时间间隔,这样可以最大限度地避免多个客户端同时向Redis集群发起获取锁的请求,引起竞争,同时获取到不足半数的锁。
- 如果某个Redis节点发生故障,恢复的时间间隔应该大于锁的有效时间。
- 假设有A、B、C三个Redis节点。
- 客户端1获取到A、B两个锁。
- 这时候B宕机,数据清空。
- B节点恢复。
- 这时候客户端2尝试从新获取锁,它获取到了B、C两个节点。
- 这样就又有两个客户端同时持有锁了。
所以如果节点恢复的时间间隔大于锁的有效时间,就能够避免这样的事情发生。
了解了RedLock这个简单又精妙的实现以后,会发现其实大多数的分布式系统其实原理很简单,但是为了保证分布式系统的可靠性,需要注意很多的细节,琐碎异常。
关于RedLock的争论
RedLock的设计简单又精妙,它就是完美的分布式锁吗?
在RedLock的官方文档最后你会发现这样一句话:
Analysis of RedLock
Martin Kleppmann analyzed Redlock here. I disagree with the analysis and posted my reply to his analysis here.
这里面藏着一个不得了的世界,两位高手的辩论堪称精彩绝伦,所以忍不住拿出来与大家分享。
Martin的批评
Martin上来就问,我们要分布式锁来干啥呢?两个原因:
- 提升效率,用锁来保证一个任务没必要被执行两次。(比如很昂贵的计算)
- 保证正确,用锁来保证任务按照正常步骤执行,防止两个节点同时操作一份数据,造成数据冲突和错误。
对于第一种原因,我们对锁是有一定宽容度的,就算发生了两个节点同时工作,对系统的影响也仅仅是多付出了一些计算成本,没什么额外影响。这个时候,使用单点Redis锁就能很好地解决问题,没必要使用RedLock,维护那么多实例,提升系统维护成本。
对于第二种原因,在对正确性有严格要求的场景下(比如订单或消费),就算使用了RedLock算法,仍然不能保证锁的正确性。
我们来分析一下RedLock有什么缺陷吧!

作者Martin给出这张图。我们知道RedLock为了防止死锁,锁都是有过期时间的。就是这个过期时间,被Martin抓住了小辫子。
- Client 1在持有锁的时候,发生了一次时间很长的FGC,超过了锁的过期时间,锁就过期了。
- Client 2获取了锁,并且提交了数据。
- 这时候Client 1从FGC中苏醒过来了,又一次提交数据。
这还了得,数据就发生了错误。RedLock只保证了锁的可用性,并没有保证锁的正确性。
或许大家能想到的是:Client 1在提交数据之前先检查一下锁的持有者是不是自己,不就能解决这个问题么?其实答案是否定的,因为FGC可能发生在任何时候,如果FGC发生在检查之后,一样会有这个问题。那换一个没有GC的语言呢?答案还是否定的,FGC只是造成系统停顿的原因之一,IO或者网络波动或堵塞都可能造成系统停顿。
文章看到这里,我都绝望了……还好Martin给出了一个解决方案:为锁增加一个token-fencing。

- 获取锁的时候,还要获取一个递增的token。上图中Client 1获取到了token=33的锁。
- Client 1发生FGC的时候,Client 2获取到了token=34的锁。
- 在提交数据的时候,需要检查token的值,如果token值小于上一次提交的值,请求就会被拒绝。
我们可以理解这个token-fencing就是一个乐观锁,一个CAS。
Martin还指出,RedLock是一个严重依赖系统时钟的分布式系统。还是这个过期时间的小辫子,如果某个Redis Master的系统时间发生了错误,造成了它持有的锁提前被过期释放。
- Client 1从A、B、C三个Redis节点中取得了A、B两个节点的锁,我们认为它持有了锁。
- 这个时候Client B的系统时间比别的系统走得快了,B就会先于其他的节点将锁提前释放掉。
- Client 2可以从B、C两个节点获取锁,系统中又有两个客户端同时持有锁了。
Martin还提出了一个相当重要的关于分布式系统的设计要点:
好的分布式系统应当是异步的,且不能以时间作为安全保障的。因为在分布式系统中有会程序暂停,网络延迟,系统时间错误,这些因素都不能影响分布式系统的安全性,只能影响系统的活性(liveness property)。换句话说,就是在极端情况下,分布式系统顶多在有限的时间内不能给出结果,但是不能给出错误的结果。
我们总结一下Martin对RedLock的批评:
- 对于提升效率的场景,RedLock太重。
- 对于正确性要求极高的场景,RedLock并不能够保证正确性。
看完之后感觉醍醐灌顶,简直太精彩了。
antirez的回应
antirez看到Martin的文章以后,就写了一篇文章回应。
他首先总结了Martin对于RedLock的批评:
- 分布式锁具有一个自动释放功能。锁的互斥性,只在过期时间之内有效,锁过期释放后就会造成多个客户端同时持有锁。
- RedLock整个系统是建立在一个实际系统无法保证的系统模型上。这里主要是假设系统时间是同步且可信的。
对于第一个问题,antirez洋洋洒洒写了很多,仔细看半天,也没有解决我心中的疑问。
回顾一下RedLock获取锁的步骤:
- 获取开始时间
- 去各个节点获取锁
- 检查获取锁所消耗的时间是否小于锁的有效时间
- 获取成功
如果程序在1、2步发生了阻塞,那么RedLock可以感知到锁已经过期,没有任何问题。可如果在第3步之后发生了阻塞,该怎么办?
答案是,其他具有自动释放机制的分布式锁都没办法解决这个问题。
对于第二个问题,antirez认为系统时间的阶跃主要来自两个方面:
- 人为修改
- 从NTP服务收到了一个跳跃式的时钟更新。
对于人为修改,能说啥呢?人非要作死搞破坏,拦不住的,避免不了。
NTP收到一个阶跃时钟更新,对于这个问题,需要通过运维来保证。需要将阶跃的时间更新到服务器的时候,应当采取小步快跑的方式。多次修改,每次更新时间尽量小。
所以,严格来说RedLock确实建立在了时间可信的模型上,理论上时间也会发生错误,但是在现实中,良好的运维和一些工程机制是可以最大限度保证时间可信。
最后,antirez还打出了一个暴击,既然Martin提出的系统使用fencing token来保证数据的顺序处理,还需要RedLock,或者别的分布式锁干啥呢?
回顾
两个人在博客上互怼得你来我往,感觉就像是看武侠戏里面的高手过招,相当爽快。二人思路清晰,Martin上来就看到RedLock的死穴,一顿猛打,antirez见招拆招成功化解。
至于二人谁对谁错?
我觉得,每一个系统设计都有自己的侧重和局限,工程和设计也不是完美的,在现实工程中也不存在完美的解决方案。我们应当深入了解其中的原理,了解解决方案的优缺点,明白选用方案的局限性,以及是否可以承受方案的局限性所带来的后果。
架构和设计本来就是一门平衡的艺术。
网友评论