Redis分布式锁是否是安全的?

Redis锁在面试中是Redis绕不开的话题,关于Redis锁,网上很多文章,大多都是这个方案:

1、单机Redis

2、RedLock

3、Redission分布式锁

本文基于这三个点,延伸出几个问题,同时介绍下Martin和RedLock实现作者Salvatore的论点。

当然分布式锁并不只限于这两种,还有基于ZooKeeper的分布式锁的实现、Chubby的分布式锁、Mysql分布式锁、基于 Etcd、Hazelcast 分布式锁的等等,其中关于基于ZooKeeper的分布式锁的内容将放在下一次进行讲解。

一 单机Redis

单节点的分布式锁,网上已经有很多介绍了,很容易找到这样一句代码:

SET key value NX PX30000

如果上面的指令成功执行,那也就意味着获得了锁,此时可以对共享资源进行操作,如果指令执行失败,则意味着获取锁失败。

虽然这个指令非常简单,其中的几个问题还是值得思考的

1   px有必要吗。为什么?

首先想如果没有PX,会发生什么情况,假设一个进程A获得了这把锁,但是这个进程挂了,此时无法释放这把锁,那是不是这个进程就会一直持有这把锁呢?这样就导致其他进程无法获得共享资源。

所以需要这个PX对锁设置一个过期时间,防止其他进程无法获得锁。

思考另外一个问题,由于我们的进程运行在机器上,而机器运行的状态我们无法确定。进程A此时获得了锁,此时可能因为GC停顿导致了程序的卡顿,亦或是调用其他接口因为拥塞网络问题导致请求变慢,也许其他进程发送SIGSTOP信号,总之此时因为一些其他情况造成进程暂停,又有一个进程B获得了锁,如果此时进程A的过期时间到达,使用DEL删除这个记录,那么就会把进程B所拿到的锁进行删除,这样就会产生一个不安全的事故,所以PX的设置还是有必要的。

(超时后只有对key执行DEL命令或者SET命令或者GETSET时才会清除)

进程A的Stop-The-World GC时间结束,发现锁过期,反手DEL删除了锁,此时也就是删除了进程B所持有的锁。

那么如何解决这个问题呢,Martin给了一个解决方案,采用一个单调递增的数字来确定延迟到来的请求,其实也就相当于版本号,如果接收到低版本的请求,便拒绝就好了。

但是这样还是会有一个问题:如果因为两个客户端都发生了GC,但是版本号到达的顺序是正确的,那么是不是又有问题了?

2   过期时间设置多少合适,超时之后,共享资源是不是失去保护了呢?

Redis锁的过期时间设置,也是可以思考的点,例如Redis锁过期了,但是业务逻辑却没有执行完成,那该怎么办呢?

如果把Redis锁的过期时间再设置长一点怎么样?这样还是有问题,如果此时业务逻辑执行的非常快,过长的Redis锁过期时间的设置反而会降低效率。

如果此时客户端因为一些原因导致锁过期了,那么这里的客户端访问接下来的共享资源还安全吗?答案是否定的,共享资源此时已经失去了保护。

3   随机字符串有必要吗,为什么?

首先随机字符串是必要的,这保证了客户端释放所必须是自己所持有的锁。我们可以假设一个场景,如果这个字符串是一个固定不变的字符串,那么还是会产生类似问题一的结果:A所属的锁被B释放。因此这个随机字符串还是有必要的。

4   指令可以拆成两条指令实现吗,缺点有什么?

加锁的指令确实可以通过两条指令进行实现,例如:

SETNXkey valueEXPIRE key 30

这样做弊端很明显,就是没有原子操作,另外还有一个需要说的点,对于SETNX指令,在Redis官方文档上SET指令介绍有这样一句话:

大意是SET指令可以替换SETNX、SETEX、PSETEX,因此在Redis的将来版本中,这些命令可能会被弃用并最终删除。

5、释放锁的方式怎么做?

释放锁的步骤其实包含三部分:获取、判断和删除,而这三个步骤如果想要一次执行,那必须要保证原子性,可以想想为什么要保证删除锁的步骤是原子性?

如果这三个步骤不是原子性,那么和第一个问题一样,将会发生下列执行情况:

进程A删除了属于进程B的同一个共享资源的锁:

由于Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断,这便是删除锁的Lua脚本。

if redis.call('get', KEYS[1]) == ARGV[1]     then         return redis.call('del', KEYS[1])     else         return 0 end

二 主从部署Redis

思考另外一个问题,Redis如果是单机部署,有什么问题呢?

首先单点部署自然有其不稳定性,主从部署可以提高其可用性,但是就算拥有了failover故障转移机制,对于单节点Redis锁还是一样有问题,这也是单节点Redis的分布式锁所无法解决的问题。

主从部署有什么问题呢

假设Redis此时是单节点部署,这个节点突然宕机了,那么其他链接这个服务的客户端都没有办法获得这个锁,因此为了提高单节点的可用性,可以给这个单节点部署一个Slave节点,这样一旦当主节点不可用的时候,通过failover自动转移机制,服务切换到Slave节点上。

但是我们也知道,Redis服务切换这个过程是异步复制的,如果此时存储Key的Master节点宕机,而存储这个锁的Key没有同步到Slave节点节点上,此时Slave因为failover自动转移机制升级到Master节点,则其他客户端一样可以获得同一个共享资源的锁。

因为主从复制集群不能保证安全属性(即不能保证一个时刻,只有一个客户端获得锁),所以这便是的Redis主从集群的问题所在。

三 RedLock

基于Redis单节点以及主从部署的Redis分布式锁存在的问题,因此催生了RedLock的实现,那么RedLock又是什么呢。

3.1 RedLock介绍

在Redis的集群模式下,假设我们有奇数个Redis节点(这里假设有五个),我们部署在不同的机器上,保证这几台机器不会同时的宕机,获取锁和释放锁的时候都由客户端去通知这里的五个节点,最终结果交给客户端,由客户端决定是否加锁成功。

3.2 RedLock获取锁

那么此时获取锁的步骤就是:

1、获取当前的系统时间,以毫秒为单位。

2、依次向N个Redis实例节点执行获取锁的操作,使用和单节点的获取锁操作一样,使用相同的key和随机值获取锁。

      向单个节点获取锁的时候,应该设置一个超时时间和一个网络链接,这个时间小于锁的失效时间。(避免Redis挂了,客户端还在等锁的结果,如果超时时间没有获取到,应该立刻获取下一个Redis实例)

3、计算获取锁的时间花费了多少(使用当前时间减去第一步的系统时间)。获取锁成功的条件是,客户端从大于一半的Redis节点获取到锁的时候,并且使用的时间小于锁的有效时间(lock validity time)这样才是获取成功,否则是失败。

4、重新计算获取锁的有效时间:等于最初的有效时间减去第三部计算出来获得锁的时间。

5、如果最终获取失败,客户端在所有Redis节点发起释放锁的操作(包括没有加锁成功的节点,使用Lua脚本解锁)

这里我们也可以看到一个基础问题,那就是RedLock锁很依赖每个Redis节点的时间信息,当然在Redis官网里面也写着:

算法基于这样一个假设:虽然多个进程之间没有时钟同步,但每个进程都以相同的时钟频率前进,时间差相对于失效时间来说几乎可以忽略不计。这种假设和我们的真实世界非常接近:每个计算机都有一个本地时钟,我们可以容忍多个计算机之间有较小的时钟漂移。

3.3 RedLock释放锁的步骤

删除锁的操作就是上面第五步,客户端向所有Redis节点发起释放锁的操作(即使这些Redis实例没有加锁成功)

这里有一个疑问:为什么要向所有Redis实例发送释放锁的过程,只需要发送已经加锁的实例不就好了?

想象一个这样的情况,我们的加锁请求在一个Redis实例上已经加锁成功了,但是由于Redis实例和客户端是网络传输,此时响应包在网络中传输丢失,那么在客户端看来就是失败了,但是在Redis集群内部则认为这个节点加锁成功。因此我们必须要向所有Redis节点发送释放锁的操作。

3.4 客户端的阻塞导致锁过期

在上面的基于单节点Redis实现的分布式锁导致过期的情况,有一个这样的情况,如果是因为客户端的问题导致锁过期,此时共享资源就已经不安全了,那么在RedLock里面是否有所改进呢?

答案是没有,因为在获得锁的过程中的第四步,重新计算锁的有效时间,此时因为客户端阻塞导致锁过期,而锁的有效时间又过短,那么共享资源还是不安全的了。

3.5 RedLock争论的点

对于RedLock的实现,也是存在很多开发者和研究人员对其的争论,争论的便是RedLock是否是安全的

这里我们先假设一种情况,如果此时有五个Redis节点,客户端A和客户端B获取同一个共享资源:

1、客户端A锁住了1、2、3号Redis节点,超过一半获得锁成功。

2、此时1号节点因为崩溃重新启动,由于RDB或者AOF的恢复,在1号节点上加锁信息没有恢复过来。

3、客户端B锁住了1、4、5号节点,获取锁成功。

那么此时客户端A和客户端B都可以对共享资源进行操作。

当然Redis的作者antirez也考虑到了这一种情况,antirez采用了延迟加载来去解决这个问题:

延迟重启也就是一个Redis节点挂了,先不重启,等到大于锁的有效时间的时候才重启这个节点,这样就不会影响现有锁了。

3.6 Martin和Salvatore的争论

有名的事件便是Martin和Salvatore的争论,这个争论的事情又是怎么样产生的呢?

redis RedLock官网

在这个Redis官网里面有一段话,如果你使用的是分布式系统,你的观点和意见很重要。

Martin Kleppmann是一个专门研究分布式的开发人员,正好研究了RedLock,于是Martin Kleppmann于2016年2月8日发布的一篇文章"How to do distributed locking",分析了RedLock在锁的安全上存在一些问题

martin 分析RedLock

“neither fish nor fowl”非驴非马

这便是Martin对于RedLock算法的评价。

2016年2月9日Redlock的原始作者 Salvatore对本文发表了反驳"Is Redlock safe?"来反驳Martin Kleppmann的论点。据Martin说他在公开那篇文章的之前一周,已经发送给Salvatore进行查看和讨论,但是公开发表的第二天Salvatore就发送了这篇反驳的文章。

antirez 的反驳

随后这场争论也引发了许多人的讨论,而这其中的内容,不得不说真的是高手过招,有目共赏,值得反复观看。

Martin文章

Martin Kleppmann的文章前半部分涉及了很多分布式基础性的问题,对于一个程序员来说,非常值得一看。

首先Martin认为分布式锁的目的主要有两个:效率正确性

效率:当我们使用分布式锁的场景是效率,也就是说为了避免两次操作的重复性时,我们完全不必要使用Redis的RedLock的方式,首先RedLock的成本并不低,多个Redis实例的搭建只是为了获得锁去减少重复操作,未免显得有些浪费,此时可以使用单实例Redis来进行减少重复操作的目的。

正确性:保证分布式并发程序的执行顺序性是分布式锁的另一个核心目的。

Martin的前半部分叙述内容就相当于和之前第一部分所讲的内容重复,这里便不再叙述了。

第二部分内容是针对于让RedLock失效的讨论。Martin认为RedLock强依赖系统时钟,一旦这个时钟不准确,这个算法的安全性也就无法保证。换句话说分布式的情况多种多样,对于一个好的分布式算法来说,安全性应该是很重要的,它不应该因为其他因素而导致系统的安全性丢失,最多是不能给出结果而已(这里可以看下Paxos协议)。

Salvatore反驳

Salvatore对其的反驳也是非常有意思,他认为通过恰当的运维就可以避免始终发生大的跳动,但是关于客户端长时间的暂停的行为,RedLock已经在设计的时候避免了。

Salvatore认为如果客户端发生暂停,那么在RedLock获得锁的时候,消耗了很长时间,不会让客户端拿到一个它认为有效,但是实际上已经无效的锁(当然基于系统时钟没有发生跳跃的前提)。

回忆起RedLock加锁的过程:

1、获取当前时间

2、向Redis集群实例发起请求获取锁

3、再获取当前时间

4、两时间相减,查看获取锁的时间,查看我们是否足够快获得锁

5、客户端拿到锁做一些工作

Note steps 1 and 3. Whatever delay happens in the network or in the processes involved, after acquiring the majority we *check again* that we are not out of time. 

Salvatore认为这在第四步的时候就避免了客户端花费时间过长导致的锁过期,因为都会再次检查。

The delay can only happen after steps 3, resulting into the lock to be considered ok while actually expired, that is, we are back at the first problem Martin identified of distributed locks where the client fails to stop working to the shared resource before the lock validity expires. 

但是延迟至发生在第三步之后,这将会导致锁被认为是有效的,而实际已经过期了,也就是Martin指出的第一个问题上,客户端没能够在锁的有效期过期之前完成对共享资源的操作

Let me tell again how this problem is common with *all the distributed locks implementations*, and how the token as a solution is both unrealistic and can be used with Redlock as well.

让我再说一下,这个问题对于所有的分布式锁的实现是普遍存在的,而且基于token的这种解决方案是不切实际的,又可以和Redlock一起用。

其实antirez也认同时钟跳跃会发生延迟,导致锁的不安全性,但是适当运维不让时钟发生跳跃的大前提下,我RedLock算法是安全的,在设计的时候就考虑了这些,如果你说第三步之后发生的问题,那是其他分布式锁普遍存在的。

当然这些争论只开头,后面还有很多大牛参与进去,具体的内容在这里,有兴趣的小伙伴可以继续探讨学习:

针对Martin的blog的讨论

针对antirez的blog的讨论

四 Redission分布式锁

之前所说对于锁的过期时间的设置,在单机情况下的设置总有其不方便之处,那么如果有一个实现,可以对Redis锁的过期时间进行修改,到了过期时间进行自动续期就好了,那就会方便很多,不会去根据业务场景去设置这个过期时间。

而基于Redission实现的分布式锁便解决了这个问题,Redssion是在加锁成功之后,注册一个定时任务去监听这个锁,每隔一段时间去检查这个锁,如果还持有这个锁,那就修改过期时间,对这个过期时间进行续期。这个延长过期时间的机制也叫做watchdog“看门狗”机制。

加锁的过程实际上也是调用了一个Lua脚本,这里只是画图表明了这个Lua脚本加锁的过程,有兴趣可以查看下Redisson加锁的Lua脚本源码。

释放锁的过程其实最后也是调用了一个Lua脚本,这里也是画出了Lua脚本释放的过程。

五 总结

最后我这边还是比较认同Martin的看法,借助他针对于分布式锁使用场景的结论,给出一个总结:

如果为了效率而使用分布式锁,并且允许锁的偶然失效,那么使用简单实现且效率高的单节点Redis锁。

如果为了正确性保证绝对不失效的场景下,那么不要使用RedLock的实现,尽量采用支持事务的数据库亦或是Zookeeper实现的分布式锁。

当然对于分布式的情况多种多样,客户端和服务器之间的延迟,会对所有分布式锁的实现都会带来影响,这也是不可避免的,也是值得我们思考的。

另外Martin在这个争论的最后也发表了一个评论,在这里也分享给大家:

 I don’t care who is right or wrong in this debate — I care about learning from others’ work, so that we can avoid repeating old mistakes, and make things better in future. So much great work has already been done for us: by standing on the shoulders of giants, we can build better software.

在这场辩论中,我不在乎谁对谁错,我在乎的是从别人的工作中学到东西,这样我们就可以避免重蹈覆辙,并且使未来变得更好。这么多伟大的工作前人已经为我们做了:站在巨人的肩膀上,我们可以建立更好的软件。

其实到这里还是有些意犹未尽,脑子里也会有很多争论的声音,不过学习似乎就是这样,有很多问题得多问问自己为什么,探讨其中的原理,便能学到更多。

如果觉得不错,欢迎关注公众号: Bee风

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容