Redlock简介
为了解决上面的问题,Redis 的作者提出了名为 Redlock 的算法。
在 Redis 的分布式环境中,我们假设有 N 个 Redis Master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。
前面已经描述了在单点 Redis 下,怎么安全地获取和释放锁,我们确保将在 N 个实例上使用此方法获取和释放锁。
在下面的示例中,我们假设有 5 个完全独立的 Redis Master 节点,他们分别运行在 5 台服务器中,可以保证他们不会同时宕机。
从官网上我们可以知道,一个客户端如果要获得锁,必须经过下面的五个步骤:
步骤描述来源:http://redis.cn/topics/distlock.html
1.获取当前 Unix 时间,以毫秒为单位。
2.依次尝试从 N 个实例,使用相同的 key 和随机值获取锁。在步骤 2,当向 Redis 设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以避免服务器端 Redis 已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个 Redis 实例。
3.客户端使用当前时间减去开始获取锁时间(步骤 1 记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是 3 个节点)的 Redis 节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
4.如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5.如果因为某些原因,获取锁失败(没有在至少 N/2+1 个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功)。
通过上面的步骤我们可以知道,只要大多数的节点可以正常工作,就可以保证 Redlock 的正常工作。这样就可以解决前面单点 Redis 的情况下我们讨论的节点挂掉,由于异步通信,导致锁失效的问题。
但是,还是不能解决故障重启后带来的锁的安全性的问题。你想一下下面这个场景:
我们一共有 A、B、C 这三个节点。
1.客户端 1 在 A,B 上加锁成功。C 上加锁失败。
2.这时节点 B 崩溃重启了,但是由于持久化策略导致客户端 1 在 B 上的锁没有持久化下来。 客户端 2 发起申请同一把锁的操作,在 B,C 上加锁成功。
3.这个时候就又出现同一把锁,同时被客户端 1 和客户端 2 所持有了。
(接下来又得说一说Redis的持久化策略了,全是知识点啊,朋友们)
比如,Redis 的 AOF 持久化方式默认情况下是每秒写一次磁盘,即 fsync 操作,因此最坏的情况下可能丢失 1 秒的数据。
当然,你也可以设置成每次修改数据都进行 fsync 操作(fsync=always),但这会严重降低 Redis 的性能,违反了它的设计理念。(我也没见过这样用的,可能还是见的太少了吧。)
而且,你以为执行了 fsync 就不会丢失数据了?天真,真实的系统环境是复杂的,这都已经脱离 Redis 的范畴了。上升到服务器、系统问题了。
所以,根据墨菲定律,上面举的例子:由于节点重启引发的锁失效问题,总是有可能出现的。
为了解决这一问题,Redis 的作者又提出了延迟重启(delayed restarts)的概念。
意思就是说,一个节点崩溃后,不要立即重启它,而是等待一定的时间后再重启。等待的时间应该大于锁的过期时间(TTL)。这样做的目的是保证这个节点在重启前所参与的锁都过期。相当于把以前的帐勾销之后才能参与后面的加锁操作。
但是有个问题就是:在等待的时间内,这个节点是不对外工作的。那么如果大多数节点都挂了,进入了等待。就会导致系统的不可用,因为系统在TTL时间内任何锁都将无法加锁成功。
Redlock 算法还有一个需要注意的点是它的释放锁操作。
释放锁的时候是要向所有节点发起释放锁的操作的。这样做的目的是为了解决有可能在加锁阶段,这个节点收到加锁请求了,也set成功了,但是由于返回给客户端的响应包丢了,导致客户端以为没有加锁成功。所以,释放锁的时候要向所有节点发起释放锁的操作。
你可能觉得这不是常规操作吗?
有的细节就是这样,说出来后觉得不过如此,但是有可能自己就是想不到这个点,导致问题的出现,所以我们才会说:细节,魔鬼都在细节里。
好了,简介大概就说到这里,有兴趣的朋友可以再去看看官网,补充一下。
中文:http://redis.cn/topics/distlock.html
英文:https://redis.io/topics/distlock
好了,经过这么长,这么长的铺垫,我们终于可以进入到神仙打架环节。
神仙打架
神仙一:Redis 的作者 antirez 。有的朋友对英文名字不太敏感,所以后面我就叫他卷发哥吧。
神仙二:分布式领域专家 Martin Kleppmann,我们叫他长发哥吧。
看完上面两位神仙的照片,再看看我为了写这篇文章又日渐稀少的头发,我忍不住哭出声来。可能只有给我点赞,才能平复我的心情吧。
卷发哥在官网介绍 Redlock 页面的最后写到:如果你也是使用分布式系统的人员,你的观点和意见非常重要,欢迎和我们讨论。
于是,“求锤得锤”!这一锤,锤出了众多的吃瓜网友,其中不乏在相关领域的专业人士。
长发哥出锤
故事得从 2016年2月8号 长发哥发布的一篇文章《How to do distributed locking》说起:
文章地址:http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
这一部分直接翻译过来就是:
作为本书(《数据密集型应用系统设计》)研究的一部分,我在Redis网站上 看到了一种称为Redlock的算法。该算法声称在Redis实现容错的分布式锁(或更确切地说, 租约),并且该页面要求来自分布式系统人员的反馈。这个算法让我产生了一些思考,因此我花了一些时间写了我的这篇文章。
由于Redlock已经有10多个独立的实现,而且我们不知道谁已经在依赖此算法,因此我认为值得公开分享我的笔记。我不会讨论Redis的其他方面,其中一些已经在其他地方受到了批评
你看这个文章,开头就是火药味十足:你说要反馈,那我就给你反馈。而且你这个东西有其他问题,我也就不说了。(其实作者在这篇文章中也说了,他很喜欢并且也在使用 Redis,只是他觉得这个 Redlock 算法是不严谨的)
长发哥主要围绕了下面的这张图进行了展开:
要是一眼没看明白,我再给你一个中文版的,来自长发哥于2017年出版的书《数据密集型应用系统设计》:
可以看到上面的图片中提到了申请租约、租约到期的关键词,租约其实就是可以理解为带超时时间的锁。
而在书中,这张图片的下面写的描述这样的,你咂摸咂摸:
拿 HBase 举例,其设计的目标是确保存储系统的文件一次只能由一个客户端访问,如果多个客户端试图同时写入该文件,文件就会被破坏。那么上面的图片解释起来就是:
1.客户端 1 先去申请锁,并且成功获取到锁。之后客户端进行了长时间的 GC 导致了 STW 的情况。
2.在 STW 期间,客户端 1 获取的锁的超时时间到了,锁也就失效了。
3.由于客户端 1 的锁已经过期失效了,所以客户端 2 去申请锁就可以成功获得锁。
4.客户端 2 开始写文件,并完成文件的写入。
5.客户端 1 从 STW 中恢复过来,他并不知道自己的锁过期了,还是会继续执行文件写入操作,导致客户端 2 写入的文件被破坏。而且可以看到,它没有满足锁在任意时刻只有一个客户端持有的原则,即没有满足互斥性。
书里面没有明说,但是你品一品,这里的锁服务难道不是在说 Redis?
有的朋友就会说了,那客户端 1 写入文件的时候,再判断一下自己的锁有没有过期不就可以了吗?
你可真是个小机灵鬼呢,那我问你,GC 可能是发生在任何时间的,万一 GC 发生在判断之后呢?
你继续怼我,如果客户端使用的是没有 GC 的语言呢?