Redis分布式锁探究

在分布式锁的实现方案中,Redis中解决方案是不得不提的,本篇文章就Redis来实现分布式锁的方案中一些思考和探究,通过本篇文章让我们更进一步的了解它,在架构选型中能够起到指导作用。

写在前面话

一般在我们聊起Redis生产实现分布式锁的时候一般我们都会想起开源类库中Redisson框架,非常的简便易用。

https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8

1
2
3
4
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
lock.unlock();

Redisson实现分布式锁的底层原理

mark

(1)加锁机制

​ 使用的lua脚本执行,保证业务逻辑执行的原子性

​ 先“exists anyLock” 判断要加锁的key是否存在,如果不存在的话就加锁。

      
1
2
3
4
5
6
7
8
9
10
11
12
13
注:8743c9c0-0795-4907-87fd-6c719a6b4586:1  是客户端ID
加锁命令:设置一个hash数据结果
hset anyLock

8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
执行后获得数据结构:
anyLock
{
"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}

然后执行:
pexpire anylock 30000,设置key这个锁的生存时间是30秒

​ 至此,加锁完成。

(2)锁互斥机制

​ 如果客户端1加锁成功,此时客户端2尝试加锁,执行同样的lua脚本,同样走上述(1)步骤加锁,先会执行第一个“exists anyLock ”,发现这个key已经存在了

​ 接着第二个if判断,判断anyLock锁key的hash数据结构中们是否包含客户端2的ID,但是明显不是的,因为锁key中包含的客户端1的ID

​ 所以,客户端2会获取到一个数字,这个数字代表anyLock这个key的剩余生存时间。

​ 由此客户端2会进入while循环,不停的尝试加锁,知道客户端1释放锁。

(3)watch dog自动延期机制

​ 客户端1加锁的锁key的默认生存时间是30秒,如果超过30秒,客户端1还想一直持有这把锁,怎么办呢?

​ 实现逻辑如下:只要客户端1加锁成功,就会有一个watch dog看门狗后台线程,它会每隔10s检查一下,如果客户端1还持有锁key,那么久会不断的延长锁key的生存时间。

​ 是不是很简单,所以有些设计虽然简单,但是作用很大,这就需要我们平常要不断积累一些设计思想,使我们思维发散,形成一个习惯这样我们在以后架构设计中也会创造出一些精妙的设计思想。

(4) 可重入加锁机制

​ 如果客户端1都已经持有这把锁了,结果可重入的加锁会怎么样呢?

​ 设计如下:

​ 第一个if判断,”exists anyLock”,会判断锁key已经存在

​ 第二个if判断,客户端1的ID是否存在锁key的hash数据结构中包含的那个ID,显然是存在的

​ 接着就会执行可重入的逻辑

       
1
2
3
4
5
6
7
8
9
incrby anyLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令面对客户端1的加锁次数,累加1
然后就变成如下数据结构:

anyLock
{
"8743c9c0-0795-4907-87fd-6c719a6b4586:1":2
}

(5) 释放锁机制

​ 执行lock.unlock(),就可以释放分布式锁

​ 可重入锁,就是每次调用都对anyLock数据结构中加锁此时减少1,直到为0说明这个客户端就不再持有锁了,此时机会用”del anyLock”命令从redis里删除这个key

​ 接着其他客户端就可以尝试完成加锁了

以上就是所谓分布式锁开源Redission框架的实现机制

Redission实现分布式锁存在的问题

假若我们Redis架构是采用master slave架构方式,我们对redis mastre实例写入anyLock这个锁key的value,此时会异步复制给对应的master slave实例。

但是此时,一旦redis master宕机,准备切换,此时slave还没有完成数据同步,redis slave变成了redis master

接着就会导致,客户端2来加锁的时候,在新的redis master上完成加锁,而客户端1也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成加锁。导致各种脏数据的产生

所以以上就是这个redis cluster,redis master-slave架构的主从异步复制导致的redis分布式锁的最大的缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

旁白:技术是没有完美,Redission实现的分布式锁,虽然存在这样的问题,但是我们就不能说这个分布式锁的实现方案不能用,在分布式锁技术选型中根据我们自己系统的具体场景来分析,如果我们系统容许有这样的问题,那么使用这个方案就没有任何问题。

Redlock算法

在分布式版本的算法里我们假设有N个Redis Master节点,这些节点都是完全独立的,因为我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况都不会同时宕机,一个客户端需要做如下操作来获取锁

​ 1.获取当前时间t0(单位是毫秒)。

​ 2.使用相同的key和value一次向5个实例获取锁,客户端在获取锁的时候自身设置一个和总的锁释放时间相比小的多的超时时间,比如如果锁的自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50ms的范围,这个可以防止一个客户端在某个宕机的master节点上的阻塞过长时间,如果一个master节点不可用,马上切换尝试下一个master节点

​ 3.客户端计算在第2步骤获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(3个),而且总共消耗的时间不超过锁锁释放时间,这个锁就获取成功

​ 4.如果锁获取成功了,那现在锁自动释放时间就是最初锁释放时间减去志气啊你获取锁锁消耗的时间

​ 5.如果锁获取失败了,不管因为获取成功的锁不超过一般(N/2+1)还是获取锁总的消耗时间超过了锁释放时间,客户算都会到每个master节点上释放锁,即时是并没有在那个节点上获取到

性能、故障恢复和fsync

很多使用Redis做锁服务器的用户在获取锁和释放锁时不止要求低延时,同时要求高吞吐量,也即单位时间内可以获取和释放的锁数量。为了达到这个要求,一定会使用多路传输来和N个服务器进行通信以降低延时(或者也可以用假多路传输,也就是把socket设置成非阻塞模式,发送所有命令,然后再去读取返回的命令,假设说客户端和不同Redis服务节点的网络往返延时相差不大的话)。

然后如果我们想让系统可以自动故障恢复的话,我们还需要考虑一下信息持久化的问题。

为了更好的描述问题,我们先假设我们Redis都是配置成非持久化的,某个客户端拿到了总共5个节点中的3个锁,这三个已经获取到锁的节点中随后重启了,这样一来我们又有3个节点可以获取锁了(重启的那个加上另外两个),这样一来其他客户端又可以获得这个锁了,这样就违反了我们之前说的锁互斥原则了。

如果我们启用AOF持久化功能,情况会好很多。举例来说,我们可以发送SHUTDOWN命令来升级一个Redis服务器然后重启之,因为Redis超时时效是语义层面实现的,所以在服务器关掉期间时超时时间还是算在内的,我们所有要求还是满足了的。然后这个是基于我们做的是一次正常的shutdown,但是如果是断电这种意外停机呢?如果Redis是默认地配置成每秒在磁盘上执行一次fsync同步文件到磁盘操作,那就可能在一次重启后我们锁的key就丢失了。理论上如果我们想要在所有服务重启的情况下都确保锁的安全性,我们需要在持久化设置里设置成永远执行fsync操作,但是这个反过来又会造成性能远不如其他同级别的传统用来实现分布式锁的系统。 然后问题其实并不像我们第一眼看起来那么糟糕,基本上只要一个服务节点在宕机重启后不去参与现在所有仍在使用的锁,这样正在使用的锁集合在这个服务节点重启时,算法的安全性就可以维持,因为这样就可以保证正在使用的锁都被所有没重启的节点持有。 为了满足这个条件,我们只要让一个宕机重启后的实例,至少在我们使用的最大TTL时间内处于不可用状态,超过这个时间之后,所有在这期间活跃的锁都会自动释放掉。 使用延时重启的策略基本上可以在不适用任何Redis持久化特性情况下保证安全性,然后要注意这个也必然会影响到系统的可用性。举个例子,如果系统里大多数节点都宕机了,那在TTL时间内整个系统都处于全局不可用状态(全局不可用的意思就是在获取不到任何锁)。

引申的问题

假如下单时,用分布式锁来防止库存超卖,但是是每秒上千订单的高并发场景,如何对分布式锁进行高并发优化来应对这个场景?

在高并发场景下如何优化分布式锁的并发性能?

优化方案:采用分段锁的设计思想来提高并发性能

原来使用分布式锁来处理每一个请求都被锁住,然后处理完释放锁,接着下一个,这样变成一个串行化的处理,加入处理完一个订单全过程需要20ms,那么1s也就处理50个请求。

–>采用分段思想,把数据分成很多段,每个段是一个单独的锁,所以多个线程过来并发修改数据时候可以并发修改不同段数据,这样并发处理能力就上来了。

例如1000件商品库存,拆分成20个库存段,每个库存端处理50件库存,这样1000/秒请求压过来后,利用随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁,也就是说同时请求一起执行,每个下单请求锁了一个库存分段。

相当于一个20ms,并发处理掉20个下单请求,那么1s也就是可以处理掉20*50=1000个下单请求了。

旁白:优化思想可以解决问题,按时具体实现还是会有好多代码量和问题需要注意,比如假若加锁后发现分库段内库存不足了,怎么办?那么久需要自动释放锁,然后换一个分段库存,再次去尝试加锁处理,这些都是要考虑实现的逻辑,实现比较复杂。

归纳代码逻辑如下:

  • 先要在redis中初始化好数据分段存储设计,比如1000个库存初始化20个分段库存字段
  • 客户端请求古来,每次处理库存时候要写随机算法,随机挑选一个分段来处理
  • 还要考虑如果分段中数据不足,需要释放锁切换随机到下一个分段数据去处理

以上思路详见博文

https://mp.weixin.qq.com/s/RLeujAj5rwZGNYMD0uLbrg

秒杀场景防止超卖库存的思路?

(1)mysql的排它锁

​ 响应时间太长

(2)使用redis队列[推荐方案]

​ 比数据库的排它锁性能提高很多

(3)Redis事物监听

(4)Token令牌 + MQ实现异步数据库修改(MQ异步数据库更新可以使用悲观锁或者乐观锁实现)

见以下的博客地址实现方案

https://blog.csdn.net/RuiKe1400360107/article/details/104731775?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-5.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-5.channel_param