redis锁机制原理(redis锁实现秒杀)

本篇文章给大家谈谈redis锁机制原理,以及redis锁实现秒杀对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。

本文目录一览:

Redis分布式锁的原理与面试细节

meta name="source" content="lake"

这里就讲了下怎么加锁的,很多原理的问题小伙伴们,可用百度下分布式锁,看图中我特别在加锁与删除锁的时候还有俩个指向就特别说下这俩个问题

我们加锁的时候为了防止死锁的问题都在加锁散锋的时候会带上 锁过期时间的问题我们使用Redis提供的设置值的时候跟设置过期时间是原子性的操命令

加锁时候的原子性问题我们解决了,我们知道分布式锁就是只有一个线程才能抢到锁位,那其他线程怎么处理呢?有些文章可能都只说了一些流程却忘记了很多坑

加锁失败的几个解决办法【这也叫锁冲突的问题】

加锁过程我们处理好了,那么删除锁的时候呢?

删冲宴晌除锁的时候我们既要防止删除是别人锁有要当业务流程执行时间大于加锁的时间问题

删除锁的原子性就我们依靠了Lua脚本来实现删除锁的原子性

Redis锁超时问题呢?

使用过或者了解过Redisson的小伙伴知道Redisson框祥灶架实现分布式锁有一个看门狗机制,当业务流程大于加锁时间的时候,看门狗机制为在加锁的时间上在添加10秒

我们就来看看Redisson的加锁实现就可用到Redisson的看门狗机制跟分布式的可重入

【重点主要是依赖lua脚本的原子性,实现加锁和释放锁的功能】

使用redisson实现分布式锁的操作步骤,三部曲

第一步: 获取锁 RLock redissonLock = redisson.getLock(lockKey);

第二步: 加锁,实现锁续命功能 redissonLock.lock();

第三步:释放锁 redissonLock.unlock();

重点的地方我都标出来了

我们看下RedissonLock构造函数

参数:

继续看 lockInterruptibly方法

继续往里面追

大流程已经梳理完了,我们看下 Long ttl = tryAcquire(leaseTime, unit, threadId); 看门狗机制了

ARGV[2] --------- getLockName(threadId) 实现如下

这个id就是自开始实例化RedissonLock的id ,是个UUID

我们来解释下这段lua脚本【讲的可重入逻辑】

那继续监听时间中的 scheduleExpirationRenewal(threadId); 逻辑【看门狗续命】

重点看 unlockInnerAsync(Thread.currentThread().getId())

又是lua脚本,核心就是 把value减到为0 ,删除key

Redisson的逻辑参考

附上流程图

[img]

如何使用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 分布式锁详细分析

锁的作用,我想大家都理解,就是让不同的线程或者进程可以安全地操作共享资源,而不会产生冲突。

比较熟悉的就是 Synchronized 和缓汪戚 ReentrantLock 等,这些可以保证同一个 jvm 程序中,不同线程安全操作共享资源。

但是在分布式系统中,这种方式就失效了;由于分布式系统多线程、多进程并且分布在不同机器上,这将使单机并发控制锁策略失效,为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问。

比较常用的分布式锁有三种实现方式:

本篇文章主要讲解基于 Redis 分布式锁的实现。

分布式锁最主要的作用就是保证任意一个时刻,只有一个客户端能访问共享资源。

我们知道 redis 有 SET key value NX 命令,仅在不存在 key 的时候才能被执行成功,保证多个客户端只有一个能执行成功,相当于获取锁。

释放锁的时候,只需要删除 del key 这个 key 就行了。

上面的实现看似已经满足要求了,但是忘了考虑在分布式环境下,有以下问题:

最大的问题就是因为客户端或者网络问题,导致 redis 中的 key 没有删除,锁无法释放,因此其他客户端无法获取到锁。

针对上面的情况,使用了下面命令:

使用 PX 的命令,给 key 添加一个自动过期时间(30秒),保证即使因为意外情况,没有调用释放锁的方法,锁也会自动释放,其他客户端仍然可以获取到锁。

注意给这个 key 设置的值 my_random_value 是一个随机值,而且必须保证这个值在客户端必须是唯一的。这个值的作用是为了更加安全地释放锁。

这是为了避免删除其他客户端成功获取的锁。考虑下面情况:

因此这里使用一个 my_random_value 随机值,保证客户端只会释放自己获取的锁,即只删除自己设置的 key 。

这种实现方式,存在下面问题:

上面章节介绍了,简单实现存在的问题,下面来介绍一下 Redisson 实现又是怎么解决的这些问题的。

主要关陵稿注 tryAcquireOnceAsync 方法,有三个参数:

方法主要流程:

这个方法的流程与 tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法基本相同。

这个方法与 tryAcquireOnceAsync 方法的区别,就是一个获取锁过期时间,一个是能否获取锁。即 获取锁过期扰陵时间 为 null 表示获取到锁,其他表示没有获取到锁。

获取锁最终都会调用这个方法,通过 lua 脚本与 redis 进行交互,来实现分布式锁。

首先分析,传给 lua 脚本的参数:

lua 脚本的流程:

为了实现无限制持有锁,那么就需要定时刷新锁的过期时间。

这个类最重要的是两个成员属性:

使用一个静态并发集合 EXPIRATION_RENEWAL_MAP 来存储所有锁对应的 ExpirationEntry ,当有新的 ExpirationEntry 并存入到 EXPIRATION_RENEWAL_MAP 集合中时,需要调用 renewExpiration 方法,来刷新过期时间。

创建一个超时任务 Timeout task ,超时时间是 internalLockLeaseTime / 3 , 过了这个时间,即调用 renewExpirationAsync(threadId) 方法,来刷新锁的过期时间。

判断如果是当前线程持有的锁,那么就重新设置过期时间,并返回 1 即 true 。否则返回 0 即 false 。

通过调用 unlockInnerAsync(threadId) 来删除 redis 中的 key 来释放锁。特别注意一点,当不是持有锁的线程释放锁时引起的失败,不需要调用 cancelExpirationRenewal 方法,取消定时,因为锁还是被其他线程持有。

传给这个 lua 脚本的值:

这个 lua 脚本的流程:

调用了 LockPubSub 的 subscribe 进行订阅。

这个方法的作用就是向 redis 发起订阅,但是对于同一个锁的同一个客户端(即 一个 jvm 系统) 只会发起一次订阅,同一个客户端的其他等待同一个锁的线程会记录在 RedissonLockEntry 中。

方法流程:

只有当 counter = permits 的时候,回调 listener 才会运行,起到控制 listener 运行的效果。

释放一个控制量,让其中一个回调 listener 能够运行。

主要属性:

这个过程对应的 redis 中监控的命令日志:

因为看门狗的默认时间是 30 秒,而定时刷新程序的时间是看门狗时间的 1/3 即 10 秒钟,示例程序休眠了 15 秒,导致触发了刷新锁的过期时间操作。

注意 rLock.tryLock(10, TimeUnit.SECONDS); 时间要设置大一点,如果等待时间太短,小于获取锁 redis 命令的时间,那么就直接返回获取锁失败了。

分析源码我们了解 Redisson 模式的分布式,解决了锁过期时间和可重入的问题。但是针对 redis 本身可能存在的单点失败问题,其实是没有解决的。

基于这个问题, redis 作者提出了一种叫做 Redlock 算法, 但是这种算法本身也是有点问题的,想了解更多,请看 基于Redis的分布式锁到底安全吗?

Redis红锁

在算法的分布式版本中,我们假设我们有N个Redis主节点。这些节点是完全独立的,因此我们不使用复制或任何其他隐式协调系统。我们已经描述了如何在单个实例中安全地获取和释放锁。我们理所当然地认为,算法将使用此方法在单个实例中获取和释放锁。在我们的示例中,我们设置了 N=5,这是一个合理的值,因此我们需要在不同的计算机或虚拟机上运行 5 个 Redis 主节点,以确保它们以一种基本独立的方式失败。

为了获取锁,客户端执行以下操作:

该算法依赖于以下假设:虽然进程之间没有同步时钟,但每个进程中的本地时间以大致相同的速率更新,与锁的自动释放时间相比,误差幅度很小。这个假设与现实世界的计算机非常相似:每台计算机都有一个本地时钟,我们通常可以依靠不同的计算机来具有很小的时钟漂移。

在这一点上,我们需要更好地指定我们的互斥规则:只要持有锁的客户端在锁有效时间内(如步骤3中获得的那哗茄核样)终止其工作,减去一些时间(只需几毫秒,以补偿进程之间的时钟漂移),它才能得到保证。

本文包含有关需要绑定 时钟漂移 的类似系统的更多信息: Leases:分布式文件缓存一致性的高效容错机制 。

当客户端无法获取锁时,它应该在随机延迟后重试,以便尝试取消同步多个客户端,尝试同时获取同一资源的锁(这可能会导致没有人获胜的裂脑情况)。此外,客户端在大多数 Redis 实例中尝试获取锁的速度越快,裂脑情况的窗口就越小(并且需要重试),因此理想情况下,客户端应尝试使用多路复用同时将 SET 命令发送到 N 个实例。

值得强调的是,对于未能获取大多数锁的客户端来说,尽快释放(部分)获取的锁是多么重要,这样就不需要等待密钥到期才能再次获取锁(但是,如果发生网络分区并且客户端不再能够与 Redis 实例通信, 在等待密钥过期时需要支付可用性罚款)。

释放锁很简单,无论客户端是否认为它纳段能够成功锁定给定实例,都可以执行。

算法安全吗?让我们来看看在不同场景中会发生什么。

首先,我们假设客户端能够在大多数实例中获取锁。所有实例都将包含一个具有相同生存时间的密钥。但是,密钥是在不同的时间设置的,因此密钥也会在不同的时间过期。但是,如果在时间 T1(我们在联系第一台服务器之前采样之前采样的时间)将第一个键设置为最差,而在时间乱掘 T2(我们从最后一个服务器获得回复的时间)将最后一个键设置为最差,则我们确信集中第一个过期的密钥将至少存在 。所有其他密钥稍后将过期,因此我们确信至少这次将同时设置这些密钥。 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT

在设置大多数密钥期间,另一个客户端将无法获取锁,因为如果 N/2+1 密钥已存在,则 N/2+1 SET NX 操作无法成功。因此,如果获得了锁,则不可能同时重新获得它(违反互斥属性)。

但是,我们还希望确保尝试同时获取锁的多个客户端无法同时成功。

如果客户端锁定大多数实例的时间接近或大于锁定最大有效时间(我们基本上用于 SET 的 TTL),它将认为锁定无效并将解锁实例,因此我们只需要考虑客户端能够在小于有效时间的时间内锁定大多数实例的情况。在这种情况下,对于上面已经表达的参数,因为任何客户端都不应该能够重新获取锁。因此,仅当锁定大多数实例的时间大于 TTL 时间时,多个客户端才能同时锁定 N/2+1 个实例(“时间”是步骤 2 的结束),从而使锁定无效。 MIN_VALIDITY

系统活动性基于三个主要功能:

但是,我们支付的可用性损失等于网络分区上的 TTL 时间,因此,如果有连续分区,我们可以无限期地支付此罚款。每次客户端获取锁并在能够删除锁之前进行分区时,都会发生这种情况。

基本上,如果存在无限连续的网络分区,则系统可能会在无限长的时间内变得不可用。

许多使用 Redis 作为锁服务器的用户在获取和释放锁的延迟以及每秒可以执行的获取/释放操作数方面都需要高性能。为了满足这一要求,与N Redis服务器对话以减少延迟的策略肯定是多路复用(将套接字置于非阻塞模式,发送所有命令,稍后读取所有命令,假设客户端和每个实例之间的RTT相似)。

但是,如果我们想以崩溃恢复系统模型为目标,则关于持久性还有另一个考虑因素。

基本上,为了看到这里的问题,让我们假设我们配置Redis时根本没有持久性。客户端在 5 个实例中的 3 个实例中获取锁。客户端能够获取锁的其中一个实例重新启动,此时我们可以为同一资源锁定3个实例,而另一个客户端可以再次锁定它,这违反了锁的独占性的安全属性。

如果我们启用AOF持久性,事情将会有所改善。例如,我们可以通过向服务器发送 SHUTDOWN 命令并重新启动它来升级服务器。由于 Redis 过期在语义上是实现的,因此当服务器关闭时,时间仍然会过去,因此我们所有的要求都很好。但是,只要它是干净关闭,一切都很好。停电怎么办?如果 Redis 配置为(默认情况下)每秒在磁盘上同步一次,则重新启动后,我们的密钥可能会丢失。从理论上讲,如果我们想在面对任何类型的实例重启时保证锁定安全,我们需要在持久性设置中启用。由于额外的同步开销,这将影响性能。 fsync=always

然而,事情比乍一看要好。基本上,只要实例在崩溃后重新启动,它就不再参与任何 当前活动的 锁定,算法安全性就会保留。这意味着实例重新启动时的当前活动锁定集都是通过锁定重新加入系统的实例以外的实例而获得的。

为了保证这一点,我们只需要在崩溃后使一个实例不可用,至少比我们使用的最大 TTL 多一点。这是实例崩溃时存在的有关锁的所有密钥变得无效并自动释放所需的时间。

使用 延迟重启 ,即使没有任何可用的Redis持久性,基本上也可以实现安全,但请注意,这可能会转化为可用性损失。例如,如果大多数实例崩溃,系统将全局不可用 TTL (此处全局意味着在此期间根本没有资源可锁定)。

如果客户端执行的工作由小步骤组成,则默认情况下可以使用较小的锁定有效期,并扩展实现锁定扩展机制的算法。基本上,如果客户端在计算过程中锁定有效性接近较低值,则可以通过将Lua脚本发送到所有实例来扩展锁定,该实例扩展密钥的TTL(如果密钥存在并且其值仍然是客户端在获取锁定时分配的随机值)。

只有当客户端能够将锁扩展到大多数实例中,并且在有效时间内(基本上要使用的算法与获取锁时使用的算法非常相似),才应考虑重新获取锁。

但是,这在技术上不会更改算法,因此应限制锁定重新获取尝试的最大次数,否则会违反其中一个活动属性。

使用redis实现的分布式锁原理是什么?

一、写在前面

现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事务、分布式锁、ZooKeeper等知识。

所以咱们这篇文章就来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理。

说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。

大家如果有兴趣,可以去看看Redisson的官网,看看如何在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。

下面给大家看一段简单的使用代码片段,先直观的感受一下:

怎么样,上面那段代码,是不是感觉简单的不行!

此外,人家还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。

二、Redisson实现Redis分布式锁的底层原理

好的,接下来就通过一张手绘图,给大家说说Redisson这个开源框架对Redis分布式锁的实现原理。

(1)加锁机制

咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。

这里注意,仅仅只是选择一台机器!这点很关键!

紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:

为啥要用lua脚本呢?

因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。

那么,这段lua脚本是什么意思呢?

KEYS[1]代表的是你加锁的那个key,比如说:

RLock lock = redisson.getLock("myLock");

这里你自己设置了加锁的那个锁key就是“myLock”。

ARGV[1]代表的就是锁key的默认生存时间,默认30秒。

ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:

8743c9c0-0795-4907-87fd-6c719a6b4586:1

给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。

如何加锁呢?很简单,用下面的命令:

hset myLock

8743c9c0-0795-4907-87fd-6c719a6b4586:1 1

通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:

上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。

接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。

好了,到此为止,ok,加锁完成了。

(2)锁互斥机制

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?

很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。

此时客户端2会进入一个while循环,不停的尝试加锁。

(3)watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会孝扰启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

(4)可重入加锁悔慎茄机制

那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?

比如下面这种代码:

这时我们来分析一下上面那段lua脚本。

第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。

第二个if判断会成立,因为myLock的碧察hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行可重入加锁的逻辑,他会用:

incrby myLock

8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

通过这个命令,对客户端1的加锁次数,累加1。

此时myLock数据结构变为下面这样:

大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

(5)释放锁机制

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

然后呢,另外的客户端2就可以尝试完成加锁了。

这就是所谓的分布式锁的开源Redisson框架的实现机制。

一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

(6)上述Redis分布式锁的缺点

其实上面那种方案最大的问题,就是如果你对某个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锁机制原理和redis锁实现秒杀的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。

标签列表