【易客吧】_全网激活码总代_激活码商城

您现在的位置是:首页 > 热门资讯 > 正文

热门资讯

Redis 分布式锁在高并发场景中的实战应用 (redis分片原理)

用户投稿2024-04-09热门资讯22

简介

在分布式系统中,确保数据的一致性和并发控制至关重要。分布式锁是一种用于协调分布式系统中对共享资源的访问的技术。它允许在多个节点上控制对资源的访问,防止并发冲突。Redis 是一个流行的分布式缓存,它提供了使用 SETNX 命令轻松实现分布式锁的功能。

Redis 分片原理

Redis 分片是一种方法,用于将大型 Redis 实例拆分为多个较小的实例。这有助于提高性能和可扩展性,特别是在高并发场景中。Redis 分片通过将键空间分成多个子空间来工作的。每个子空间映射到一个特定的 Redis 实例。当客户端访问键时,Redis 根据键将客户端定向到正确的分片。

Redis 分布式锁实现

使用 Redis 分片实现分布式锁,涉及以下步骤:1. 创建锁键: 创建一个唯一的 Redis 键,用于表示锁。2. 获取锁: 使用 SETNX

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秒,导致触发了刷新锁的过期时间操作。 注意(10, );时间要设置大一点,如果等待时间太短,小于获取锁redis命令的时间,那么就直接返回获取锁失败了。 分析源码我们了解Redisson模式的分布式,解决了锁过期时间和可重入的问题。 但是针对redis本身可能存在的单点失败问题,其实是没有解决的。 基于这个问题, redis作者提出了一种叫做Redlock算法, 但是这种算法本身也是有点问题的,想了解更多,请看基于Redis的分布式锁到底安全吗?

Redis怎么实现分布式锁

阿粉最近迷上了 Redis,为什么呢?感觉 Redis 确实功能很强大呀,一个基于内存的系统 Key-Value 存储的数据库,竟然有这么多的功能,而阿粉也要实实在在地把 Redis 来弄一下,毕竟面试的时候,Redis 可以说是一个非常不错的加分项。 为什么需要分布式锁? 目前很多的大型项目全部都是基于分布式的,而分布式场景中的数据一致性问题一直是一个不可忽视的问题,大家知道关于分布式的 CAP 理论么? CAP 理论就是说任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。 而我们的系统最终满足的永远都是最终一致性,而这种最终一致性,有些时候有人会喜欢问关于分布式事务,而有些人则偏重在分布式锁上。 但是阿粉选择的就是使用缓存来实现分布式锁,也就是我们在项目中最经常使用的 Redis ,谈到 Redis,那真是可以用在太多地方了,比如说: 我们今天就来实现用 Redis 来实现分布式锁,并且要学会怎么使用。 1.准备使用 Jedis 的 jar 包,在项目中导入 jar 包。 (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 分布式锁,大家学会了么?

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

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通信。

可以看到,我们加锁就一行代码(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,保证了只有锁的持有者才能来解锁,否则任何竞争者都能解锁,那岂不是乱套了。

Redis 分布式锁在高并发场景中的实战应用 (redis分片原理) 第1张

解锁的步骤:

注意到这里解锁其实是分为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加锁和解锁的代码如下:

若对本页面资源感兴趣,请点击下方或右方图片,注册登录后

搜索本页相关的【资源名】【软件名】【功能词】或有关的关键词,即可找到您想要的资源

如有其他疑问,请咨询右下角【在线客服】,谢谢支持!

Redis 分布式锁在高并发场景中的实战应用 (redis分片原理) 第2张

发表评论

评论列表

  • 这篇文章还没有收到评论,赶紧来抢沙发吧~
你上次访问网站的时间为:24-05-22 05:26:15 你第1访问网站的时间为:24-05-22 05:26:17