Redis实现分布式锁--Redlock篇

各位老板好呀,我是风云。

好长好长时间没有更新了,掐指一算,得有小两个月没有更新内容了。

最近这段时间,发生了不少的事情。工作、生活也感觉好像是一团乱麻。我其实想挣脱出来,改变一下。但是不知道怎么的,我好像出不来,似乎被困在一个套子里面了。

原来沉浸解压的电子游戏,我竟然也玩不进去了。年前定的一大批今年想要完成的目标,也是抛之脑后,完全丧失了动力。总的来说这两个月的感觉,就是周围的时间在不知不觉中溜走,但是却单单把我丢下了。。。。

我不知道这是啥原因,但是肯定不是一种好现象,所以今天想尽量通过写一点东西,让自己稍微走起来,不求多,抓住一点时光的尾巴。。

1. RedLock安全么?

其实最近看的技术类文章几乎可以忽略不计,我就不讲新鲜的内容了,补一个前段时间埋的坑吧。关于Redis实现分布式锁的集群方案--Redlock。

上一篇文章介绍了单机版redis实现的分布式锁,如果对单机版的实现方案遗忘了的话,欢迎大家关注我的上一篇文章:

Java八股文(2) -- Redis实现分布式锁(1)

理所当然,这自然就想到这种方案会出现单点故障问题。或者说即使采用了主备模式,甚至是哨兵模式,还是会出现“奇点”,这个时间段内的数据丢失,就会出现加锁信息丢失,导致加锁失败,没有达到互斥的作用。

如果你之前使用过Redlock,或者了解过Redlock相关的知识,这里可以跟着我再复习一遍,如果你从来就不知道不了解 Redlock,没关系,我们重新开始,从头再来。

在开始文章之前,我觉得有必要提醒大家。这一篇文章有一万多字,而且你需要跟紧我的步伐。这篇文章在介绍Redlock之后,会基于Redlock的实现引出分布式领域一场著名的讨论。我会带着大家重新跟进这场讨论的过程,在这个完整的讨论过程中,跟随分布式领域的大师一起思考,相信你一定也能领悟到不少智慧的火花。

首先需要说明的是Redlock 的方案的成立是基于 2 个前提:

1. 不再需要部署从库哨兵实例,只部署主库

2. 但主库要部署多个,官方推荐至少 5 个实例

也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。

注意:不是部署 Redis Cluster,就是部署 5 个简单的 Redis 实例。

图片

Redlock使用的整体流程如下,一共分为 5 步:

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 实例根本就没有加锁成功)。

我简单帮你总结一下,有 4 个重点:

1. 客户端在多个 Redis 实例上申请加锁

2. 必须保证大多数节点加锁成功

3. 大多数节点加锁的总耗时,要小于锁设置的过期时间

4. 释放锁,要向全部节点发起释放锁请求

针对上面的四条重点,我们来看看细节中的魔鬼。。。。。我推荐大家可以巩固一下我以前写过的关于分布式基础的文章:

zookeeper背后的分布式理论介绍(上)

zookeeper背后的分布式理论介绍(下)

有了这些理论基础,我们探讨一下这四个重点背后的道理:

1. 为什么要在多个实例上加锁?

本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。

2. 为什么大多数加锁成功,才算成功?

多个 Redis 实例一起来用,其实就组成了一个「分布式系统」。

在分布式系统中,总会出现「异常节点」,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。

这是一个分布式系统「容错」问题,这个问题的结论是:如果只存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。

3. 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?

因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。

所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。

4. 为什么释放锁,要操作所有节点?

在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。

例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致读取失败,那这把锁其实已经在 Redis 上加锁成功了。

所以,释放锁时,不管之前有没有加锁成功,需要释放「所有节点」的锁,以保证清理节点上「残留」的锁。

好了,明白了 Redlock 的流程和相关问题,看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题,保证了锁的「安全性」。

但事实真的如此吗?

2. Redlock 的争论谁对谁错?

好啦,Redlock方案暂时介绍到这里,让我们看一下关于这个方案提出后,参与这场神仙辩论的双方都有谁吧。

神仙一:Redis 的作者 Antirez,我们就叫他A哥吧。

图片

神仙二:英国剑桥大学分布式系统研究员Martin哥,我们就叫他小马哥吧。

图片

Redis 作者把这个方案一经提出,就马上受到业界著名的分布式系统专家的质疑

小马哥曾是软件工程师和企业家,从事大规模数据基础设施相关的工作。它还经常在大会做演讲,写博客,写书,也是开源贡献者。

分布式专家 Martin 对于 Redlock 的质疑

在他的文章中,主要阐述了 4 个论点:

1) 分布式锁的目的是什么?

Martin 表示,你必须先清楚你在使用分布式锁的目的是什么?

他认为有两个目的。

第一,效率。

使用分布式锁的互斥能力,是避免不必要地做同样的两次工作(例如一些昂贵的计算任务)。如果锁失效,并不会带来「恶性」的后果,例如发了 2 次邮件等,无伤大雅。

第二,正确性。

使用锁用来防止并发进程互相干扰。如果锁失效,会造成多个进程同时操作同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题,就像给患者服用了重复剂量的药物,后果很严重。

他认为,如果你是为了前者——效率,那么使用单机版 Redis 就可以了,即使偶尔发生锁失效(宕机、主从切换),都不会产生严重的后果。而使用 Redlock 太重了,没必要。

而如果是为了正确性,Martin 认为 Redlock 根本达不到安全性的要求,也依旧存在锁失效的问题!

2) 锁在分布式系统中会遇到的问题

Martin 表示,一个分布式系统,更像一个复杂的「野兽」,存在着你想不到的各种异常情况。

这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山:NPC

  • N:Network Delay,网络延迟

  • P:Process Pause,进程暂停(GC)

  • C:Clock Drift,时钟漂移

Martin 用一个进程暂停(GC)的例子,指出了 Redlock 安全性问题:

  1. 客户端 1 请求锁定节点 A、B、C、D、E

  2. 客户端 1 的拿到锁后,进入 GC(时间比较久)

  3. 所有 Redis 节点上的锁都过期了

  4. 客户端 2 获取到了 A、B、C、D、E 上的锁

  5. 客户端 1 GC 结束,认为成功获取锁

  6. 客户端 2 也认为获取到了锁,发生「冲突」

图片

Martin 认为,GC 可能发生在程序的任意时刻,而且执行时间是不可控的。

如果你考虑使用的不是GC语言,那我们考虑下面的场景:

我们一共有 A、B、C 这三个节点。

  1. 客户端 1 在 A,B 上加锁成功。C 上加锁失败。

  2. 这时节点 B 崩溃重启了,但是由于持久化策略导致客户端 1 在 B 上的锁没有持久化下来。

  3. 客户端 2 发起申请同一把锁的操作,在 B,C 上加锁成功。

  4. 这个时候就又出现同一把锁,同时被客户端 1 和客户端 2 所持有了。

(接下来又得说一说Redis的持久化策略了,全是知识点啊,朋友们)

图片

比如,Redis 的 AOF 持久化方式默认情况下是每秒写一次磁盘,即 fsync 操作,因此最坏的情况下可能丢失 1 秒的数据。

当然,你也可以设置成每次修改数据都进行 fsync 操作(fsync=always),但这会严重降低 Redis 的性能,违反了它的设计理念。(我也没见过这样用的,可能还是见的太少了吧。)

而且,你以为执行了 fsync 就不会丢失数据了?天真,真实的系统环境是复杂的,这都已经脱离 Redis 的范畴了。上升到服务器、系统问题了。

所以,根据墨菲定律,上面举的例子:由于节点重启引发的锁失效问题,总是有可能出现的。

为了解决这一问题,Redis 的作者又提出了延迟重启(delayed restarts的概念。

图片

意思就是说,一个节点崩溃后,不要立即重启它,而是等待一定的时间后再重启。等待的时间应该大于锁的过期时间(TTL)。这样做的目的是保证这个节点在重启前所参与的锁都过期。相当于把以前的帐勾销之后才能参与后面的加锁操作。

但是有个问题就是:在等待的时间内,这个节点是不对外工作的。那么如果大多数节点都挂了,进入了等待。就会导致系统的不可用,因为系统在TTL时间内任何锁都将无法加锁成功。而且这也依赖于正确的时钟,也是第三条提出的不合理点。

3) 假设时钟正确的是不合理的

又或者,当多个 Redis 节点「时钟」发生问题时,也会导致 Redlock 锁失效

  1. 客户端 1 获取节点 A、B、C 上的锁,但由于网络问题,无法访问 D 和 E

  2. 节点 C 上的时钟「向前跳跃」,导致锁到期

  3. 客户端 2 获取节点 C、D、E 上的锁,由于网络问题,无法访问 A 和 B

  4. 客户端 1 和 2 现在都相信它们持有了锁(冲突)

Martin 觉得,Redlock 必须「强依赖」多个节点的时钟是保持同步的,一旦有节点时钟发生错误,那这个算法模型就失效了。

Martin 继续阐述,机器的时钟发生错误,是很有可能发生的:

  • 系统管理员「手动修改」了机器时钟

  • 机器时钟在同步 NTP 时间时,发生了大的「跳跃」

总之,Martin 认为,Redlock 的算法是建立在「同步模型」基础上的,有大量资料研究表明,同步模型的假设,在分布式系统中是有问题的。

在混乱的分布式系统的中,你不能假设系统时钟就是对的,所以,你必须非常小心你的假设。

4) 提出 fecing token 的方案,保证正确性

相对应的,Martin 提出一种被叫作 fecing token 的方案,保证分布式锁的正确性。

这个模型流程如下:

  1. 客户端在获取锁时,锁服务可以提供一个「递增」的 token

  2. 客户端拿着这个 token 去操作共享资源

  3. 共享资源可以根据 token 拒绝「后来者」的请求

图片

这样一来,无论 NPC 哪种异常情况发生,都可以保证分布式锁的安全性,因为它是建立在「异步模型」上的。

而 Redlock 无法提供类似 fecing token 的方案,所以它无法保证安全性。

他还表示,一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的「性能」(或称之为活性),而不会影响它的「正确性」。

Martin 的结论:

1、Redlock 不伦不类:它对于效率来讲,Redlock 比较重,没必要这么做,而对于正确性来说,Redlock 是不够安全的。

2、时钟假设不合理:该算法对系统时钟做出了危险的假设(假设多个节点机器时钟都是一致的),如果不满足这些假设,锁就会失效。

3、无法保证正确性:Redlock 不能提供类似 fencing token 的方案,所以解决不了正确性的问题。为了正确性,请使用有「共识系统」的软件,例如 Zookeeper。

好了,以上就是 Martin 反对使用 Redlock 的观点,看起来有理有据。

下面我们来看 Redis 作者 Antirez 是如何反驳的。

Redis 作者 Antirez 的反驳

在 Redis 作者的文章中,重点有 3 个:

1) 解释时钟问题

首先,Redis 作者一眼就看穿了对方提出的最为核心的问题:时钟问题

Redis 作者表示,Redlock 并不需要完全一致的时钟,只需要大体一致就可以了,允许有「误差」。

例如要计时 5s,但实际可能记了 4.5s,之后又记了 5.5s,有一定误差,但只要不超过「误差范围」锁失效时间即可,这种对于时钟的精度要求并不是很高,而且这也符合现实环境。

对于对方提到的「时钟修改」问题,Redis 作者反驳到:

  1. 手动修改时钟:不要这么做就好了,否则你直接修改 Raft 日志,那 Raft 也会无法工作...

  2. 时钟跳跃:通过「恰当的运维」,保证机器时钟不会大幅度跳跃(每次通过微小的调整来完成),实际上这是可以做到的

为什么 Redis 作者优先解释时钟问题?因为在后面的反驳过程中,需要依赖这个基础做进一步解释。

2) 解释网络延迟、GC 问题

之后,Redis 作者对于对方提出的,网络延迟、进程 GC 可能导致 Redlock 失效的问题,也做了反驳:

我们重新回顾一下,Martin 提出的问题假设:

  1. 客户端 1 请求锁定节点 A、B、C、D、E

  2. 客户端 1 的拿到锁后,进入 GC

  3. 所有 Redis 节点上的锁都过期了

  4. 客户端 2 获取节点 A、B、C、D、E 上的锁

  5. 客户端 1 GC 结束,认为成功获取锁

  6. 客户端 2 也认为获取到锁,发生「冲突」

图片

Redis 作者反驳到,这个假设其实是有问题的,Redlock 是可以保证锁安全的。

这是怎么回事呢?

还记得前面介绍 Redlock 流程的那 5 步吗?这里我再拿过来让你复习一下。

  1. 客户端先获取「当前时间戳T1」

  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁

  3. 如果客户端从 3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败

  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)

  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

注意,重点是 1-3,在步骤 3,加锁成功后为什么要重新获取「当前时间戳T2」?还用 T2 - T1 的时间,与锁的过期时间做比较?

Redis 作者强调:如果在 1-3 发生了网络延迟、进程 GC 等耗时长的异常情况,那在第 3 步 T2 - T1,是可以检测出来的,如果超出了锁设置的过期时间,那这时就认为加锁会失败,之后释放所有节点的锁就好了!

Redis 作者继续论述,如果对方认为,发生网络延迟、进程 GC 是在步骤 3 之后,也就是客户端确认拿到了锁,去操作共享资源的途中发生了问题,导致锁失效,那这不止是 Redlock 的问题,任何其它锁服务例如 Zookeeper,都有类似的问题,这不在讨论范畴内。

这里我举个例子解释一下这个问题:

  1. 客户端通过 Redlock 成功获取到锁(通过了大多数节点加锁成功、加锁耗时检查逻辑)

  2. 客户端开始操作共享资源,此时发生网络延迟、进程 GC 等耗时很长的情况

  3. 此时,锁过期自动释放

  4. 客户端开始操作 MySQL(此时的锁可能会被别人拿到,锁失效)

Redis 作者这里的结论就是:

  • 客户端在拿到锁之前,无论经历什么耗时长问题,Redlock 都能够在第 3 步检测出来

  • 客户端在拿到锁之后,发生 NPC,那 Redlock、Zookeeper 都无能为力

所以,Redis 作者认为 Redlock 在保证时钟正确的基础上,是可以保证正确性的。

3) 质疑 fencing token 机制

Redis 作者对于对方提出的 fecing token 机制,也提出了质疑,主要分为 2 个问题,这里最不宜理解,请跟紧我的思路。

第一,这个方案必须要求要操作的「共享资源服务器」有拒绝「旧 token」的能力。

例如,要操作 MySQL,从锁服务拿到一个递增数字的 token,然后客户端要带着这个 token 去改 MySQL 的某一行,这就需要利用 MySQL 的「事物隔离性」来做。

// 两个客户端必须利用事物和隔离性达到目的
// 注意 token 的判断条件
UPDATE table T SET val = $new_val WHERE id = $id AND current_token < $token

但如果操作的不是 MySQL 呢?例如向磁盘上写一个文件,或发起一个 HTTP 请求,那这个方案就无能为力了,这对要操作的资源服务器,提出了更高的要求。

也就是说,大部分要操作的资源服务器,都是没有这种互斥能力的。

再者,既然资源服务器都有了「互斥」能力,那还要分布式锁干什么?

所以,Redis 作者认为这个方案是站不住脚的。

第二,退一步讲,即使 Redlock 没有提供 fecing token 的能力,但 Redlock 已经提供了随机值(就是前面讲的 UUID),利用这个随机值,也可以达到与 fecing token 同样的效果。

如何做呢?

Redis 作者只是提到了可以完成 fecing token 类似的功能,但却没有展开相关细节,根据我查阅的资料,大概流程应该如下,如有错误,欢迎交流~

  1. 客户端使用 Redlock 拿到锁

  2. 客户端在操作共享资源之前,先把这个锁的 VALUE,在要操作的共享资源上做标记

  3. 客户端处理业务逻辑,最后,在修改共享资源时,判断这个标记是否与之前一样,一样才修改(类似 CAS 的思路)

还是以 MySQL 为例,举个例子就是这样的:

  1. 客户端使用 Redlock 拿到锁

  2. 客户端要修改 MySQL 表中的某一行数据之前,先把锁的 VALUE 更新到这一行的某个字段中(这里假设为 current_token 字段)

  3. 客户端处理业务逻辑

  4. 客户端修改 MySQL 的这一行数据,把 VALUE 当做 WHERE 条件,再修改

UPDATE table T SET val = $new_val WHERE id = $id AND current_token = $redlock_value

可见,这种方案依赖 MySQL 的事物机制,也达到对方提到的 fecing token 一样的效果。

但这里还有个小问题,是网友参与问题讨论时提出的:两个客户端通过这种方案,先「标记」再「检查+修改」共享资源,那这两个客户端的操作顺序无法保证啊?

而用 Martin 提到的 fecing token,因为这个 token 是单调递增的数字,资源服务器可以拒绝小的 token 请求,保证了操作的「顺序性」!

Redis 作者对这问题做了不同的解释,我觉得很有道理,他解释道:分布式锁的本质,是为了「互斥」,只要能保证两个客户端在并发时,一个成功,一个失败就好了,不需要关心「顺序性」。

前面 Martin 的质疑中,一直很关心这个顺序性问题,但 Redis 的作者的看法却不同。

综上,Redis 作者的结论:

1、作者同意对方关于「时钟跳跃」对 Redlock 的影响,但认为时钟跳跃是可以避免的,取决于基础设施和运维。

2、Redlock 在设计时,充分考虑了 NPC 问题,在 Redlock 步骤 3 之前出现 NPC,可以保证锁的正确性,但在步骤 3 之后发生 NPC,不止是 Redlock 有问题,其它分布式锁服务同样也有问题,所以不在讨论范畴内。

要说大佬不愧是大佬,A哥的回击条理清楚,行文流畅。他总结后认为长发哥觉得 Redlock 不安全主要分为两个方面:

图片

  1. 带有自动过期功能的分布式锁,需要一种方法(fencing机制)来避免客户端在过期时间后使用锁时出现问题,从而对共享资源进行真正的互斥保护。马丁说Redlock没有这种机制。

  2. 马丁说,无论问题“1”如何解决,该算法本质上都是不安全的,因为它对系统模型进行了记时假设,而这些假设在实际系统中是无法保证的。

对于第一个点,卷发哥列了5大点来反驳这个问题,其中一个重要的观点是他认为虽然 Redlock 没有提供类似于fencing机制那样的单调递增的令牌,但是也有一个随机串,把这个随机串当做token,也可以达到同样的效果啊。当需要和共享资源交互的时候,我们检查一下这个token是否发生了变化,如果没有再执行“获取-修改-写回”的操作。

图片

最终得出的结论是一个灵魂反问:既然在锁失效的情况下已经存在一种fencing机制能继续保持资源的互斥访问了,那为什么还要使用一个分布式锁并且还要求它提供那么强的安全性保证呢?

图片

图片

然而第二个问题,对于网络延迟或者 GC 暂停,我们前面分析过,对 Redlock 的安全性并不会产生影响,说明卷发哥在设计的时候其实是考虑过时间因素带来的问题的。

但是如果是长发哥提出的时钟发生跳跃,很明显,卷发哥知道如果时钟发生跳跃, Redlock 的安全性就得不到保障,这是他的命门。

但是对于长发哥写时钟跳跃的时候提出的两个例子:

  1. 运维人员手动修改了系统时钟。

  2. 从NTP服务收到了一个大的时钟更新事件。

卷发哥进行了回击:

第一点这个运维人员手动修改时钟,属于人为因素,这个我也没办法啊,人家就是要搞你,怎么办?加强管理,不要这样做。

第二点从NTP服务收到一个大的时钟更新,对于这个问题,需要通过运维来保证。需要将大的时间更新到服务器的时候,应当采取少量多次的方式。多次修改,每次更新时间尽量小。

关于这个地方的争论,就看你是信长发哥的时间一定会跳跃,还是信卷发哥的时间跳跃我们也是可以处理的。

关于时钟跳跃,有一篇文章可以看看,也是这次神仙打架导致的产物:

https://jvns.ca/blog/2016/02/09/til-clock-skew-exists/

文章得出的最终结论是:时钟跳跃是存在的。

图片

其实我们大家应该都经历过时钟跳跃的情况,你还记得2016年的最后一天,当时有个“闰秒”的概念吗?导致2017年1月1日出现了07:59:60的奇观。

图片

基于 Zookeeper 的锁安全吗?

如果你有了解过 Zookeeper,基于它实现的分布式锁是这样的:

  1. 客户端 1 和 2 都尝试创建「临时节点」,例如 /lock

  2. 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败

  3. 客户端 1 操作共享资源

  4. 客户端 1 删除 /lock 节点,释放锁

你应该也看到了,Zookeeper 不像 Redis 那样,需要考虑锁的过期时间问题,它是采用了「临时节点」,保证客户端 1 拿到锁后,只要连接不断,就可以一直持有锁。

而且,如果客户端 1 异常崩溃了,那么这个临时节点会自动删除,保证了锁一定会被释放。

不错,没有锁过期的烦恼,还能在异常时自动释放锁,是不是觉得很完美?

其实不然。

思考一下,客户端 1 创建临时节点后,Zookeeper 是如何保证让这个客户端一直持有锁呢?

原因就在于,客户端 1 此时会与 Zookeeper 服务器维护一个 Session,这个 Session 会依赖客户端「定时心跳」来维持连接。

如果 Zookeeper 长时间收不到客户端的心跳,就认为这个 Session 过期了,也会把这个临时节点删除。

图片

同样地,基于此问题,我们也讨论一下 GC 问题对 Zookeeper 的锁有何影响:

  1. 客户端 1 创建临时节点 /lock 成功,拿到了锁

  2. 客户端 1 发生长时间 GC

  3. 客户端 1 无法给 Zookeeper 发送心跳,Zookeeper 把临时节点「删除」

  4. 客户端 2 创建临时节点 /lock 成功,拿到了锁

  5. 客户端 1 GC 结束,它仍然认为自己持有锁(冲突)

可见,即使是使用 Zookeeper,也无法保证进程 GC、网络延迟异常场景下的安全性。

这就是前面 Redis 作者在反驳的文章中提到的:如果客户端已经拿到了锁,但客户端与锁服务器发生「失联」(例如 GC),那不止 Redlock 有问题,其它锁服务都有类似的问题,Zookeeper 也是一样!

所以,这里我们就能得出结论了:一个分布式锁,在极端情况下,不一定是安全的。

如果你的业务数据非常敏感,在使用分布式锁时,一定要注意这个问题,不能假设分布式锁 100% 安全。

好,现在我们来总结一下 Zookeeper 在使用分布式锁时优劣:

Zookeeper 的优点:

  1. 不需要考虑锁的过期时间

  2. watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁

但它的劣势是:

  1. 性能不如 Redis

  2. 部署和运维成本高

  3. 客户端与 Zookeeper 的长时间失联,锁被释放问题

我对分布式锁的理解

好了,前面详细介绍了基于 Redis 的 Redlock 和 Zookeeper 实现的分布锁,在各种异常情况下的安全性问题,下面我想和你聊一聊我的看法,仅供参考,不喜勿喷。

1) 到底要不要用 Redlock?

前面也分析了,Redlock 只有建立在「时钟正确」的前提下,才能正常工作,如果你可以保证这个前提,那么可以拿来使用。

但保证时钟正确,我认为并不是你想的那么简单就能做到的。

第一,从硬件角度来说,时钟发生偏移是时有发生,无法避免。

例如,CPU 温度、机器负载、芯片材料都是有可能导致时钟发生偏移的。

第二,从我的工作经历来说,曾经就遇到过时钟错误、运维暴力修改时钟的情况发生,进而影响了系统的正确性,所以,人为错误也是很难完全避免的。

所以,我对 Redlock 的个人看法是,尽量不用它,而且它的性能不如单机版 Redis,部署成本也高,我还是会优先考虑使用主从+ 哨兵的模式 实现分布式锁。

那正确性如何保证呢?第二点给你答案。

2) 如何正确使用分布式锁?

在分析 Martin 观点时,它提到了 fecing token 的方案,给我了很大的启发,虽然这种方案有很大的局限性,但对于保证「正确性」的场景,是一个非常好的思路。

所以,我们可以把这两者结合起来用:

1、使用分布式锁,在上层完成「互斥」目的,虽然极端情况下锁会失效,但它可以最大程度把并发请求阻挡在最上层,减轻操作资源层的压力。

2、但对于要求数据绝对正确的业务,在资源层一定要做好「兜底」,设计思路可以借鉴 fecing token 的方案来做。

两种思路结合,我认为对于大多数业务场景,已经可以满足要求了。

总结

好了,总结一下。

这篇文章,我们主要探讨了基于 Redis 实现的分布式锁,究竟是否安全这个问题。

从最简单分布式锁的实现,到处理各种异常场景,再到引出 Redlock,以及两个分布式专家的辩论,得出了 Redlock 的适用场景。

最后,我们还对比了 Zookeeper 在做分布式锁时,可能会遇到的问题,以及与 Redis 的差异。

其实在这场争论的最后,小马哥对这场争论进行了一个非常感性的总结,他说:

下面翻译来自:

https://www.jianshu.com/p/dd66bdd18a56

对我来说最重要的一点在于:我并不在乎在这场辩论中谁对谁错 —— 我只关心从其他人的工作中学到的东西,以便我们能够避免重蹈覆辙,并让未来更加美好。前人已经为我们创造出了许多伟大的成果:站在巨人的肩膀上,我们得以构建更棒的软件。

对于任何想法,务必要详加检验,通过论证以及检查它们是否经得住别人的详细审查。那是学习过程的一部分。但目标应该是为了获得知识,而不应该是为了说服别人相信你自己是对的。有时候,那只不过意味着停下来,好好地想一想。

好啦,今天关于Redis分布式锁的第二部分的介绍就到这里了,这片文章我也是借鉴了很多内容,尤其是关于神仙辩论的部分,更是参考了 “why技术” 与 “水滴与银弹”这两个优秀公众号的文章,也欢迎大家关注。

图片

写到最后

如果这篇文章对您有所帮助或启发,帮忙扫描下方二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看

欢迎大家关注我的公众号:“风云编程录”

 

往期好文推荐  

程序员三部曲(1):童年

程序员三部曲(2):  我的大学(上)

技术提升(1) -- 缓存专题

缓存更新的Design Pattern -- 缓存专题(2)

▲Java八股文(1) -- 如何理解 String 类型值的不可变?

Java八股文(2) -- Redis实现分布式锁(1)

Java开发的杀手级应用有哪些?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值