2022
我们一起努力

为什么使用redis实现分布式锁(为什么redis能做分布式锁)

目录:

  • 1、Redisson实现分布式锁原理
  • 2、大厂面试题详解:如何用Redis实现分布式锁?
  • 3、redis 为什么需要分布式锁
  • 4、如何使用redis实现分布式锁功能?
  • 5、Redis的Setnx命令实现分布式锁
  • 6、高并发没锁可不行,三种分布式锁详解

Redisson实现分布式锁原理

如图所示啊,石杉大佬画的redisson分布式锁原理。

大概总结下,保证我们的key落到一个集群里,并且加锁操作是基于lua脚本的原子性操作,对于锁延迟由watch dog控制。

具体可以看

如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步**给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。

此时就会 导致多个客户端对一个分布式锁完成了加锁。

这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。

所以这个就是redis cluster,或者是redis master-slave架构的主从异步**导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

如果主动结构redis架构模式下,我们想保证完全一致,必须重写加锁的逻辑了, 保证必须mater和slave同时加锁成功,我们整个加锁才是成功的 。

上面的2是对于单个主从结构我们可以这样干,如果假设我们有多个相对独立的master,无slave呢?我们在其中一个master上加了🔐,然后它挂了岂不是我们的数据会在剩下的结点会重新加锁成功?

redis引入了 红锁 的概念:用Redis中的多个master实例,来获取锁,只有 大多数 实例获取到了锁,才算是获取成功 。

具体的红锁算法分为以下五步:

以上步骤来自redis 分布式锁的解释,如下

”相同的key和随机值(随机值用于唯一关系客户端和key)在N个节点上请求锁“

这里的随机数是什么?

这对于 避免删除由另一个客户端创建的锁 很重要。例如,客户端可能会获取锁,执行某些操作时被阻塞的时间超过锁的有效时间(密钥将过期的时间),然后移除已被其他客户端获取的锁。使用 just DEL 是不安全的,因为客户端可能会删除另一个客户端的锁。使用上面的脚本,每个锁都用一个随机字符串“签名”,所以只有当它仍然是客户端试图移除它时设置的锁才会被移除。

这个随机字符串应该是什么?我们假设它是 20 个字节 /dev/urandom ,但您可以找到更便宜的方法使其对您的任务足够独特。例如,一个安全的选择是用 RC4 播种 /dev/urandom ,并从中生成一个伪随机流。一个更简单的解决方案是使用具有微秒精度的 UNIX 时间戳,将 时间戳与客户端 ID 连接起来。它并不安全,但对于大多数环境来说可能就足够了。

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

为了应对这一问题,提出了 延迟重启 (delayed restarts)的概念。

就是,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。

这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

客户端1在获得锁之后发生了很长时间的GC pause,在此期间,它获得的锁过期了,而客户端2获得了锁。

当客户端1从GC pause中恢复过来的时候,它不知道自己持有的锁已经过期了,它依然向共享资源( 比如 一个存储服务)发起了写数据请求,

而这时锁实际上被客户端2持有,因此两个客户端的写请求就有可能冲突(锁的互斥作用失效了)。

如何解决这个问题呢?引入了 fencing token 的概念:

首先:RedLock根据随机字符串来作为单次锁服务的token,这就意味着对于资源而言,无法根据锁token来区分client持有的锁所获取的先后顺序。

fencing token可以理解成采用全局递增的序列替代随机字符串,即 有序token ,作为锁token来使用

流程:

假设有5个Redis节点A, B, C, D, E。

这个问题用Redis实现分布式锁暂时无解。而生产环境这种情况是存在的。

时钟跳跃是可以避免的,取决于基础设施和运维; 时钟跳跃是可以避免的,取决于基础设施和运维;

redis是保持的AP而非CP,如果要追求强一致性可以使用zookeeper分布式锁,但是zookeeper也不是完全没问题,在出现网络颜值,客户端与服务端失联情况的时候也依然可能会出现分布式的问题。

大厂面试题详解:如何用Redis实现分布式锁?

说一道常见面试题:

一个很简单的答案就是去使用 Redission 客户端。Redission 中的锁方案就是 Redis 分布式锁得比较完美的详细方案。

那么,Redission 中的锁方案为什么会比较完美呢?

正好,我用 Redis 做分布式锁经验十分丰富,在实际工作中,也 探索 过许多种使用 Redis 做分布式锁的方案,经过了无数血泪教训。

所以,在谈及 Redission 锁为什么比较完美之前,先给大家看看我曾经使用 Redis 做分布式锁是遇到过的问题。

我曾经用 Redis 做分布式锁是想去解决一个用户抢优惠券的问题。这个业务需求是这样的:当用户领完一张优惠券后,优惠券的数量必须相应减一,如果优惠券抢光了,就不允许用户再抢了。

在实现时,先从数据库中先读出优惠券的数量进行判断,当优惠券大于 0,就进行允许领取优惠券,然后,再将优惠券数量减一后,写回数据库。

当时由于请求数量比较多,所以,我们使用了三台服务器去做分流。

这个时候会出现一个问题:

如果其中一台服务器上的 A 应用获取到了优惠券的数量之后,由于处理相关业务逻辑,未及时更新数据库的优惠券数量;在 A 应用处理业务逻辑的时候,另一台服务器上的 B 应用更新了优惠券数量。那么,等 A 应用去更新数据库中优惠券数量时,就会把 B 应用更新的优惠券数量覆盖掉。

看到这里,可能有人比较奇怪,为什么这里不直接使用 SQL:

原因是这样做,在没有分布式锁的协调下,优惠券数量可能直接会出现负数。因为当前优惠券数量为 1 的时候,如果两个用户通过两台服务器同时发起抢优惠券的请求,都满足优惠券大于 0 每个条件,然后都执行这条 SQL 说了句,结果优惠券数量直接变成 -1 了。

还有人说可以用乐观锁,比如使用如下 SQL:

这种方式就在一定几率下,很可能出现数据一直更新不上,导致长时间重试的情况。

所以,经过综合考虑,我们就采用了 Redis 分布式锁,通过互斥的方式,以防止多个客户端同时更新优惠券数量的方案。

当时,我们首先想到的就是使用 Redis 的 setnx 命令,setnx 命令其实就是 set if not exists 的简写。

当 key 设置值成功后,则返回 1,否则就返回 0。所以,这里 setnx 设置成功可以表示成获取到锁,如果失败,则说明已经有锁,可以被视作获取锁失败。

如果想要释放锁,执行任务 del 指令,把 key 删除即可。

利用这个特性,我们就可以让系统在执行优惠券逻辑之前,先去 Redis 中执行 setnx 指令。再根据指令执行结果,去判断是否获取到锁。如果获取到了,就继续执行业务,执行完再使用 del 指令去释放锁。如果没有获取到,就等待一定时间,重新再去获取锁。

乍一看,这一切没什么问题,使用 setnx 指令确实起到了想要的互斥效果。

但是,这是建立在所有运行环境都是正常的情况下的。一旦运行环境出现了异常,问题就出现了。

想一下,持有锁的应用突然崩溃了,或者所在的服务器宕机了,会出现什么情况?

这会造成死锁——持有锁的应用无法释放锁,其他应用根本也没有机会再去获取锁了。这会造成巨大的线上事故,我们要改进方案,解决这个问题。

怎么解决呢?咱们可以看到,造成死锁的根源是,一旦持有锁的应用出现问题,就不会去释放锁。从这个方向思考,可以在 Redis 上给 key 一个过期时间。

这样的话,即使出现问题,key 也会在一段时间后释放,是不是就解决了这个问题呢?实际上,大家也确实是这么做的。

不过,由于 setnx 这个指令本身无法设置超时时间,所以一般会采用两种办法来做这件事:

1、采用 lua 脚本,在使用 setnx 指令之后,再使用 expire 命令去给 key 设置过期时间。

2、直接使用 set(key,value,NX,EX,timeout) 指令,同时设置锁和超时时间。

以上两种方法,使用哪种方式都可以。

释放锁的脚本两种方式都一样,直接调用 Redis 的 del 指令即可。

到目前为止,我们的锁既起到了互斥效果,又不会因为某些持有锁的系统出现问题,导致死锁了。这样就完美了吗?

假设有这样一种情况,如果一个持有锁的应用,其持有的时间超过了我们设定的超时时间会怎样呢?会出现两种情况:

出现第一种情况比较正常。因为你毕竟执行任务超时了,key 被正常清除也是符合逻辑的。

但是最可怕的是第二种情况,发现设置的 key 还存在。这说明什么?说明当前存在的 key,是另外的应用设置的。

这时候如果持有锁超时的应用调用 del 指令去删除锁时,就会把别人设置的锁误删除,这会直接导致系统业务出现问题。

所以,为了解决这个问题,我们需要继续对 Redis 脚本进行改动……毁灭吧,累了……

首先,我们要让应用在获取锁的时候,去设置一个只有应用自己知道的独一无二的值。

通过这个唯一值,系统在释放锁的时候,就能识别出这锁是不是自己设置的。如果是自己设置的,就释放锁,也就是删除 key;如果不是,则什么都不做。

脚本如下:

或者

这里,ARGV[1] 是一个可传入的参数变量,可以传入唯一值。比如一个只有自己知道的 UUID 的值,或者通过雪球算法,生成只有自己持有的唯一 ID。

释放锁的脚本改成这样:

可以看到,从业务角度,无论如何,我们的分布式锁已经可以满足真正的业务需求了。能互斥,不死锁,不会误删除别人的锁,只有自己上的锁,自己可以释放。

一切都是那么美好!!!

可惜,还有个隐患,我们并未排除。这个隐患就是 Redis 自身。

要知道,lua 脚本都是用在 Redis 的单例上的。一旦 Redis 本身出现了问题,我们的分布式锁就没法用了,分布式锁没法用,对业务的正常运行会造成重大影响,这是我们无法接受的。

所以,我们需要把 Redis 搞成高可用的。一般来讲,解决 Redis 高可用的问题,都是使用主从集群。

但是搞主从集群,又会引入新的问题。主要问题在于,Redis 的主从数据同步有延迟。这种延迟会产生一个边界条件:当主机上的 Redis 已经被人建好了锁,但是锁数据还未同步到从机时,主机宕了。随后,从机提升为主机,此时从机上是没有以前主机设置好的锁数据的——锁丢了……丢了……了……

到这里,终于可以介绍 Redission(开源 Redis 客户端)了,我们来看看它怎么是实现 Redis 分布式锁的。

Redission 实现分布式锁的思想很简单,无论是主从集群还是 Redis Cluster 集群,它会对集群中的每个 Redis,挨个去执行设置 Redis 锁的脚本,也就是集群中的每个 Redis 都会包含设置好的锁数据。

我们通过一个例子来介绍一下。

假设 Redis 集群有 5 台机器,同时根据评估,锁的超时时间设置成 10 秒比较合适。

第 1 步,咱们先算出集群总的等待时间,集群总的等待时间是 5 秒(锁的超时时间 10 秒 / 2)。

第 2 步,用 5 秒除以 5 台机器数量,结果是 1 秒。这个 1 秒是连接每台 Redis 可接受的等待时间。

第 3 步,依次连接 5 台 Redis,并执行 lua 脚本设置锁,然后再做判断:

再额外多说一句,在很多业务逻辑里,其实对锁的超时时间是没有需求的。

比如,凌晨批量执行处理的任务,可能需要分布式锁保证任务不会被重复执行。此时,任务要执行多长时间是不明确的。如果设置分布式锁的超时时间在这里,并没有太大意义。但是,不设置超时时间,又会引发死锁问题。

所以,解决这种问题的通用办法是,每个持有锁的客户端都启动一个后台线程,通过执行特定的 lua 脚本,去不断地刷新 Redis 中的 key 超时时间,使得在任务执行完成前,key 不会被清除掉。

脚本如下:

其中,ARGV[1] 是可传入的参数变量,表示持有锁的系统的唯一值,也就是只有持有锁的客户端才能刷新 key 的超时时间。

到此为止,一个完整的分布式锁才算实现完毕。总结实现方案如下:

这个分布式锁满足如下四个条件:

当然,在 Redission 中的脚本,为了保证锁的可重入,又对 lua 脚本做了一定的修改,现在把完整的 lua 脚本贴在下面。

获取锁的 lua 脚本:

对应的刷新锁超时时间的脚本:

对应的释放锁的脚本:

到现在为止,使用 Redis 作为分布式锁的详细方案就写完了。

我既写了一步一坑的坎坷经历,也写明了各个问题和解决问题的细节,希望大家看完能有所收获。

最后再给大家提个醒,使用 Redis 集群做分布式锁,有一定的争议性,还需要大家在实际用的时候,根据现实情况,做出更好的选择和取舍。

原文

redis 为什么需要分布式锁

比如:秒杀,全局递增ID,楼层生成等等。

大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

如何使用redis实现分布式锁功能?

由于redis是单线程的且性能很快,所以比较适合做全局分布式锁。

基本流程就是在操作可能某个全局冲突资源的时候,使用一个全局唯一key来判断是否有其他线程占用了资源,如果有其他线程占用,则报错退出或者循环等待。如果没有其他线程占用,则就可以通过添加分布式锁来占用这个资源,然后再执行后续的任务,在任务执行完成之后,再释放分布式锁,其他线程就可以继续使用这个资源了。

那么通过redis加锁的动作是什么呢?

简单加锁命令:

命令是:setnx

内部的实现机制就是判断这个key位置是不是有数据,没有数据就设置成value返回,有数据就返回一个特殊数值。

但是这里有一个问题是,如果占用资源的线程错误退出了,没有来得及释放分布式锁,这个锁就被永远的占用了

改进版的加锁:

命令是:1. setnx 2. expire

添加分布式锁的同时,添加一个锁锁过期的时间。这样,当加锁线程退出之后,至少等一段时间之后,锁是有机会释放掉的。

这里有一个小问题是,这两个命令是分开执行的,不是原子操作。那么就存在理论上来说,第一个命令执行完之后,就出现错误,来不及执行expire命令的可能,一种办法是自己写lua脚本,可以实现多条命令的原子化执行。一种办法是引用一些开源库。在2.8版本之后,redis为了解决这个问题,提供了官方版的解法,就是命令:set key value nx expireTimeNum ex,将上述两个命令合并成了一个命令。

有了过期时间之后解决了一部分问题,但是也有可能出现锁都过期了,但是中间执行的任务还没有结束,第一个线程还在执行了,第二个线程已经拿到锁开始执行了,那么这时候第一个线程如果执行完成之后,那么就会将第二个线程的锁释放掉了。第二个线程释放锁的时候,要不然出错,要不然是释放的其他线程的锁,这样也会和预期不符。

如果单纯地要解决这个问题的话,可以在设置value的时候使用一个随机数,释放锁的时候,先判断这个随机数是否一致,如果一致再删除锁,否则就退出。但是判断value和删除key也不是一个原子操作,这时候就需要使用lua脚本了。

上面的方案依然不能解决超时释放的问题,依然违背分布式锁的初衷。怎么办了?

解题思路是另外启动一个线程,它的任务就是每隔一段时间判断一下如果发现当前线程的任务快过期了还没有完成,则定期给当前线程的锁续个期。

有个开源库解决了这个问题,它大概率会比你实现得更好一些。这个库就是redisson,非常好记,就是redis的儿子son,连起来就是reidsson,虽然可能不是亲的,但是也足够了。

这个库里面有一个组件是watchdog,直译过来就是看门狗,它的作用就是每隔一段时间判断的。

再继续思考,还有一个更极端的问题是,redis如果是单节点的,它宕机了;或者是主备节点的,但是备份节点还没有来得及同步主节点的数据,主节点拿到锁之后,在同步数据之前就马上宕机了,则也有可能出现锁不住的问题。如果认为这是一个问题,想要解决这个问题,这个问题怎么解决了?

思路是在加锁的时候多加锁几台redis服务器,通常情况下redis部署的时候是2n+1台,那么在加锁的时候需要保证过半数服务器加锁成功了,也就是说n+1台服务器。这时候除非整个集群都不可用了,则这个安全性将大幅度提升。

这个问题也有开源库解决了,就是redis红锁。

下一个问题是分布式锁可以重入么?

如果想要实现可重入的分布式锁的话,需要在设置value的时候加上线程信息和加锁次数的信息。但是这是简单的思路,如果加上过期时间等问题之后,可重入锁就可能比较复杂了。

Redis的Setnx命令实现分布式锁

首先,分布式锁和我们平常讲到的锁原理基本一样,目的就是确保在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法、变量。

在一个进程中,也就是一个jvm或者说应用中,我们很容易去处理控制,在 java.util 并发包中已经为我们提供了这些方法去加锁,比如 synchronized 关键字或者 Lock 锁,都可以处理。

但是如果在分布式环境下,要保证多个线程同时只有1个能访问某个资源,就需要用到分布式锁。这里我们将介绍用Redis的 setnx 命令来实现分布式锁。

其实目前通常所说的 setnx 命令,并非单指redis的 setnx key value 这条命令,这条命令可能会在后期redis版本中删除。

一般代指redis中对 set 命令加上 nx 参数进行使用, set 这个命令,目前已经支持这么多参数可选:

从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

注入bean

这里同时启动5个线程并发往redis中存储 lock 这个key(key可以自定义,但需要一致),同时设置10秒的过期时间。

setIfAbsent 这个函数实现的功能与 setnx 命令一样,代表如果没有这个key则set成功获取到锁,否则set失败没有获取到锁。

获得锁后进行资源的操作,最后释放锁。

执行效果 :

可以看到同时只有1个线程能够获取到锁。

使用 setnx 命令方式虽然操作比较简单方便,但是会有如下问题:

可以在再次获取锁时,如果锁被占用就get值,判断值是否是当前线程存的随机值,如果是则再次执行 set 命令重新上锁;当然为了保证原子性这些操作都要用 lua 脚本来执行。

可以使用 while 循环重复执行 setnx 命令,并设置一个超时时间退出循环。

可以尽量把锁自动过期的时间设的冗余一些。但也不能彻底解决。

可以在删除锁的时候先get值,判断值是否是当前线程存的随机值,只有相同才执行删锁的操作;当然也要使用 lua 脚本执行来保证原子性。

分布式锁需要满足的特性

综上:使用 setnx 命令来实现分布式锁并不是一个很严谨的方案,如果是Java技术栈,我们可以使用 Redisson 库来解决以上问题,接下来的文章会介绍如何使用。

Redisson实现分布式锁

Redlock实现分布式锁

高并发没锁可不行,三种分布式锁详解

Java中的锁主要包括synchronized锁和JUC包中的锁,这些锁都是针对单个JVM实例上的锁,对于分布式环境如果我们需要加锁就显得无能为力。在单个JVM实例上,锁的竞争者通常是一些不同的线程,而在分布式环境中,锁的竞争者通常是一些不同的线程或者进程。如何实现在分布式环境中对一个对象进行加锁呢?答案就是分布式锁。

目前分布式锁的实现方案主要包括三种:

基于数据库实现分布式锁主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。加锁时我们在数据库中插入一条锁记录,利用业务id进行防重。当第一个竞争者加锁成功后,第二个竞争者再来加锁就会抛出唯一索引冲突,如果抛出这个异常,我们就判定当前竞争者加锁失败。防重业务id需要我们自己来定义,例如我们的锁对象是一个方法,则我们的业务防重id就是这个方法的名字,如果锁定的对象是一个类,则业务防重id就是这个类名。

基于缓存实现分布式锁:理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会涉及到磁盘文件IO,效率相对低下。一般使用Redis来实现分布式锁都是利用Redis的 SETNX key value 这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败。

基于Zookeeper:Zookeeper一般用作配置中心,其实现分布式锁的原理和Redis类似,我们在Zookeeper中创建瞬时节点,利用节点不能重复创建的特性来保证排他性。

在实现分布式锁的时候我们需要考虑一些问题,例如:分布式锁是否可重入,分布式锁的释放时机,分布式锁服务端是否有单点问题等。

上面已经分析了基于数据库实现分布式锁的基本原理:通过唯一索引保持排他性,加锁时插入一条记录,解锁是删除这条记录。下面我们就简要实现一下基于数据库的分布式锁。

id字段是数据库的自增id,unique_mutex字段就是我们的防重id,也就是加锁的对象,此对象唯一。在这张表上我们加了一个唯一索引,保证unique_mutex唯一性。holder_id代表竞争到锁的持有者id。

如果当前sql执行成功代表加锁成功,如果抛出唯一索引异常(DuplicatedKeyException)则代表加锁失败,当前锁已经被其他竞争者获取。

解锁很简单,直接删除此条记录即可。

是否可重入 :就以上的方案来说,我们实现的分布式锁是不可重入的,即是是同一个竞争者,在获取锁后未释放锁之前再来加锁,一样会加锁失败,因此是不可重入的。解决不可重入问题也很简单:加锁时判断记录中是否存在unique_mutex的记录,如果存在且holder_id和当前竞争者id相同,则加锁成功。这样就可以解决不可重入问题。

锁释放时机 :设想如果一个竞争者获取锁时候,进程挂了,此时distributed_lock表中的这条记录就会一直存在,其他竞争者无法加锁。为了解决这个问题,每次加锁之前我们先判断已经存在的记录的创建时间和当前系统时间之间的差是否已经超过超时时间,如果已经超过则先删除这条记录,再插入新的记录。另外在解锁时,必须是锁的持有者来解锁,其他竞争者无法解锁。这点可以通过holder_id字段来判定。

数据库单点问题 :单个数据库容易产生单点问题:如果数据库挂了,我们的锁服务就挂了。对于这个问题,可以考虑实现数据库的高可用方案,例如MySQL的MHA高可用解决方案。

使用Jedis来和Redis通信。

可以看到,我们加锁就一行代码:

jedis.set(String key, String value, String nxxx, String expx, int time);

这个set()方法一共五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,这里写的是锁竞争者的id,在解锁时,我们需要判断当前解锁的竞争者id是否为锁持有者。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作。

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期时间的设置,具体时间由第五个参数决定;

第五个参数为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1.当前没有锁(key不存在),那么久进行加锁操作,并对锁设置一个有效期,同时value表示加锁的客户端。2.已经有锁存在,不做任何操作。

上述解锁请求中, SET_IF_NOT_EXIST (不存在则执行)保证了加锁请求的排他性,缓存超时机制保证了即使一个竞争者加锁之后挂了,也不会产生死锁问题:超时之后其他竞争者依然可以获取锁。通过设置value为竞争者的id,保证了只有锁的持有者才能来解锁,否则任何竞争者都能解锁,那岂不是乱套了。

解锁的步骤:

注意到这里解锁其实是分为2个步骤,涉及到解锁操作的一个原子性操作问题。这也是为什么我们解锁的时候用Lua脚本来实现,因为Lua脚本可以保证操作的原子性。那么这里为什么需要保证这两个步骤的操作是原子操作呢?

设想:假设当前锁的持有者是竞争者1,竞争者1来解锁,成功执行第1步,判断自己就是锁持有者,这是还未执行第2步。这是锁过期了,然后竞争者2对这个key进行了加锁。加锁完成后,竞争者1又来执行第2步,此时错误产生了:竞争者1解锁了不属于自己持有的锁。可能会有人问为什么竞争者1执行完第1步之后突然停止了呢?这个问题其实很好回答,例如竞争者1所在的JVM发生了GC停顿,导致竞争者1的线程停顿。这样的情况发生的概率很低,但是请记住即使只有万分之一的概率,在线上环境中完全可能发生。因此必须保证这两个步骤的操作是原子操作。

是否可重入 :以上实现的锁是不可重入的,如果需要实现可重入,在 SET_IF_NOT_EXIST 之后,再判断key对应的value是否为当前竞争者id,如果是返回加锁成功,否则失败。

锁释放时机 :加锁时我们设置了key的超时,当超时后,如果还未解锁,则自动删除key达到解锁的目的。如果一个竞争者获取锁之后挂了,我们的锁服务最多也就在超时时间的这段时间之内不可用。

Redis单点问题 :如果需要保证锁服务的高可用,可以对Redis做高可用方案:Redis集群+主从切换。目前都有比较成熟的解决方案。

利用Zookeeper创建临时有序节点来实现分布式锁:

其基本思想类似于AQS中的等待队列,将请求排队处理。其流程图如下:

解决不可重入 :客户端加锁时将主机和线程信息写入锁中,下一次再来加锁时直接和序列最小的节点对比,如果相同,则加锁成功,锁重入。

锁释放时机 :由于我们创建的节点是顺序临时节点,当客户端获取锁成功之后突然session会话断开,ZK会自动删除这个临时节点。

单点问题 :ZK是集群部署的,主要一半以上的机器存活,就可以保证服务可用性。

Zookeeper第三方客户端curator中已经实现了基于Zookeeper的分布式锁。利用curator加锁和解锁的代码如下:

赞(0)
文章名称:《为什么使用redis实现分布式锁(为什么redis能做分布式锁)》
文章链接:https://www.fzvps.com/246967.html
本站文章来源于互联网,如有侵权,请联系管理删除,本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。
图片版权归属各自创作者所有,图片水印出于防止被无耻之徒盗取劳动成果的目的。

评论 抢沙发

评论前必须登录!