redis怎么实现分布式锁(redis实现分布式锁需要解决的问题)

本篇文章给大家谈谈redis怎么实现分布式锁,以及redis实现分布式锁需要解决的问题对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。

本文目录一览:

分布式锁的实现方式

分布式锁的实现方式如下:

1、基于数据库实现分布式锁:主要是利用数据库的唯一索引来实现,唯一索引天然具有排他性,这刚好符合我们对锁的要求:同一时刻只能允许一个竞争者获取锁。

2、基于缓存实现分布式锁:理论上来说使用缓存来实现分布式锁的效率最高,加锁速度最快。一般使用Redis来实现分布式锁都是利用Redis的SETNXkeyvalue这个命令散雹。

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

分布式锁应该具备的条件

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行冲昌帆。

2、高可用的获取锁与释放锁。

3、高性能的获取锁与释放锁。

4、具备可重入特性。

5、具备锁失效机制,防止死锁。

6、具备非阻塞锁特迅圆性,即没有获取到锁将直接返回获取锁失败。

redis 分布式锁

1、一个tomcat是一个进程,其中有很多线程(与有多少个app无关) 

2、一个tomcat启动一个JVM,其中可以有很多APP 

3、一个tomcat中部署的多个app,虽然同处一个JVM里,但是由于散源无法相互调用,所以也可以认为是分布式的 

synchronized 只是本地锁啊,锁的也只是当前jvm下的对象,在分布式场景下,要用分布式锁。

redis 分布式锁应用场景: 程序不是在一台tomcat(不同jvm)或者一台 tomcat部署的多个由于无法相互调用,synchronized失效,此时操作共享变备掘桐量,例如库存,就要用分布式锁仿坦

简陋版:

解决key 失效时间小于业务执行时间问题

//放到启动类

redisson底层主要是lua脚本

原理图:

解决key 失效时间小于业务执行时间问题

使用lua后的效果:

redis 集群,主redis挂了,此时还没同步到从redis,怎么办?

可以使用zookeeper,它会等 其他的zookeeper同步加速成功再返回成功

redis没办法100%解决这个问题,可以容忍,redis性能远高于zookeeper

解决

1.可以使用redlock(不推荐,不完善):2.使用redission

高并发分布式锁实现:

将数据在redis里分段减库存

正确的Redis分布式锁实现(Distributed lock)

锁要实现的三个目标:

该命令仅在密钥尚不存在时才设置key(NX选项),到期时间为30000毫秒(PX选项)中罩。键设置为值 “myrandomvalue” 。此值必须在所有客户端和所有锁定并友请求中都是唯一的。

使用随机值是为了以安全的方式释放锁,实现上使用了Redis的脚本:只有当key存在且其值恰好是我期望的随机值时才删除key。这是通过以下Lua脚本完成的:

这一点很重要,这可以避免删除由另一个客户端创建的锁。例如:

ok,这样我们完成redis分布式锁的实现。你以为这样就安全了吗,接下来我们设想这样一个情况:

首先,假设我们的Redis是主从的:

这时就会同时有两把锁存在的情况。对于这种情况绝培槐(非常特殊),Redis官方提供了一个叫RedLock的解决方案。

我们假设我们有N个Redis主机。这些节点完全独立,因此我们不使用复制或任何其他隐式协调系统。我们假设N = 5,这是一个合理的值,因此我们需要在不同的计算机或虚拟机上运行5个Redis主服务器,以确保它们以大多数独立的方式失败。

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

我个人觉得对于小公司这还是相当昂贵的。

其他的细节请参考 redis官方文档 。

[img]

大厂面试题详解:如何用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 分布式锁,就该是这样实现的

众所周知,redis 分布式锁使用 SET 指令可以实现,但是仅仅使用该命令就行了吗?是否还需要考虑 CAP 理论。

要是有上面说的那么简单就好喽,我们平时在开发中用到的分布式锁方案可能比较简单,这个取决于业务的复杂程度以及并发量。

下面我们来说说在高并发场景中,该如何正确使用分布式锁。

在正式讲解分布式锁之前,先来看下将要围绕展开来讲的几个问题:

在同一时刻,只能有一个线程去读写一个【共享资源】,也就是高并发的场景下,通常为了保证数据的正确,需要控制同一时刻只允许一个线程访问。

此时就需要使用分布式锁了。

简而言之,分布式锁就是用来控制同一时刻,只有一个线程可以访问被保护的资源。

可信肆以使用 SETNX key value 命令实现互斥的特性。

解释下:如果 key 不存在,则设置 value 给这个 key ,否则啥都不做。

该命令的返回值有如下两种情况:

举例如下:

成功的情况:

SETNX lock:101 1 (integer) 1 # 获取 101 锁 成功

失败的情况:

SETNX lock:101 2 (integer) 0 # 后面申请锁 获取失败

可见,成功的就可以开始使用「共享资源」了。

使用结束后,要及时释放锁,给后面申请获得资源的机会。

释放锁比较简单,使用 DEL 命令删除这个 key 就可以了。如下:

DEL lock:101 (integer) 1

分布式锁简单使用方案如下:

这看起来不是挺简单的吗,能有什么问题?往下听我分析。

首先该方案存在一个锁无法被释放的问题,场景如下:

可见,这个锁就会一直被占用,导致其它客户端也拿不到这个锁了。

设置举例如下:

SETNX lock:101 1 // 获取锁 (integer) 1

EXPIRE lock:101 60 // 60s 过期删除 (integer) 1

可见,60 秒后后该锁就好释放掉,其他客户就可以申请使用察凳了。

由上面举例可知: 加锁 和 设置过期时间 是两个操作命令,并不是原子操作。

试想一下,可能存在这么个情况:

比如执行第一条命了成功,第二条命令还没来得及执行就出现了异常,导致设置 「 过期时间」失败,这样锁也是无法释放。

SET keyName value NX PX 30000

这样一看,似乎没啥毛病。不,仔细一看,写的还是不够严谨。想下,有没可能释放的不是自己加的锁。

思考中……

说下什么场景下, 释放的锁不是自己的 :

所以,有个关键点需要注意的是:只能释放自己申请的锁。

总之,解铃还须系铃人

伪代码如下:

// 判断 value 与 锁的唯一标识

此时,我们可以考虑通过 Lua 脚本来实现,这样判断和删除的过滑没轿程就是原子操作了。

// 获取锁的 value 值与 ARGV[1] 比较,匹配成功则执行 del

使用上面的脚本,为每个锁分配一个随机字符串“签名”,只有当删除锁的客户端的“签名”与锁的 value 匹配的时候,才会去删除它。

遇到问题不要慌,先从官方文档入手:redis.io/topics/dist…

到目前为止,以上修改后(优化后)的方案算相比较完善的了,业界大部分使用的也都是该方案。

当然这个锁的过期时间不能瞎写,通常是根据多次测试后的结果来做选择,比如压测多轮之后,平均执行时间在 300 ms。

那么我们的锁 过期时间就应该放大到平均执行时间的 3~4 倍。

有些小伙伴可能会问:为啥是放大 3~4 倍 呢 ?

这叫凡事留一手:考虑到锁的操作逻辑中如果有网络 IO 操作等,线上的网络不会总是稳定的,此时需要留点时间来缓冲。

加锁的时候设置一个过期时间,同时客户端开启一个「守护线程」,定时去检测这个锁的失效时间。

如果快要过期,但是业务逻辑还没执行完成,自动对这个锁进行续期,重新设置过期时间。

可以先谷歌一下,相信谷歌大哥会告诉你有这么一个库把这些工作都封装好了,你只管用就是了,它叫 Redisson 。

在使用分布式锁的时候,其实就是采用了「自动续期」的方案来避免锁过期,这个守护线程我们一般也把它叫做「看门狗」线程。

这个方案可以说很 OK 了,能想到这些的优化点已经击败一大批程序猿了。

对于追求极致的程序员来说,你们可能会考虑到:

这里就不展开讨论了。有兴趣的可以在评论区一起讨论交流哈。

关于redis怎么实现分布式锁和redis实现分布式锁需要解决的问题的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。

标签列表