redis:redLock 古城微笑少年丶 2022-09-15 06:30 111阅读 0赞 # 分布式锁是什么 # 实际开发过程中,会遇到如下场景,不但要保证同一个key只能被一个客户端增删改操作,还要监控该key对应的value值操作,这时就需要用到分布式了 > 场景分析 还是双11热卖过程中,怎样避免最后一件商品不被多人同时购买(超卖问题) * watch监听能监听特定的key是否被修改,但是无法监听被修改的值,此处要监控的是具体的数据 * 虽然redis是单线程的,但是多个客户端对同一个数据同时进行操作时,如何避免不被同时修改呢? > 解决方案 * 虽然`setnx`设置一个公共锁`setnx lock-key value`,value可以为随机任意值 * `setnx`命令能返回value值,只有第一次执行的才会成功并返回1,其他情况返回0 * 如果返回是1,说明没有人持有锁,当前客户端设置锁成功,可以进行下一次具体业务操作 * 如果返回是0,说明有人持有了锁,当前客户端设置锁失败,那么需要排队或者等待锁的释放 * 操作完成通过del操作是否所 这是redis分布式锁的雏形,当然实际开发中需要考虑锁时效性避免死锁问题,还有避免锁误删问题,因此有接下来几种版本的分布式锁 # 分布式锁版本1 # ## 业务场景 ## > 场景分析 依赖分布式锁的机制,某个用户操作redis时对应的客户端宕机了,而且此时已经获取到了锁,导致锁一直被持有,其他客户端拿不到锁,这就是**死锁**,如何解决呢? * 由于锁操作由用户控制加锁解锁,必定会存在加锁未解锁的风险 * 需要解锁操作不能仅依赖用户控制,系统级别要给出对应的保底处理方案 > 解锁方案 1. 使用expire为锁key添加时间限制,到时不释放锁,则放弃锁 setnx lock-key 001 # 设置分布式锁 expire lock-key second # 设置单位为秒 pexpire lock-key milliseconds # 设置单位为毫秒 1. 或者直接设置的时候添加时间限制 set lock-key value NX PX 毫秒数 # 比如为key为name设置分布式锁 set lock-name 001 NX PX 5000 * value可以是任意值 * NX代码只有lock-key不存在时才设置值 > 实际开发中如何知道设置多少时间才合适 由于操作通过都是微秒或者毫秒级,因此锁设定时间不宜过大。具体时间需要业务测试后确定 比如:持有锁的操作最长执行时间127ms,最短执行时间7ms。 * 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时 * 时间设定推荐:最大耗时*120%+平均网络延迟*110% * 如果二者相差2个数量级,取其中单个耗时较长即可. ## 流程图 ## ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAT2NlYW4mJlN0YXI_size_14_color_FFFFFF_t_70_g_se_x_16] # 分布式锁版本2 # ## 业务场景 ## > 场景分析 * 三个进程:A和B和C,在执行任务,并争抢锁,此时A获取了锁,并设置自动过期时间为20ms * A开始执行业务,因为某种原因,业务阻塞,耗时超过了20ms,此时锁自动释放了. * B恰好此时开始尝试获取锁,因为锁已经自动释放,成功获取锁. * A此时业务执行完毕,执行释放锁逻辑(删除key),于是B的锁被释放了,而B其实还在执行业务. * 此时进程C尝试获取锁,也成功了,因为A把B的锁删除了。 问题(1):使用版本1的分布式锁, 有可能B和C同时获取到锁,违反了**锁只能被一个客户端持有的特性**. 如何解决这个问题呢?**我们应该在删除锁之前, 判断这个锁是否是自己设置的锁, 如果不是(例如自己的锁已经超时释放), 那么就不要删除了**. 问题(2):如何得知当前获取锁的是不是自己呢? > 解决方案 1. 我们可以在set 锁时,存入自己的信息!删除锁前, 判断下里面的值是不是与自己相等. 如果不等,就不要删除了. 2. 这里自己的信息通常是一个随机值+当前线程的id, 通过UUID.randomUUID().toString()+Thread.currentThread().getId()获取到. ## 业务流程 ## ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAT2NlYW4mJlN0YXI_size_14_color_FFFFFF_t_70_g_se_x_16 1] # 分布式锁版本3 # > 场景分析 就是实际开发过程中,一端代码内部会有嵌套方法,外层方法获取到锁之后,内层再去获取锁时由于锁已经存在了就无法获取了,但内层代码不执行完外层也释放不了锁啊,这就是**方法嵌套导致的死锁问题**,怎么解决呢? > 解决方案 1. 让锁成为可冲入锁,也就是外层代码获取到这把锁,内层代码可以获取到该锁 2. 获取时判断是不是自己的锁,如果是则继续使用,而且要记录重入的次数 3. 这里的锁不能使用之前的string类型作为lock-key的值了,所的value要使用hash结构 hset lock-key 线程信息, 重入次数(默认1) NX PX 毫秒数 key: lock-key value-key:线程信息 value-value:重入次数 ## 流程图 ## ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAT2NlYW4mJlN0YXI_size_17_color_FFFFFF_t_70_g_se_x_16] 下面我们假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20 获取锁的步骤: 1、判断lock是否存在 EXISTS lock * 返回1则存在,说明有人获取锁了,下面判断是不是自己的锁 * 判断当前线程id作为hashKey是否存在:HEXISTS lock threadId * 存在,说明是自己获取的锁,重入次数+1:HINCRBY lock threadId 1,去到步骤2 * 不存在,说明锁已经有了,且不是自己获取的,锁获取失败,end * 返回0则不存在,说明可以获取锁,并存入value值HSET key threadId 1 2、设置锁自动释放时间,EXPIRE lock 20、 释放锁的步骤: 1、判断当前线程id作为hashKey是否存在:HEXISTS lock threadId * 存在,说明锁还在,重入次数减1:HINCRBY lock threadId -1,获取新的重入次数. * 不存在,说明锁已经失效,不用管了 2、判断重入次数是否为0: * 为0,说明锁全部释放,删除key:DEL lock * 大于0,说明锁还在使用,重置有效时间:EXPIRE lock 20 # 集群模式的Redis分布式锁 # ## 为什么引入 ## 上面版本的锁有一个很大的缺点,就是它加锁时只能作用在一个redis节点上,即使redis通过sentinel保证高可用,如果这个master由于某些原因发送了主从切换,那么就会出现锁丢失的情况: * 在redis的master节点上拿到了锁 * 但是这个加锁的key还没有同步到slave节点 * master故障,发生故障转移,slave节点升级为master几点 * 导致锁丢失 正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。这种方法比原先单节点的方法更加安全,它可以保证一下特性: * 安全特性:互斥访问,即永远只能由一个客户端能够拿到锁 * 避免死锁:最终客户端都可以拿到锁,不会出现死锁的情况技术,即使原先锁住某资源的客户端宕机了会在出现了网络分区 * 容错性:只要大部分redis节点存活就可以正常提供服务 笔者认为,Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。 ## 使用场景 ## 多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击); ## 什么是红锁(Redlock) ## 这里的场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁 * 客户端获取服务器当前的时间t0,毫秒数 * 使用相同的key和具有唯一性的value(例如UUID)依次向5个实例获取锁。客户端在获取锁的时候自身设置一个远小于业务锁需要的持续时间的超时时间。举个例子,假设锁需要10s,超时时间可以设置成5-50毫秒。这是为了不要过长时间等待已经关闭的Redis服务。并且试着获取下一个Redis实例。超时之后就直接跳到下一个结点。 * 客户端通过获取所有能获取的锁后的时间减去第一步的时间t2(=t1-t0)。只有t2小于锁的业务有效时间(也就是第二步的10秒),并且,客户端在至少3(5/2+1)台上获取到锁我们才认为锁获取成功。 * 如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移); * 如果客户端没有获取到锁,可能是没有在大于等于N/2+1个实例上获取锁,也可能是有效时间(10s-t2)为负数,我们就必须去释放所有redis上的锁,否则影响其他client获取锁 > 假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。 [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAT2NlYW4mJlN0YXI_size_14_color_FFFFFF_t_70_g_se_x_16]: /images/20220828/afef80a27fd4480bbe0c590aa665d423.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAT2NlYW4mJlN0YXI_size_14_color_FFFFFF_t_70_g_se_x_16 1]: /images/20220828/9b2b6f07cfa2402199570e8c3896a192.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBAT2NlYW4mJlN0YXI_size_17_color_FFFFFF_t_70_g_se_x_16]: /images/20220828/d4ffb7b69a0a4ba4a132388bb0890a3c.png
还没有评论,来说两句吧...