百度
360搜索
搜狗搜索

redis分布式锁原理面试,大厂面试题详解:如何用Redis实现分布式锁?详细介绍

本文目录一览: 使用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实现分布式锁?

说一道常见面试题:

一个很简单的答案就是去使用 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 高可用的问题,都是使用主从集群。

阅读更多 >>>  harmonyos是什么意思

但是搞主从集群,又会引入新的问题。主要问题在于,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 集群做分布式锁,有一定的争议性,还需要大家在实际用的时候,根据现实情况,做出更好的选择和取舍。

原文 https://www.cnblogs.com/siyuanwai/p/16011836.html

分布式锁的三种实现方式面试

分布式锁三种实现方式:1、基于数据库实现分布式锁;2、基于缓存(Redis等)实现分布式锁;3、基于Zookeeper实现分布式锁。从性能角度(从高到低)来看:“缓存方式>Zookeeper方式>=数据库方式”。1。悲观锁:利用select?where?forupdate排他锁。所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有updateversion失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。

分布式锁的一些细节问题,值得收藏

原文
我看在技术大群里面有人问分布式锁问题,说是被面试官各种刁难,我是实在看不下去了,面试官意思很明确,你们有造火箭的经验吗,有的话来他们公司造自行车,没有的话:今天面试就到这里了!
但是说实话面试官没必要因为这个问题继续追问,没啥意思,但是又不得不问,确实有些企业用到分布式锁了。
没办法,逼迫你必须得背会它。
接下来就说下群友面试碰到的问题,因为候选人可能自己已经掌握了实现分布式锁的原理,但是被面试官问到细节可能就不清楚了,因此给大家讲下这块。
问题-1 如果setnx执行成功,但是在expire执行的时候redis节点宕机了,在这种情况下,锁不会被释放,导致死锁。解决方案:
问题-2 如果expire时间过短,但是任务执行时间过长,那么锁会因为过期而被删除,其它客户端可以重新获取锁。在这种情况下,多个客户端同时获取到了锁。解决方案:
问题-3 如果expire刚好过期,此时删除了这个key,那么当另一个客户端进行获取锁的时候就会抢到锁,那么这个时候当前释放锁的客户端会执行最后的del命令把别的客户端刚才设置的删除了,这个时候就破坏了资源的并发操作。
解决方案:

`1. uuid = gen()
问题-4 假如redis主节点宕机,主从同步延迟或者有问题,那么从成为主之后,客户端就会重新获取到锁,这样也会并发不安全。解决方案:
问题-5 如果redis发生脑裂,那么也会发生多个客户端并发持有多个锁的问题,所以redis为了解决这个脑裂问题,引入两个配置,只有合理配置这两个参数就可以尽最大努力避免脑裂,细节大家下去自行研究哈。


只要线程成功获取到锁,就会启动一个watch dog,它是一个后台线程,每10秒检查一次,如果线程一持有锁,那么它会不断延长锁的生存时间。因此,Redisson是可以解决过期时间到了但是业务还没执行完的问题。
Redlock核心思想是这样的:部署多个redis master节点,确保它们不会同时宕机。而且这些主节点之间是完全独立的,它们之间没有数据同步。同时,我们需要确保使用相同的方法来获取和释放锁。具体获取锁和释放锁的步骤大家下去自行研究。

Redis分布式锁的原理是什么?如何续期?

在传统单体应用单机部署的情况下,并发问题可以通过使用Java并发相关的锁如synchronized,但是当规模上升到分布式集群的情况下,要控制共享资源访问,就需要通过分布式锁来实现。常见的分布式锁方案如数据库乐观锁,Redis锁,zk锁等。



Redis分布式锁的原理
Redis分布式锁可以有多种方式实现但是其核心就是通过以下三个Redis命令组合实现。

SETNX SETNX key val 当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

Expire expire key timeout 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

Delete delete key 删除key

核心思想
使用setnx获取锁。如果成功取到锁,则使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁。

获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。



注意
上面为Redis的一个最简单的锁实现原理,实际中还需要考虑更多具体的情况作出相应的调整。如

上面的demo中,当集群系统时间不一致时会有问题

当服务器异常关闭或是重启,加锁后没来得急设置锁超时时间,如何避免死锁

实际开发环境中不确定的因素有很多,需要慢慢地去调整实践达到理想状态,可以考虑使用redisson框架来实现。
如何续期?
这个情况比较独特,出现这个问题的根本原因在于锁失效的时间小于业务处理的时间导致业务还没处理完毕锁就释放了。那么解决方案是合理地结合业务去设置锁失效的时间。



但是也有更好的方案就如前文提到的redisson,其中的可重入锁概念。

默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒。
以上就是redis锁的原理及续期的方式,希望我的回答能对你有所帮助。

redis分布式锁如何实现

redis分布式锁:1、实现原理利用redis中的set命令来实现分布式锁。从Redis 2.6.12版本开始,set可以使用下列参数:SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX] EX second :设置键的过期时间为second秒。 SET key value EX second效果等同于SETEX key second value 。PX millisecond :设置键的过期时间为millisecond毫秒。 SET key value PX millisecond效果等同于PSETEX key millisecond value 。NX :只在键不存在时,才对键进行设置操作。 SET key value NX效果等同于SETNX key value 。XX :只在键已经存在时,才对键进行设置操作。返回值:SET 在设置操作成功完成时,才返回OK 。如果设置了NX或者XX ,但因为条件没达到而造成设置操作未执行,那么命令返回空批量回复(NULL Bulk Reply)。命令:> SET key value EX ttl NX大致思想是:(a)SET lock currentTime+expireTime EX 600 NX,使用set设置lock值,并设置过期时间为600秒,如果成功,则获取锁;(b)获取锁后,如果该节点掉线,则到过期时间ock值自动失效;(c)释放锁时,使用del删除lock键值;使用redis单机来做分布式锁服务,可能会出现单点问题,导致服务可用性差,因此在服务稳定性要求高的场合,官方建议使用redis集群(例如5台,成功请求锁超过3台就认为获取锁),来实现redis分布式锁。详见RedLock。2、优点性能高,redis可持久化,也能保证数据不易丢失;redis集群方式提高稳定性。3、缺点使用redis主从切换时可能丢失部分数据。4、开源实现python版本的开源实现:python-redis-lock。redis分布式锁的具体实现方式:加锁和解锁的方式:private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * 尝试获取分布式锁 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public Boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { Jedis jedis = this.jedisPool.getResource(); String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } /** * 释放分布式锁 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public Boolean releaseDistributedLock(String lockKey, String requestId) { Jedis jedis = this.jedisPool.getResource(); String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }2、具体的应用try{String requestId = UUID.randomUUID().toString();Boolean flag = tryGetDistributedLock(lock,requestId,1000);int n = 0;while(!flag){ //如果没有获取锁,可以尝试下一个lock,如果都没有,则尝试 n 次,退出 ... if(n++>5){ throw new Exception("尝试获取锁失败");} ... }if(!flag){throw new Exception("尝试获取锁失败");}}catch(){}finally{ releaseDistributedLock(lock,requestId);}更多Redis相关知识,请访问Redis使用教程栏目!

Redis怎么实现分布式锁

阿粉最近迷上了 Redis,为什么呢?感觉 Redis 确实功能很强大呀,一个基于内存的系统 Key-Value 存储的数据库,竟然有这么多的功能,而阿粉也要实实在在地把 Redis 来弄一下,毕竟面试的时候,Redis 可以说是一个非常不错的加分项。

为什么需要分布式锁?

目前很多的大型项目全部都是基于分布式的,而分布式场景中的数据一致性问题一直是一个不可忽视的问题,大家知道关于分布式的 CAP 理论么?

CAP 理论就是说任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

而我们的系统最终满足的永远都是最终一致性,而这种最终一致性,有些时候有人会喜欢问关于分布式事务,而有些人则偏重在分布式锁上。

但是阿粉选择的就是使用缓存来实现分布式锁,也就是我们在项目中最经常使用的 Redis ,谈到 Redis,那真是可以用在太多地方了,比如说:

我们今天就来实现用 Redis 来实现分布式锁,并且要学会怎么使用。

1.准备使用 Jedis 的 jar 包,在项目中导入 jar 包。

jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 这个加锁的姿势才是我们最需要了解的,不然你用的时候都不知道怎么使用。

key:加锁的键,实际上就是相当于一个唯一的标志位,不同的业务,你可以使用不同的标志位进行加锁。

requestId:这个东西实际上就是用来标识他是哪一个请求进行的加锁,因为在分布式锁中,我们要知道一件事,就是加锁的和解锁的,必须是同一个客户端才可以。

而且还有一种比较经典的就是 B 把 A 的锁给释放了,导致释放混乱,如果你不加相同的请求,A 线程处理业务,执行了加锁,锁的过期时间是5s, B线程尝试获取锁,如果 A 处理业务时间超过5s,这时候 A 就要开始释放锁,而B在这时候没有检测到这个锁,从而进行了加锁,这时候加锁的时候,A还没处理完对应业务,当他处理完了之后,再释放锁的话,要是就是直接把 B 刚加的锁释放了,要么就是压根都没办法释放锁。

SET_IF_NOT_EXIST:看字面意思,如果 key 不存在,我们进行Set操作,如果存在,啥都不干,也就不在进行加锁。

SET_WITH_EXPIRE_TIME:是否过期

expireTime:这是给 key 设置一个过期的时间,万一你这业务一直被锁着了,然后之后的业务想加锁,你直接给一直持有这个这个锁,不进行过期之后的释放,那岂不是要凉了。

上面的方法中 tryGetDistributedLock 这个方法也就是我们通常使用的加锁的方法。

大家看到这个 script 的时候,会感觉有点奇怪,实际上他就是一个 Lua 的脚本,而 Lua 脚本的意思也比较简单。

其实这时候就有些人说,直接 del 删除不行么?你试试你如果这么写的话,你们的领导会不会把你的腿给你打断。

这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,也就是说,这锁就算不是我加的,我都能开,这怎么能行呢?

在这里给大家放一段使用的代码,比较简单,但是可以直接用到你们的项目当中

我们先把这个实现方式实现了,然后我们再来说说大家最不愿意看的理论知识,毕竟这理论知识是你面试的时候经常会被问到的。

分布式CAP理论:

加州大学伯克利分校的 Eric Brewer 教授在 ACM PODC 会议上提出 CAP 猜想。2年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP。之后,CAP 理论正式成为分布式计算领域的公认定理。

也就是说,在二十年前的时候,CAP 理论只是个猜想。结果两年之后被证实了,于是,大家在考虑分布式的时候,就有根据来想了,不再是空想了。

什么是分布式的 CAP 理论 ?

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项

这个和(Atomicity)不太一样,因为之前看有些人说,在 CAP 理论中的 A 和数据库事务中的 A 是一样的,单词都不一样,那能一样么?

Availability :分布式中的 A 表示的是可用性,也就是说服务一直可用,而且是正常响应时间。

而你在搭建分布式系统的时候,要保证每个节点都是稳定的,不然你的可用性就没有得到相对应的保证,也谈不上是什么分布式了。只能称之为一个伪分布式。

Consistency: 一致性

也就是说你的更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,这个如果你在使用 Redis 做数据展示的时候,很多面试官都会问你,那你们是怎么保证数据库和缓存的一致性的呢?

毕竟你只是读取的话,没什么问题,但是设计到更新的时候,不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

所以如果你对这个很感兴趣,可以研究一下,比如说:

如果你能在面试的时候把这些都给面试官说清楚,至少感觉你应该能达到你自己的工资要求。

Partition tolerance:分区容错性

分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

其实在 CAP 理论当中,我们是没有办法同时满足一致性、可用性和分区容错性这三个特性,所以有所取舍就可以了。

关于使用 Redis 分布式锁,大家学会了么?

Redisson实现分布式锁原理

如图所示啊,石杉大佬画的redisson分布式锁原理。 大概总结下,保证我们的key落到一个集群里,并且加锁操作是基于lua脚本的原子性操作,对于锁延迟由watch dog控制。
具体可以看 https://www.cnblogs.com/AnXinliang/p/10019389.html
如果你对某个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分布式锁

分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。举个不太恰当的例子:(推荐学习:Redis视频教程)假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推实现原理互斥性保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作安全性只有加锁的服务才能有解锁权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁那分布式锁就没啥意义了可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候忽然a持有的锁过期了,然后b去获得锁,因为a锁过期,b拿到锁,这时候a继续执行第二步进行解锁如果不加校验,就将b持有的锁就给删除了避免死锁出现死锁就会导致后续的任何服务都拿不到锁,不能再对共享资源进行任何操作了保证加锁与解锁操作是原子性操作这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁假设加锁操作,操作步骤分为两步:1,设置key set(key,value)2,给key设置过期时间假设现在a刚实现set后,程序崩了就导致了没给key设置过期时间就导致key一直存在就发生了死锁如何实现分布式锁实现分布式锁的方式有很多,只要满足上述条件的都可以实现分布式锁,比如数据库,redis,zookeeper,在这里就先讲一下如何使用redis实现分布式锁分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器,存储锁信息,这时候我们很容易就想到了Redis。首先我们要搭建一个Redis服务器,用Redis服务器来存储锁信息。使用redis实现分布式锁使用redis命令 set key value NX EX max-lock-time 实现加锁使用redis命令 EVAL 实现解锁在实现的时候要注意的几个关键点:1、锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁;2、同一时刻只能有一个线程获取到锁。几个要用到的redis命令:setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。get(key):获得key对应的value值,若不存在则返回nil。getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。expire(key, seconds):设置key-value的有效期为seconds秒。更多Redis相关技术文章,请访问Redis数据库使用入门教程栏目进行学习!

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分布式锁原理面试,大厂面试题详解:如何用Redis实现分布式锁?"浏览人数已经达到19次,如你需要查询该站的相关权重信息,可以点击进入"Chinaz数据" 查询。更多网站价值评估因素如:redis分布式锁原理面试,大厂面试题详解:如何用Redis实现分布式锁?的访问速度、搜索引擎收录以及索引量、用户体验等。 要评估一个站的价值,最主要还是需要根据您自身的需求,如网站IP、PV、跳出率等!