网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词“Redis 分布式锁”随便到哪个搜索引擎上去搜索一下就知道了。这些文章的思路大体相近,给出的实现算法也看似合乎逻辑,但当我们着手去实现它们的时候,却发现如果你越是仔细推敲,疑虑也就越来越多。 实际上,大概在一年以前,关于Redis分布式锁的安全性问题,在分布式系统专家Martin Kleppmann和Redis的作者antirez之间就发生过一场争论。由于对这个问题一直以来比较关注,所以我前些日子仔细阅读了与这场争论相关的资料。 这场争论的大概过程是这样的: 为了规范各家对基于Redis的分布式锁的实现,Redis的作者提出了一个更安全的实现,叫做Redlock。有一天,Martin Kleppmann写了一篇blog,分析了Redlock在安全性上存在的一些问题。然后Redis的作者立即写了一篇blog来反驳Martin的分析。但Martin表示仍然坚持原来的观点。随后,这个问题在Twitter和Hacker News上引发了激烈的讨论,很多分布式系统的专家都参与其中。 对于那些对分布式系统感兴趣的人来说,这个事件非常值得关注。不管你是刚接触分布式系统的新手,还是有着多年分布式开发经验的老手,读完这些分析和评论之后,大概都会有所收获。要知道,亲手实现过Redis Cluster这样一个复杂系统的antirez,足以算得上分布式领域的一名专家了。但对于由分布式锁引发的一系列问题的分析中,不同的专家却能得出迥异的结论,从中我们可以窥见分布式系统相关的问题具有何等的复杂性。实际上,在分布式系统的设计中经常发生的事情是:许多想法初看起来毫无破绽,而一旦详加考量,却发现不是那么天衣无缝。 下面,我们就从头至尾把这场争论过程中各方的观点进行一下回顾和分析。在这个过程中,我们把影响分布式锁的安全性的那些技术细节展开进行讨论,这将是一件很有意思的事情。这也是一个比较长的故事。当然,其中也免不了包含一些小“八卦”。 Redlock算法 就像本文开头所讲的,借助Redis来实现一个分布式锁(Distributed Lock)的做法,已经有很多人尝试过。人们构建这样的分布式锁的目的,是为了对一些共享资源进行互斥访问。 但是,这些实现虽然思路大体相近,但实现细节上各不相同,它们能提供的安全性和可用性也不尽相同。所以,Redis的作者antirez给出了一个更好的实现,称为Redlock,算是Redis官方对于实现分布式锁的指导规范。Redlock的算法描述就放在Redis的官网上: https://redis.io/topics/distlock 在Redlock之前,很多人对于分布式锁的实现都是基于单个Redis节点的。而Redlock是基于多个Redis节点(都是Master)的一种实现。为了能理解Redlock,我们首先需要把简单的基于单Redis节点的算法描述清楚,因为它是Redlock的基础。 基于单Redis节点的分布式锁 首先,Redis客户端为了获取锁,向Redis节点发送如下命令: SET resource_name my_random_value NX PX 30000 上面的命令如果执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果上面的命令执行失败,则说明获取锁失败。 注意,在上面的SET命令中:
最后,当客户端完成了对共享资源的操作之后,执行下面的Redis Lua脚本来释放锁: if redis.call("get",KEYS[1]) == ARGV[1] then 这段Lua脚本在执行的时候要把前面的my_random_value作为ARGV[1]的值传进去,把resource_name作为KEYS[1]的值传进去。 至此,基于单Redis节点的分布式锁的算法就描述完了。这里面有好几个问题需要重点分析一下。 首先第一个问题,这个锁必须要设置一个过期时间。否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分割(network partition)导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。 第二个问题,第一步获取锁的操作,网上不少文章把它实现成了两个Redis命令: SETNX resource_name my_random_value 虽然这两个命令和前面算法描述中的一个SET命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX后崩溃了,那么就没有机会执行EXPIRE了,导致它一直持有这个锁。 第三个问题,也是antirez指出的,设置一个随机字符串my_random_value是很有必要的,它保证了一个客户端释放的锁必须是自己持有的那个锁。假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:
之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。 第四个问题,释放锁的操作必须使用Lua脚本来实现。释放锁其实包含三步操作:'GET'、判断和'DEL',用Lua脚本来实现能保证这三步的原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题类似的执行序列:
实际上,在上述第三个问题和第四个问题的分析中,如果不是客户端阻塞住了,而是出现了大的网络延迟,也有可能导致类似的执行序列发生。 前面的四个问题,只要实现分布式锁的时候加以注意,就都能够被正确处理。但除此之外,antirez还指出了一个问题,是由failover引起的,却是基于单Redis节点的分布式锁无法解决的。正是这个问题催生了Redlock的出现。 这个问题是这样的。假如Redis节点宕机了,那么所有客户端就都无法获得锁了,服务变得不可用。为了提高可用性,我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但由于Redis的主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性。考虑下面的执行序列:
于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了Redlock算法,我们接下来会讨论。 【其它疑问】 前面这个算法中出现的锁的有效时间(lock validity time),设置成多少合适呢?如果设置太短的话,锁就有可能在客户端完成对于共享资源的访问之前过期,从而失去保护;如果设置太长的话,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。看来真是个两难的问题。 而且,在前面对于随机字符串my_random_value的分析中,antirez也在文章中承认的确应该考虑客户端长期阻塞导致锁过期的情况。如果真的发生了这种情况,那么共享资源是不是已经失去了保护呢?antirez重新设计的Redlock是否能解决这些问题呢? 分布式锁Redlock 由于前面介绍的基于单Redis节点的分布式锁在failover的时候会产生解决不了的安全性问题,因此antirez提出了新的分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)。 运行Redlock算法的客户端依次执行下面各个步骤,来完成获取锁的操作:
|
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|