微信搜索关注「水滴与银弹」公众号,第一时间获取优质技术干货。7年资深后端研发,给你呈现不同的技术视角。html
你们好,我是 Kaito。python
这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题。程序员
Redis 分布式锁的话题,不少文章已经写烂了,我为何还要写这篇文章呢?redis
由于我发现网上 99% 的文章,并无把这个问题真正讲清楚。致使不少读者看了不少文章,依旧云里雾里。例以下面这些问题,你能清晰地回答上来吗?算法
这篇文章,我就来把这些问题完全讲清楚。sql
读完这篇文章,你不只能够完全了解分布式锁,还会对「分布式系统」有更加深入的理解。shell
文章有点长,但干货不少,但愿你能够耐心读完。编程
在开始讲分布式锁以前,有必要简单介绍一下,为何须要分布式锁?后端
与分布式锁相对应的是「单机锁」,咱们在写多线程程序时,避免同时操做一个共享变量产生数据问题,一般会使用一把锁来「互斥」,以保证共享变量的正确性,其使用范围是在「同一个进程」中。安全
若是换作是多个进程,须要同时操做一个共享资源,如何互斥呢?
例如,如今的业务应用一般都是微服务架构,这也意味着一个应用会部署多个进程,那这多个进程若是须要修改 MySQL 中的同一行记录时,为了不操做乱序致使数据错误,此时,咱们就须要引入「分布式锁」来解决这个问题了。
想要实现分布式锁,必须借助一个外部系统,全部进程都去这个系统上申请「加锁」。
而这个外部系统,必需要实现「互斥」的能力,即两个请求同时进来,只会给一个进程返回成功,另外一个返回失败(或等待)。
这个外部系统,能够是 MySQL,也能够是 Redis 或 Zookeeper。但为了追求更好的性能,咱们一般会选择使用 Redis 或 Zookeeper 来作。
下面我就以 Redis 为主线,由浅入深,带你深度剖析一下,分布式锁的各类「安全性」问题,帮你完全理解分布式锁。
咱们从最简单的开始讲起。
想要实现分布式锁,必需要求 Redis 有「互斥」的能力,咱们可使用 SETNX 命令,这个命令表示SET if Not eXists,即若是 key 不存在,才会设置它的值,不然什么也不作。
两个客户端进程能够执行这个命令,达到互斥,就能够实现一个分布式锁。
客户端 1 申请加锁,加锁成功:
127.0.0.1:6379> SETNX lock 1
(integer) 1 // 客户端1,加锁成功
复制代码
客户端 2 申请加锁,由于后到达,加锁失败:
127.0.0.1:6379> SETNX lock 1
(integer) 0 // 客户端2,加锁失败
复制代码
此时,加锁成功的客户端,就能够去操做「共享资源」,例如,修改 MySQL 的某一行数据,或者调用一个 API 请求。
操做完成后,还要及时释放锁,给后来者让出操做共享资源的机会。如何释放锁呢?
也很简单,直接使用 DEL 命令删除这个 key 便可:
127.0.0.1:6379> DEL lock // 释放锁
(integer) 1
复制代码
这个逻辑很是简单,总体的路程就是这样:
可是,它存在一个很大的问题,当客户端 1 拿到锁后,若是发生下面的场景,就会形成「死锁」:
这时,这个客户端就会一直占用这个锁,而其它客户端就「永远」拿不到这把锁了。
怎么解决这个问题呢?
咱们很容易想到的方案是,在申请锁时,给这把锁设置一个「租期」。
在 Redis 中实现时,就是给这个 key 设置一个「过时时间」。这里咱们假设,操做共享资源的时间不会超过 10s,那么在加锁时,给这个 key 设置 10s 过时便可:
127.0.0.1:6379> SETNX lock 1 // 加锁
(integer) 1
127.0.0.1:6379> EXPIRE lock 10 // 10s后自动过时
(integer) 1
复制代码
这样一来,不管客户端是否异常,这个锁均可以在 10s 后被「自动释放」,其它客户端依旧能够拿到锁。
但这样真的没问题吗?
仍是有问题。
如今的操做,加锁、设置过时是 2 条命令,有没有可能只执行了第一条,第二条却「来不及」执行的状况发生呢?例如:
总之,这两条命令不能保证是原子操做(一块儿成功),就有潜在的风险致使过时时间设置失败,依旧发生「死锁」问题。
怎么办?
在 Redis 2.6.12 版本以前,咱们须要想尽办法,保证 SETNX 和 EXPIRE 原子性执行,还要考虑各类异常状况如何处理。
但在 Redis 2.6.12 以后,Redis 扩展了 SET 命令的参数,用这一条命令就能够了:
// 一条命令保证原子性执行
127.0.0.1:6379> SET lock 1 EX 10 NX
OK
复制代码
这样就解决了死锁问题,也比较简单。
咱们再来看分析下,它还有什么问题?
试想这样一种场景:
看到了么,这里存在两个严重的问题:
致使这两个问题的缘由是什么?咱们一个个来看。
第一个问题,多是咱们评估操做共享资源的时间不许确致使的。
例如,操做共享资源的时间「最慢」可能须要 15s,而咱们却只设置了 10s 过时,那这就存在锁提早过时的风险。
过时时间过短,那增大冗余时间,例如设置过时时间为 20s,这样总能够了吧?
这样确实能够「缓解」这个问题,下降出问题的几率,但依旧没法「完全解决」问题。
为何?
缘由在于,客户端在拿到锁以后,在操做共享资源时,遇到的场景有多是很复杂的,例如,程序内部发生异常、网络请求超时等等。
既然是「预估」时间,也只能是大体计算,除非你能预料并覆盖到全部致使耗时变长的场景,但这其实很难。
有什么更好的解决方案吗?
别急,关于这个问题,我会在后面详细来说对应的解决方案。
咱们继续来看第二个问题。
第二个问题在于,一个客户端释放了其它客户端持有的锁。
想一下,致使这个问题的关键点在哪?
重点在于,每一个客户端在释放锁时,都是「无脑」操做,并无检查这把锁是否还「归本身持有」,因此就会发生释放别人锁的风险,这样的解锁流程,很不「严谨」!
如何解决这个问题呢?
解决办法是:客户端在加锁时,设置一个只有本身知道的「惟一标识」进去。
例如,能够是本身的线程 ID,也能够是一个 UUID(随机且惟一),这里咱们以 UUID 举例:
// 锁的VALUE设置为UUID
127.0.0.1:6379> SET lock $uuid EX 20 NX
OK
复制代码
这里假设 20s 操做共享时间彻底足够,先不考虑锁自动过时的问题。
以后,在释放锁时,要先判断这把锁是否还归本身持有,伪代码能够这么写:
// 锁是本身的,才释放
if redis.get("lock") == $uuid:
redis.del("lock")
复制代码
这里释放锁使用的是 GET + DEL 两条命令,这时,又会遇到咱们前面讲的原子性问题了。
因而可知,这两个命令仍是必需要原子执行才行。
怎样原子执行呢?Lua 脚本。
咱们能够把这个逻辑,写成 Lua 脚本,让 Redis 来执行。
由于 Redis 处理每个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。
安全释放锁的 Lua 脚本以下:
// 判断锁是本身的,才释放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
复制代码
好了,这样一路优化,整个的加锁、解锁的流程就更「严谨」了。
这里咱们先小结一下,基于 Redis 实现的分布式锁,一个严谨的的流程以下:
好,有了这个完整的锁模型,让咱们从新回到前面提到的第一个问题。
锁过时时间很差评估怎么办?
前面咱们提到,锁的过时时间若是评估很差,这个锁就会有「提早」过时的风险。
当时给的妥协方案是,尽可能「冗余」过时时间,下降锁提早过时的几率。
这个方案其实也不能完美解决问题,那怎么办呢?
是否能够设计这样的方案:加锁时,先设置一个过时时间,而后咱们开启一个「守护线程」,定时去检测这个锁的失效时间,若是锁快要过时了,操做共享资源还未完成,那么就自动对锁进行「续期」,从新设置过时时间。
这确实一种比较好的方案。
若是你是 Java 技术栈,幸运的是,已经有一个库把这些工做都封装好了:Redisson。
Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过时,这个守护线程咱们通常也把它叫作「看门狗」线程。
除此以外,这个 SDK 还封装了不少易用的功能:
这个 SDK 提供的 API 很是友好,它能够像操做本地锁的方式,操做分布式锁。若是你是 Java 技术栈,能够直接把它用起来。
这里不重点介绍 Redisson 的使用,你们能够看官方 Github 学习如何使用,比较简单。
到这里咱们再小结一下,基于 Redis 的实现分布式锁,前面遇到的问题,以及对应的解决方案:
还有哪些问题场景,会危害 Redis 锁的安全性呢?
以前分析的场景都是,锁在「单个」Redis 实例中可能产生的问题,并无涉及到 Redis 的部署架构细节。
而咱们在使用 Redis 时,通常会采用主从集群 + 哨兵的模式部署,这样作的好处在于,当主库异常宕机时,哨兵能够实现「故障自动切换」,把从库提高为主库,继续提供服务,以此保证可用性。
那当「主从发生切换」时,这个分布锁会依旧安全吗?
试想这样的场景:
可见,当引入 Redis 副本后,分布锁仍是可能会受到影响。
怎么解决这个问题?
为此,Redis 的做者提出一种解决方案,就是咱们常常听到的 Redlock(红锁)。
它真的能够解决上面这个问题吗?
好,终于到了这篇文章的重头戏。啊?上面讲的那么多问题,难道只是基础?
是的,那些只是开胃菜,真正的硬菜,从这里刚刚开始。
若是上面讲的内容,你尚未理解,我建议你从新阅读一遍,先理清整个加锁、解锁的基本流程。
若是你已经对 Redlock 有所了解,这里能够跟着我再复习一遍,若是你不了解 Redlock,不要紧,我会带你从新认识它。
值得提醒你的是,后面我不只仅是讲 Redlock 的原理,还会引出有关「分布式系统」中的不少问题,你最好跟紧个人思路,在脑中一块儿分析问题的答案。
如今咱们来看,Redis 做者提出的 Redlock 方案,是如何解决主从切换后,锁失效问题的。
Redlock 的方案基于 2 个前提:
也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,并且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
注意:不是部署 Redis Cluster,就是部署 5 个简单的 Redis 实例。
Redlock 具体如何使用呢?
总体的流程是这样的,一共分为 5 步:
我简单帮你总结一下,有 4 个重点:
第一次看可能不太容易理解,建议你把上面的文字多看几遍,加深记忆。
而后,记住这 5 步,很是重要,下面会根据这个流程,剖析各类可能致使锁失效的问题假设。
好,明白了 Redlock 的流程,咱们来看 Redlock 为何要这么作。
1) 为何要在多个实例上加锁?
本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
2) 为何大多数加锁成功,才算成功?
多个 Redis 实例一块儿来用,其实就组成了一个「分布式系统」。
在分布式系统中,总会出现「异常节点」,因此,在谈论分布式系统问题时,须要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。
这是一个分布式系统「容错」问题,这个问题的结论是:若是只存在「故障」节点,只要大多数节点正常,那么整个系统依旧是能够提供正确服务的。
这个问题的模型,就是咱们常常听到的「拜占庭将军」问题,感兴趣能够去看算法的推演过程。
3) 为何步骤 3 加锁成功后,还要计算加锁的累计耗时?
由于操做的是多个节点,因此耗时确定会比操做单个实例耗时更久,并且,由于是网络请求,网络状况是复杂的,有可能存在延迟、丢包、超时等状况发生,网络请求越多,异常发生的几率就越大。
因此,即便大多数节点加锁成功,但若是加锁的累计耗时已经「超过」了锁的过时时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
4) 为何释放锁,要操做全部节点?
在某一个 Redis 节点加锁时,可能由于「网络缘由」致使加锁失败。
例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题致使读取失败,那这把锁其实已经在 Redis 上加锁成功了。
因此,释放锁时,无论以前有没有加锁成功,须要释放「全部节点」的锁,以保证清理节点上「残留」的锁。
好了,明白了 Redlock 的流程和相关问题,看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题,保证了锁的「安全性」。
但事实真的如此吗?
Redis 做者把这个方案一经提出,就立刻受到业界著名的分布式系统专家的质疑!
这个专家叫 Martin,是英国剑桥大学的一名分布式系统研究员。在此以前他曾是软件工程师和企业家,从事大规模数据基础设施相关的工做。它还常常在大会作演讲,写博客,写书,也是开源贡献者。
他立刻写了篇文章,质疑这个 Redlock 的算法模型是有问题的,并对分布式锁的设计,提出了本身的见解。
以后,Redis 做者 Antirez 面对质疑,不甘示弱,也写了一篇文章,反驳了对方的观点,并详细剖析了 Redlock 算法模型的更多设计细节。
并且,关于这个问题的争论,在当时互联网上也引发了很是激烈的讨论。
二人思路清晰,论据充分,这是一场高手过招,也是分布式系统领域很是好的一次思想的碰撞!双方都是分布式系统领域的专家,却对同一个问题提出不少相反的论断,到底是怎么回事?
下面我会从他们的争论文章中,提取重要的观点,整理呈现给你。
提醒:后面的信息量极大,可能不宜理解,最好放慢速度阅读。
在他的文章中,主要阐述了 4 个论点:
1) 分布式锁的目的是什么?
Martin 表示,你必须先清楚你在使用分布式锁的目的是什么?
他认为有两个目的。
第一,效率。
使用分布式锁的互斥能力,是避免没必要要地作一样的两次工做(例如一些昂贵的计算任务)。若是锁失效,并不会带来「恶性」的后果,例如发了 2 次邮件等,无伤大雅。
第二,正确性。
使用锁用来防止并发进程互相干扰。若是锁失效,会形成多个进程同时操做同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题,就像给患者服用重复剂量的药物同样,后果严重。
他认为,若是你是为了前者——效率,那么使用单机版 Redis 就能够了,即便偶尔发生锁失效(宕机、主从切换),都不会产生严重的后果。而使用 Redlock 过重了,不必。
而若是是为了正确性,Martin 认为 Redlock 根本达不到安全性的要求,也依旧存在锁失效的问题!
2) 锁在分布式系统中会遇到的问题
Martin 表示,一个分布式系统,更像一个复杂的「野兽」,存在着你想不到的各类异常状况。
这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山:NPC。
Martin 用一个进程暂停(GC)的例子,指出了 Redlock 安全性问题:
Martin 认为,GC 可能发生在程序的任意时刻,并且执行时间是不可控的。
注:固然,即便是使用没有 GC 的编程语言,在发生网络延迟、时钟漂移时,也都有可能致使 Redlock 出现问题,这里 Martin 只是拿 GC 举例。
3) 假设时钟正确的是不合理的
又或者,当多个 Redis 节点「时钟」发生问题时,也会致使 Redlock 锁失效。
Martin 以为,Redlock 必须「强依赖」多个节点的时钟是保持同步的,一旦有节点时钟发生错误,那这个算法模型就失效了。
即便 C 不是时钟跳跃,而是「崩溃后当即重启」,也会发生相似的问题。
Martin 继续阐述,机器的时钟发生错误,是颇有可能发生的:
总之,Martin 认为,Redlock 的算法是创建在「同步模型」基础上的,有大量资料研究代表,同步模型的假设,在分布式系统中是有问题的。
在混乱的分布式系统的中,你不能假设系统时钟就是对的,因此,你必须很是当心你的假设。
4) 提出 fecing token 的方案,保证正确性
相对应的,Martin 提出一种被叫做 fecing token 的方案,保证分布式锁的正确性。
这个模型流程以下:
这样一来,不管 NPC 哪一种异常状况发生,均可以保证分布式锁的安全性,由于它是创建在「异步模型」上的。
而 Redlock 没法提供相似 fecing token 的方案,因此它没法保证安全性。
他还表示,一个好的分布式锁,不管 NPC 怎么发生,能够不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的「性能」(或称之为活性),而不会影响它的「正确性」。
Martin 的结论:
一、Redlock 不三不四:它对于效率来说,Redlock 比较重,不必这么作,而对于正确性来讲,Redlock 是不够安全的。
二、时钟假设不合理:该算法对系统时钟作出了危险的假设(假设多个节点机器时钟都是一致的),若是不知足这些假设,锁就会失效。
三、没法保证正确性:Redlock 不能提供相似 fencing token 的方案,因此解决不了正确性的问题。为了正确性,请使用有「共识系统」的软件,例如 Zookeeper。
好了,以上就是 Martin 反对使用 Redlock 的观点,看起来有理有据。
下面咱们来看 Redis 做者 Antirez 是如何反驳的。
在 Redis 做者的文章中,重点有 3 个:
1) 解释时钟问题
首先,Redis 做者一眼就看穿了对方提出的最为核心的问题:时钟问题。
Redis 做者表示,Redlock 并不须要彻底一致的时钟,只须要大致一致就能够了,容许有「偏差」。
例如要计时 5s,但实际可能记了 4.5s,以后又记了 5.5s,有必定偏差,但只要不超过「偏差范围」锁失效时间便可,这种对于时钟的精度的要求并非很高,并且这也符合现实环境。
对于对方提到的「时钟修改」问题,Redis 做者反驳到:
为何 Redis 做者优先解释时钟问题?由于在后面的反驳过程当中,须要依赖这个基础作进一步解释。
2) 解释网络延迟、GC 问题
以后,Redis 做者对于对方提出的,网络延迟wan、进程 GC 可能致使 Redlock 失效的问题,也作了反驳:
咱们从新回顾一下,Martin 提出的问题假设:
Redis 做者反驳到,这个假设实际上是有问题的,Redlock 是能够保证锁安全的。
这是怎么回事呢?
还记得前面介绍 Redlock 流程的那 5 步吗?这里我再拿过来让你复习一下。
注意,重点是 1-3,在步骤 3,加锁成功后为何要从新获取「当前时间戳T2」?还用 T2 - T1 的时间,与锁的过时时间作比较?
Redis 做者强调:若是在 1-3 发生了网络延迟、进程 GC 等耗时长的异常状况,那在第 3 步 T2 - T1,是能够检测出来的,若是超出了锁设置的过时时间,那这时就认为加锁会失败,以后释放全部节点的锁就行了!
Redis 做者继续论述,若是对方认为,发生网络延迟、进程 GC 是在步骤 3 以后,也就是客户端确认拿到了锁,去操做共享资源的途中发生了问题,致使锁失效,那这不止是 Redlock 的问题,任何其它锁服务例如 Zookeeper,都有相似的问题,这不在讨论范畴内。
这里我举个例子解释一下这个问题:
Redis 做者这里的结论就是:
因此,Redis 做者认为 Redlock 在保证时钟正确的基础上,是能够保证正确性的。
3) 质疑 fencing token 机制
Redis 做者对于对方提出的 fecing token 机制,也提出了质疑,主要分为 2 个问题,这里最不宜理解,请跟紧个人思路。
第一,这个方案必需要求要操做的「共享资源服务器」有拒绝「旧 token」的能力。
例如,要操做 MySQL,从锁服务拿到一个递增数字的 token,而后客户端要带着这个 token 去改 MySQL 的某一行,这就须要利用 MySQL 的「事物隔离性」来作。
// 两个客户端必须利用事物和隔离性达到目的
// 注意 token 的判断条件
UPDATE table T SET val = $new_val, current_token = $token WHERE id = $id AND current_token < $token
复制代码
但若是操做的不是 MySQL 呢?例如向磁盘上写一个文件,或发起一个 HTTP 请求,那这个方案就无能为力了,这对要操做的资源服务器,提出了更高的要求。
也就是说,大部分要操做的资源服务器,都是没有这种互斥能力的。
再者,既然资源服务器都有了「互斥」能力,那还要分布式锁干什么?
因此,Redis 做者认为这个方案是站不住脚的。
第二,退一步讲,即便 Redlock 没有提供 fecing token 的能力,但 Redlock 已经提供了随机值(就是前面讲的 UUID),利用这个随机值,也能够达到与 fecing token 一样的效果。
如何作呢?
Redis 做者只是提到了能够完成 fecing token 相似的功能,但却没有展开相关细节,根据我查阅的资料,大概流程应该以下,若有错误,欢迎交流~
仍是以 MySQL 为例,举个例子就是这样的:
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 做者的结论:
一、做者赞成对方关于「时钟跳跃」对 Redlock 的影响,但认为时钟跳跃是能够避免的,取决于基础设施和运维。
二、Redlock 在设计时,充分考虑了 NPC 问题,在 Redlock 步骤 3 以前出现 NPC,能够保证锁的正确性,但在步骤 3 以后发生 NPC,不止是 Redlock 有问题,其它分布式锁服务一样也有问题,因此不在讨论范畴内。
是否是以为颇有意思?
在分布式系统中,一个小小的锁,竟然可能会遇到这么多问题场景,影响它的安全性!
不知道你看完双方的观点,更赞同哪一方的说法呢?
别急,后面我还会综合以上论点,谈谈本身的理解。
好,讲完了双方对于 Redis 分布锁的争论,你可能也注意到了,Martin 在他的文章中,推荐使用 Zookeeper 实现分布式锁,认为它更安全,确实如此吗?
若是你有了解过 Zookeeper,基于它实现的分布式锁是这样的:
你应该也看到了,Zookeeper 不像 Redis 那样,须要考虑锁的过时时间问题,它是采用了「临时节点」,保证客户端 1 拿到锁后,只要链接不断,就能够一直持有锁。
并且,若是客户端 1 异常崩溃了,那么这个临时节点会自动删除,保证了锁必定会被释放。
不错,没有锁过时的烦恼,还能在异常时自动释放锁,是否是以为很完美?
其实否则。
思考一下,客户端 1 建立临时节点后,Zookeeper 是如何保证让这个客户端一直持有锁呢?
缘由就在于,客户端 1 此时会与 Zookeeper 服务器维护一个 Session,这个 Session 会依赖客户端「定时心跳」来维持链接。
若是 Zookeeper 长时间收不到客户端的心跳,就认为这个 Session 过时了,也会把这个临时节点删除。
一样地,基于此问题,咱们也讨论一下 GC 问题对 Zookeeper 的锁有何影响:
可见,即便是使用 Zookeeper,也没法保证进程 GC、网络延迟异常场景下的安全性。
这就是前面 Redis 做者在反驳的文章中提到的:若是客户端已经拿到了锁,但客户端与锁服务器发生「失联」(例如 GC),那不止 Redlock 有问题,其它锁服务都有相似的问题,Zookeeper 也是同样!
因此,这里咱们就能得出结论了:一个分布式锁,在极端状况下,不必定是安全的。
若是你的业务数据很是敏感,在使用分布式锁时,必定要注意这个问题,不能假设分布式锁 100% 安全。
好,如今咱们来总结一下 Zookeeper 在使用分布式锁时优劣:
Zookeeper 的优势:
但它的劣势是:
好了,前面详细介绍了基于 Redis 的 Redlock 和 Zookeeper 实现的分布锁,在各类异常状况下的安全性问题,下面我想和你聊一聊个人见解,仅供参考,不喜勿喷。
1) 到底要不要用 Redlock?
前面也分析了,Redlock 只有创建在「时钟正确」的前提下,才能正常工做,若是你能够保证这个前提,那么能够拿来使用。
但保证时钟正确,我认为并非你想的那么简单就能作到的。
第一,从硬件角度来讲,时钟发生偏移是时有发生,没法避免的。
例如,CPU 温度、机器负载、芯片材料都是有可能致使时钟发生偏移。
第二,从个人工做经从来说,曾经就遇到过期钟错误、运维暴力修改时钟的状况发生,进而影响了系统的正确性,因此,人为错误也是很难彻底避免的。
因此,我对 Redlock 的我的见解是,尽可能不用它,并且它的性能不如单机版 Redis,部署成本也高,我仍是会优先考虑使用 Redis「主从+哨兵」的模式,实现分布式锁。
那正确性如何保证呢?第二点给你答案。
2) 如何正确使用分布式锁?
在分析 Martin 观点时,它提到了 fecing token 的方案,给我了很大的启发,虽然这种方案有很大的局限性,但对于保证「正确性」的场景,是一个很是好的思路。
因此,咱们能够把这二者结合起来用:
一、使用分布式锁,在上层完成「互斥」目的,虽然极端状况下锁会失效,但它能够最大程度把并发请求阻挡在最上层,减轻操做资源层的压力。
二、但对于要求数据绝对正确的业务,在资源层必定要作好「兜底」,设计思路能够借鉴 fecing token 的方案来作。
两种思路结合,我认为对于大多数业务场景,已经能够知足要求了。
好了,总结一下。
这篇文章,咱们主要探讨了基于 Redis 实现的分布式锁,到底是否安全这个问题。
从最简单分布式锁的实现,处处理各类异常场景,再到引出 Redlock,以及两个分布式专家的辩论,得出了 Redlock 的适用场景。
最后,咱们还对比了 Zookeeper 在作分布式锁时,可能会遇到的问题,以及与 Redis 的差别。
这里我把这些内容总结成了思惟导图,方便你理解。
这篇文章的信息量实际上是很是大的,我以为应该把分布锁的问题,完全讲清楚了。
若是你没有理解,我建议你多读几遍,并在脑海中构建各类假定的场景,反复思辨。
在写这篇文章时,我又从新研读了两位大神关于 Redlock 争辩的这两篇文章,可谓是是收获满满,在这里也分享一些心得给你。
一、在分布式系统环境下,看似完美的设计方案,可能并非那么「严丝合缝」,若是稍加推敲,就会发现各类问题。因此,在思考分布式系统问题时,必定要谨慎再谨慎。
二、从 Redlock 的争辩中,咱们不要过多关注对错,而是要多学习大神的思考方式,以及对一个问题严格审查的严谨精神。
最后,用 Martin 在对于 Redlock 争论事后,写下的感悟来结尾:
“前人已经为咱们创造出了许多伟大的成果:站在巨人的肩膀上,咱们能够才得以构建更好的软件。不管如何,经过争论和检查它们是否经得起别人的详细审查,这是学习过程的一部分。但目标应该是获取知识,而不是为了说服别人,让别人相信你是对的。有时候,那只是意味着停下来,好好地想想。”
共勉。
想看更多硬核技术文章?欢迎关注个人公众号「水滴与银弹」。
我是 Kaito,是一个对于技术有思考的资深后端程序员,在个人文章中,我不只会告诉你一个技术点是什么,还会告诉你为何这么作?我还会尝试把这些思考过程,提炼成通用的方法论,让你能够应用在其它领域中,作到触类旁通。
参考文献: