[分布式]-Redis实现分布式锁

分布式锁

简单来说分布式锁就是在分布式环境下,多个节点多个实例之间抢同一把锁

本地锁通常用于某个进程中多个线程之间对共享资源竞争访问需要保证同步的场景;那么分布式锁基本同理,只不过提供锁的服务在某个节点上,例如一台提供 Redis 服务的服务器,而竞争资源的对象变成了其它节点,实例;而且竞争时相关操作需要通过网络来完成

分布式锁的难点就在于网络,其实分布式系统中很多问题都来自于网络,网络是个永恒的问题,我们永远无法避开它

四个特性

  1. 互斥性:锁的目的是获取资源的使用权,所以只让一个竞争者持有锁,这一点要尽可能保证
  2. 安全性:避免死锁情况发生。当一个竞争者在持有锁期间内,由于意外崩溃而导致未能主动解锁,其持有的锁也能够被正常释放,并保证后续其它竞争者也能加锁
  3. 对称性:同一个锁,加锁和解锁必须是同一个竞争者。不能把其他竞争者持有的锁给释放了
  4. 可靠性:分布式锁服务需要有一定程度的异常处理能力与容灾能力。

最简单的实现

实现一个分布式锁的起点,就是利用 setnx 命令来设置一个键值对,这个命令可以确保排它性地设置键值对,即多并发的情况下只会有一个客户端能成功拿到锁。

setnx 的结果可能是成功,返回 1,即成功拿到锁了;
也可能是返回 0,失败,可能是别人先把锁拿到了;
也有可能请求超时了,根本没有到达锁服务器;
也可能请求发过去了,也确实拿到锁了,但是服务端的响应由于网络问题没有发回来。

所以最简单实现就是,加锁操作为 setnx 命令设置一个 key,value 任意;然后解锁操作就是 del 掉这个 key 即可

看上去没有任何问题,多节点可以同步地获取分布式锁。但分布式系统中我们不得不考虑的是节点宕机的问题。如果节点加锁后宕机了无法正常解锁,那么这个分布式锁就会一直处于加锁的状态,其它节点就无法获取锁

所以为了解决这个问题,节点加锁时需要指定超时时间,如果锁超过超时时间还没被解锁就自动解锁。很多因素都是不确定,但时间是确定的,所以经常被拿来兜底

超时

加入分布式锁超时机制,可以防止加锁的节点宕机导致锁无法及时解锁的问题,所以分布式锁是一定需要设置过期时间的。但又会出现新的问题。假设节点加锁后,因为业务处理时间太长,或者 GC,网络延迟等问题,导致锁超时自动解锁了,而且被第二个节点加了锁。

那么当第一个节点稍后要来解锁时,它可能会解锁成功,但是解的却不是自己的锁是第二个节点加的锁。更严重的,此时第二个节点还是认为自己是锁的持有者,但此时锁已经被第一个节点删掉了,那就有可能被第三个节点加锁,此时第二跟第三个节点都认为自己是锁的持有者,这就违反了分布式锁最基本的语义。

为了解决这个问题,需要确保节点在解锁时这个锁确实是自己加的才可以解锁,可以在加锁时设定键值对的 value 为一个唯一标识

唯一标识

节点加锁时指定键值对的 value 为一个唯一标识,例如 UUID。在节点解锁时,判断 value 是否为加锁时所指定的 value,是的话说明这个锁上次确实是自己加的,可以解锁

这种操作也存在问题:解锁时需要先取出 value 值,然后判断是否是自己加的锁,如果是才删除 key,这三个操作是分开的不是原子进行的。就有可能,节点在取出 value 值时,确实是自己加的锁,但此时节点由于某些原因阻塞,导致锁超时自动释放,且第二个节点拿到了锁,等第一个节点阻塞结束,再来判断取出的 value 值,判断出是自己加的锁,然后就解锁,但此时解的已经是第二个节点加的锁了

所以需要保证取 value,判断,跟解锁三个操作原子性进行,在 Redis 中可以使用 Lua 脚本 来解决这个问题

续约

前面提到,分布式锁一定要设置过期时间。那么过期时间究竟应该设计多长?

对于加锁的用户来说,他很难确定锁的过期时间应该多长,设置短了,可能业务还没完成,锁就过期了;设置长了,万一实例真的宕机崩溃了,那么就导致其它节点长期拿不到锁直到锁自动超时删除;更严重的,业务处理时间无法确定,可能出现业务执行时间超过锁时间的情况

我们可以引入续约机制,即在锁还没有过期的时候,再一次延长超时时间。这样的话,过期时间就不必设置得很长,因为如果真的出现业务执行时间可能超过锁原本的超时时间情况,也可以通过续约延长超时时间;而如果实例真的崩溃了,续约也就不会继续了,过一段时间自然就失效了

手动续约

在 Redis 中,如果使用手动续约的方式,就可以通过 Lua 脚本原子性地进行取出锁的 value 值,判断是否是自己加的锁,是的话使用 expire 命令或者 pexpire (设置毫秒级过期时间) 重新设置键值对的超时时间 这几步来完成

自动续约

手动续约的操作并不复杂,难点在于使用的细节:间隔多久续约一次?如果续约失败了怎么处理?失败有可能是请求超时了,此时可以采取重试策略;但如果是发生了超时以外的其它问题,又该如何处理?如果真的确认续约失败了,正在执行的业务如何处置?如果只能中断掉业务,又该怎么中断

对大部分用户来讲,处理续约过程中的各种异常情况还是比较棘手的,所以可以考虑通过设计中间件为用户提供自动续约的方式。当然,设计者还是躲不开那些异常问题:

隔多久续一次约,每次续多长? 由于续约操作受网络,Redis 服务器影响较大,不是设计者能把握的问题,因此让用户选择续约的间隔;至于续多长,可以简单地使用一开始加锁时设置的超时时间。但其实续约间隔跟超时时间是存在一定关系的,肯定不能说等到锁超时都被删了我才来续约,需要考虑从服务的可用性。假如我们将分布式锁的超时时间设置为 10s,而且 预期 2s 内绝大概率能续约成功,那么相应地就可以考虑把续约间隔设置为 8s。这个预期续约成功时间的概率其实就是对可用性的要求

如何处理超时?超时时间应设为多久? 这里的超时时间指的是对 Redis 服务器的续约请求的超时,这种超时的情况我们并不知道究竟有没有续约成功,而且大多数时候超时都是 偶发性 的,所以我们选择重试续约请求。缺点在于万一某次超时不是偶发性的,而是真的 Redis 服务器崩溃了或者网络不通,那么会导致无限次尝试续约。至于超时时间我们同样让用户来指定

如何通知用户续约失败? 我们只处理超时引起的续约失败,通过重试来处理;发生其它错误的情况下,我们直接告诉用户遇到了无法处理的 error。在考虑操作过程中可能发生的错误时,往往只需要分为超时跟其它错误两类即可,超时是区别于其它错误的一种错误

要不要设置续约次数上限? 假如一个业务,不断续约以至于几分钟都没释放分布式锁,我们要不要强制释放掉?我们的答案是不设置,理由是如果用户有这种需求,如业务确确实实需要执行这么久,那么他应该采取手动续约的方式。作为一个中间件的设计者,我们可以尽可能为用户考虑并解决所有的问题,但是像这种比较客户个性化的需求,我们是无法考虑到的,只能让用户自己操作

加锁重试

加锁时也可能遇到偶发性地失败,此时我们可以尝试重试。其实,只有是超时失败的情况下我们才会去重试,其它错误大多都是重试也无法解决问题的。在分布式环境下,超时错误就是与其它错误要不同。

  • 如果调用超时了,那就直接再次加锁,注意此时的 value 值还需要是上次超时前加锁时设置的 value 值。如果发现 Redis 中并不存在 key 这个键值对,说明超时前的加锁请求确实没有成功,此时就继续加锁,设置锁超时时间,然后返回。
  • 如果 key 存在,要先判断 key 对应的 value 是不是我们刚才超时前尝试加锁时的 value 值,如果是的话,说明超时前加的锁成功了,只不过 Redis 的响应没有回到我们客户端而已
  • 而如果 value 值为其它值,那就说明上次加锁失败了,而且已经被其它节点加锁了,那么本次加锁就是失败的
    所以重试加锁与直接加锁不同,最大的差别在于 重试加锁前需要判断 value 是否为第一次加锁时指定的 value 值

类似于自动续约,加锁重试也需要考虑一些问题:怎么重试?重试逻辑如上。隔多久重试一次,总共重试几次?这个也应当让用户来指定。什么情况下应该重试,什么情况下不应该重试?这个我们也说过,超时错误就应该重试,其它错误就没必要重试

singleflight 优化

在非常 高并发,且 热点集中,即多个节点中多个线程竞争的都是那几把锁,的情况下,可以考虑结合 singleflight 来优化

具体来说,就是单个节点本地的所有线程/协程自己先在本地竞争一把锁,胜利者再去抢全局的分布式锁
同个节点上的线程先竞争,胜利的线程再去和其它节点的胜利者竞争全局的分布式锁,系统中共有多少个节点最终就会有最多几个线程去竞争分布式锁

可以看出来,其实只有在单个节点中参与竞争分布式锁的线程数很多的情况下,singleflight 才能发挥更大的优势;如果每个节点只有那么一两个线程会参与分布式锁的竞争,那最终参与分布式锁的竞争的线程数跟各个线程不经过 singleflight 而直接竞争分布式锁的线程数相差无几,甚至每个节点上还要经历一次本地锁的竞争,最终整个流程下来造成的开销可能比使用 singleflight 还要大

一些问题

  1. 加锁失败的原因?请求超时,网络故障,Redis 服务器故障,锁被别人持有了
  2. 怎么优化分布式锁的性能?整个分布式锁的操作流程主要涉及到本节点的业务,网络通信,Redis 服务器 三个部分。而 Redis 本身的性能是很快的,网络又是我们把握不住的一种影响因素。真要优化的话应该 从业务下手,减少业务占有锁的时间,尽量使锁正常释放掉,避免出现其它的异常问题

猜你喜欢

转载自blog.csdn.net/Pacifica_/article/details/127719758