微信公众号:路人zhang
扫码关注微信公众号

回复“面试手册”,获取本站PDF版

回复“简历”,获取高质量简历模板

回复“加群”,加入程序员交流群

回复“电子书”,获取程序员类电子书

当前位置: 计算机基础 > Redis高频面试题 > 33.Redis如何实现分布式锁?

前面讲过了分布式锁的特性,其实实现分布式锁就是围绕着这些展开的

Redis实现分布式锁的主要命令:SETNX,该命令的作用是当key不存在时设置key的值,当Key存在时,什么都不做。

先来看最简单的实现方式,如下图

Redis分布式锁
Redis分布式锁

从上图可以看到主要两个关键步骤,加锁和解锁

但是这个简陋的分布式锁存在很多问题,并不能满足上述介绍的分布式锁的特性,

比如,当线程1执行到上图中执行业务这步时,业务代码突然出现异常了,无法进行删除锁这一步,那就完犊子了,死锁了,其他线程也无法获取到锁了(因为SETNX的特性)。

改进方案1

那咋整呢?

一提到异常,有人想起了try-catch-finally了,把删除锁的操作放到finally代码块中,就算出现异常,也是能正常释放锁的,执行业务出现异常这个问题确实解决了。但这玩意并不靠谱,如果Redis在执行业务这步宕机了呢,finally代码块也不会执行了。

改进方案2

其实这个问题很好解决,只需给锁设置一个过期时间就可以了,对key设置过期时间在Redis中是常规操作了。就是这个命令SET key value [EX seconds][PX milliseconds] [NX|XX]

  • EX second: 设置键的过期时间为second秒;
  • PX millisecond:设置键的过期时间为millisecond毫秒;
  • NX:只在键不存在时,才对键进行设置操作;
  • XX:只在键已经存在时,才对键进行设置操作;
  • SET操作完成时,返回OK,否则返回nil。

那先现在这个方案就完美了吗?显然没有

例如,线程1获取到了锁,并设置了有效时间10秒,但线程1在执行业务时超过了10秒,锁到期自动释放了,在锁释放后,线程2又获取了锁,在线程2执行业务时,线程1执行完了,随后执行了删除锁这一步,但是线程1的锁早就到期自动释放了,他删除的是线程2的锁!!!

上面这个例子说的有点乱,突然想到一个现实生活中的例子,把Redis比作一个厕所,张三去上厕所,关上了门(获取锁,并设置了10秒),上到一半(10秒到了,门自动开了),这时李四进去了,关上了门(获取锁,并设置了10秒),张三上完了厕所,把门打开了走了(执行了删除锁操作)

上面这个荒诞的例子说明了方案2有两处不合理的地方,第一是张三厕所没上完,李四怎么能进去呢?他们上厕所产生了冲突,第二是张三上完厕所怎么能打开李四的门呢(分布式锁的特性,加锁和解锁必须同一台服务器执行)?

改进方案3

其实看起来方案2的问题很容易解决,只需要把锁的过期时间设置的非常长,就可以避免这两个问题,但是这样并不可行,因为这样相当于回到最简陋的方案(会导致李四一直上不到厕所)。

那如何能让李四上到厕所,还不会让自己锁的门被张三打开门呢?

很简单,为锁加一个标识,例如生成一个UUID,作为锁的标识,每个线程获取锁时都会生成一个不同的UUID作为锁的标识,在进行删除锁时会进行判断,锁的标识和自己生成UUID相等时才进行删除操作,这样就避免线程1释放了线程2的锁。(相当于自己上自己的锁,不要计较为什么张三在李四上厕所时不需要李四的钥匙就能离开厕所这种事,上厕所和分布式锁逻辑并不完全相同,只是简单类比)

那怎么解决李四未等张三上完厕所就进厕所呢?(如何确定锁的过期时间)

可以在加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

好了,张三和李四上厕所的解决了。

那方案3就没有其他问题了吗?其实还是有的,比如目前的分布式锁还不具备可重入性(同一线程可以重复获取锁,解决线程需要多次进入锁内执行任务的问题)

改进方案4

参考其他重入锁的实现,可以通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时才能释放锁。

那现在方案就没有问题了吗,其实还有

比如,线程1获取了锁,线程2没有获取到锁,那么线程2怎么知道线程1啥时候释放了锁,进而再去获取锁呢?

改进方案5

方案4中问题的解决方案,一般以下两种解决方案:

  • 可以通过客户端轮询的方式,就是线程2过一会就来看看是不是能获取锁了。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
  • 通过Redis的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。

那现在这个方案完美了吗?也还没有

目前讨论的都是redis是单节点的情况,如果这个节点挂了,那么所有的客户端都获取不到锁了

改进方案6

为了实现多节点Redis的分布式锁,Redis的作者提出了RedLock算法。

这是RedLock算法官网的地址,https://redis.io/topics/distlock,英文好的建议直接看官方文档,毕竟我这四六级飘过的人也可能理解的不准确,下面的内容主要是参考官网内容。

首先介绍保证分布式锁的有效性和安全性的要求:
  • 互斥性:在任何给定时刻,只有一个客户端可以持有一个锁
  • 释放死锁:即使锁定资源的客户端崩溃或被分区,也可以释放锁
  • 容错性:只要大多数Redis节点都在运行,客户端就能够获取和释放锁。
为什么基于故障转移实现的Redis分布式锁还不够用?

官网中举了一个例子:

客户端A获得主服务器上的锁,然后主服务器向从服务器复制数据的过程中崩了,导致数据没有复制到从数据库中,这时会在从服务器中选出来一个升级为主服务器,但新的主服务器中并没有客户端A设置的锁。所以客户端B也可以获取到锁,违背了上面说的互斥性

这就解释为什么需要RedLock算法

RedLock算法

假设有5个完全独立的Redis服务器,多节点Redis实现的RedLock算法如下

  • 获取当前时间戳
  • 客户端尝试在5个实例中按顺序获取锁,在所有实例中使用相同的键名和随机值。当在每个实例中设置锁时,需要将锁的获取时间设置为比锁过期短很多。例如,如果锁自动释放时间为10秒,则锁的获取时间在5-50毫秒。这是为了不要过长时间等待已经关闭的Redis实例,如果一个Redis实例不可用,我们应该尽快尝试获取下一个Redis实例的锁。
  • 客户端通过从当前时间中减去步骤1中获得的时间戳,计算出获取锁所需的时间。当且仅当客户端能够在大多数实例(至少3个)中获得锁,并且花费在获取锁的总时间小于锁的有效性时间时,该锁被认为已经获得。
  • 如果获得了锁,锁真正的有效时间为锁初始设置的有效时间(过期时间)减去第三步的时间,例如,锁初始有限时间为5s,获取锁花了0.5s,则锁真正的有效时间为4.5s(忽略了时钟漂移,时间漂移指指两个电脑间时间流速基本相同的情况下,两个电脑(或两个进程间)时间的差值)
  • 如果客户端由于某些原因无法获得锁(要么无法锁定N/2+1个Redis实例,要么有锁的效性时间为负数),客户端将尝试解锁所有Redis实例(即使是它认为无法锁定的Redis实例)。
RedLock算法是异步的吗?

可以看成同步算法,虽然没有跨进程的同步时钟,但每个进程(多个电脑)的本地时间仍然大致以相同的速度流动,与锁的自动释放时间相比,误差较小,将其忽略的话,则可以看成同步算法。

RedLock失败重试

当客户端无法获取到锁时,应该在随机时间后重试,并且理想的客户端应该并发地将所有命令用时发给所有Redis实例。对于已经获取锁的客户端要在完成任务后及时释放锁,这样其他客户端就不需要等锁自动过期后在获取。如果在获取锁后,在主动释放锁前无法连接到Redis实例,就只能等锁自动失效了。

释放锁

释放锁很简单,只要释放所有实例中的锁,不需要考虑是否释放成功(释放时会判断这个锁的value值是不是自己设置的,避免释放其他客户端设置的锁)

RedLock的 Safety arguments
  • 假设客户端可以获取到大多数Redis实例,并且所有Redis实例具有相同的key和过期时间,但不同的Redis实例的key是不同的时间设置的(获取锁的时间不可能完全一致),所以过期时间也不同,假设获取第一个Redis实例的锁的时间为T1,最后一个为T2,则客户端获得锁的最小有效时间为key的有效时间-(T2-T1)-时钟漂移。
  • 为什么需要获取一半以上的Redis实例的锁才算获取到锁成功呢?因为如果获取不到一半也算成功的话会导致多个客户端同时获取到锁,违背了互斥性
  • 一个客户端锁定大多数Redis实例所需的时间大于或者接近锁的过期时间时,会认为锁无效,并解锁所有Redis实例
RedLock崩溃的相关解决方法

场景:客户端A在成功获取锁后,如果所有Redis重启,这时客户端B就可以再次获取到锁,违背了互斥性

解决方法:开启AOF持久化,可以解决这个问题,但是AOF同步到磁盘上的方式默认是每秒一次,如果1秒内断电,会导致1秒内的数据丢失,如果客户端是在这1秒内获得的锁,立即重启可能会导致锁的互斥性失效,解决方法是每次Redis无论因为什么原因停掉都要等key的过期时间到了在重启(延迟重启),这么做的缺点就是在等待重启这段时间内Redis处于关闭的状态。

最后,上面的方案6还有其他问题吗?其实还是有的,不过还有一种更适合Java的强大方案是Redisson,有兴趣的小伙伴可以去了解下

本站链接:https://www.mianshi.online如需勘误或投稿,请联系微信:lurenzhang888


点击面试手册,获取本站面试手册PDF完整版