分布式锁的实现原理

目录

1.基于sentnx命令的实现原理

Redis 分布式锁过期了,还没处理完怎么办

Redisson 使用看门狗(守护线程)“续命”的方案在大多数场景下是挺不错的,也被广泛应用于生产环境,但是在极端情况下还是会存在问题。

2.Redlock

3.Zookeeper

分布式锁方案到底选哪个?


谈到锁,首先我们想到的就是lock和synchronized,但是这都是基于JVM的锁,JVM进程管不到其他的服务实例的线程,分布式锁油然而生。

1.基于sentnx命令的实现原理

127.0.0.1:6379> setnx stockLock 10.12.35.12_stockService 
(integer) 1

在这里插入图片描述

如上图所示,大致的加锁以及释放锁的过程其实和数据库的分布式锁方案还是比较类似的。只不过将其中向数据库插入数据的步骤替换成了向Redis获取锁的步骤,由于Redis是基于内存进行操作的,因此性能上比基于数据库的分布式锁方案更好一点。

方案分析

另外还应该注意的是,在我们设置锁的时候,还需要带有自身服务的业务属性,否则容易造成错乱。为什么这么说呢?举个栗子,库存服务在加完锁之后开始执行扣减库存的任务,当扣减库存完成之后,服务挂了,原先需要删除的锁资源,等到过期之后被Redis删除,此时库存服务2可以继续申请锁,如果此时库存服务1恢复了,它并不知道锁资源已经释放,起来后立马删除了库存服务2加的锁,那么此时就会出现两个问题:


1、库存服务执行完库存扣减之后,回头来进行锁资源释放的时候,发现锁实际已经不在了;
2、当库存服务1恢复后发现锁还在,立马删除了该锁,完成了它挂掉之前未完成的工作。但是实际上这个锁是库存服务2加的锁,如果此时库存服务3也要尝试加锁,发现可以加锁成功,然后和库存服务2一样同样对库存进行操作,那么此时就会出现线程安全问题。


经过上文的分析,这个问题的根源就是在加锁的时候没有具体区分到底是哪个服务加的锁。因此在执行命令的时候,我们需要将带有服务实例关联属性的设置为value,这样在进行锁获取的时候检查下当前锁的持有者是谁,如果不是服务实例自己则不能执行删除操作。

那这样是不是就完美解决问题了呢?实际上还是有问题存在的,有同学会说,怎么这么多问题?实际上这种方案的实现就是在各种不完美的方案中逐渐找到相对完美的方案。

上文提到的获取锁判断是不是自己方服务实例加的锁,再执行删除锁的过程实际并不是原子的。因此还是会出现并发安全问题,这个问题可以通过lua脚本来解决,在lua脚本中实现这个逻辑,而不是在客户端中实现。


但是实际上还是有问题没有解决,比如说我们在加锁的时候会设置过期时间,但是过期时间应该设置多长时间呢?设置短了的话,出现网络超时或者服务还没有执行完业务,锁就失效了。设置长了话,其他服务节点等待获取锁的时间就会变长,降低了服务的性能。
 

Redis 分布式锁过期了,还没处理完怎么办

为了防止死锁,我们会给分布式锁加一个过期时间,但是万一这个时间到了,我们业务逻辑还没处理完,怎么办?

首先,我们在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的。

之后,我们再来考虑对这个问题进行兜底设计。

关于这个问题,目前常见的解决方法有两种:

1、守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。

2、超时回滚:当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。


Redisson 使用看门狗(守护线程)“续命”的方案在大多数场景下是挺不错的,也被广泛应用于生产环境,但是在极端情况下还是会存在问题。

问题例子如下:

1、线程1首先获取锁成功,将键值对写入 redis 的 master 节点

2、在 redis 将该键值对同步到 slave 节点之前,master 发生了故障

3、redis 触发故障转移,其中一个 slave 升级为新的 master

4、此时新的 master 并不包含线程1写入的键值对,因此线程2尝试获取锁也可以成功拿到锁

5、此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据

解决方法:上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致。

当前比较主流的解法和思路有两种:

1)Redis 作者提出的 RedLock;2)Zookeeper 实现的分布式锁。
————————————————

2.Redlock

为了解决Redis作为分布式锁存在的单点问题,Redis的作者又提出了Redlock的解决方案,该解决方案依赖多个Redis的Master节点,官方推荐使用5个Master节点,他们彼此之间是独立的,直接舍弃了异步复制,只使用 master 节点。大致的交互步骤如下所示:

1、首先获取当前节点的系统时间;
2、客户端尝试向所有的Redis实例顺序地发送加锁的请求(官方推荐Redis集群至少5个实例),在设置锁的过程中,使用相同的key以及随机值value,同时请求的超时时间需要远小于锁的有效时间。这样做的目的是为了防止节点不可用的时候导致请求锁的时候被阻塞,当实例没响应的时候可以快速跳过,向下一个节点继续请求锁。
3、假设Redis集群规模为5,那么如果客户端在大多数实例中(超过3个实例)获得了锁,同时计算了当前的时间减去步骤1中获得的时间,这个事件差如果小于锁的有效时间,那么此时可以认为加锁成功,可以操作执行后续的业务;
4、如果不满足步骤3是条件,那么就表示加锁失败,客户端需要向所有的Redis节点发起锁释放请求。

方案分析
为什么Redlock要在集群中多个实例上加锁呢?实际目的是通过锁的冗余来实现分布式锁的高容错性。试想一下如果只有一个Redis实例,一旦它挂掉了,客户端就无法进行加锁操作了或者锁信息就会丢失,影响业务功能。通过在集群中多实例中冗余锁信息,即使出现Redis挂了的情况,其他节点中依然存在锁信息,从而提升了分布式锁的可用性。

那么为什么还要计算几所时间呢?由于我们加锁的时候,每个节点都设置了超时时间,如果整个加锁的时间过长,整个过程的累加时间超过了锁的有效时间,那么加锁完成之后就会哦出现锁失效的情况了,因此我们需要确保加锁的事件尽可能的短,这也是为什么加锁请求都有超时时间的原因了,发现超时立马跳到下一个节点,避免单个节点耗时过长。

虽然Redlock看上去是比较完善的分布式解决方案,但是实际上这个方案是比较重的,需要维护一个Redis集群,另外过程中依赖系统时间,但是如果出现了时间跳变,那么对于整个分布式锁都有非常大的影响。
 

3.Zookeeper

Zookeeper是一个分布式的应用协调服务中间件,通过它也可以实现分布式锁的效果,这里介绍的是基于临时有序的ZNode分布式锁实现方案。在介绍方案之前,先补充下Zookeeper中和分布式锁息息相关的特性。

我们来看下Zookeeper的数据结构,实际上它是一种树形模型,类似于Linux的文件系统。Zookeeper使用类似于文件目录的层级目录数据结构来组织自身的数据存储节点,这些节点就被称作为ZNode,每个节点都用一个以斜杠(/)分隔的路径来表示,而且每个节点都有父节点(根节点除外)。另外在Zookeeper中,如果我们使用不同的创建参数,可以创建不同类型的ZNode。
1、持久化ZNode:当createMode为PERSISTENT会创建持久化ZNode,节点存储的数据会永久保存在Zookeeper中,如果createMode为PERSISTENT_SEQUENTIAL,则会创建有序持久化ZNode,和之前的持久化节点不通的是,有序持久化节点的节点名称会附加上全局有序的递增序号;
2、临时ZNode:当createMode为EPHEMERAL时,创建的节点临时节点,在与客户端的session过期后,对应的临时节点也会被删除。当createMode为EPHEMERAL_SEQUENTIAL时创建出来的为有序的临时节点,当session过期之后,节点及其存储的数据也是会被删除的。在这里插入图片描述

 通过上述对于节点特性的描述,可以看出来它的全局递增有序以及过期删除的特性与分布式锁实现的原理非常契合。因此通过Zookeeper实现分布式锁的大致可以分为以下几个步骤:

  1. 首先创建一个持久化节点也就是父节点,这个持久化节点代表着一个分布式锁实例;
  2. 当有线程想要申请分布式锁的时候,则在该持久化节点下创建临时有序节点;
  3. 如果此时新建的临时有序节点是该父节点小所有有序节点中序号最小的节点,那么此时就表示申请到了分布式锁;
  4. 如果新建的临时节点当前不是最小序号的节点,则需要不断检查是否最小,知道最终获取到锁,或者节点超时。实际上这个是通过Zookeeper的watch机制实现的,在当前节点的上一序号的节点设置监听器,检查是否为最小节点的任务可以一直阻塞,直到收到上一节点被删除的时间事件,则唤醒检查事件,检查当前节点是不是最小序号节点。
  5. 当线程执行完业务之后,可以手动删除该临时节点以便于释放持有的锁。另外即使服务挂掉,由于对应的session失效,对应的临时节点也会被删除,防止出现死锁问题。
     

 方案分析
看上去通过Zookeeper实现分布式锁还是比较好的一种解决方案,但是它是完美的吗?从上面的分布式锁的流程可知,客户端线程想要获取锁就需要创建临时节点,这个时候客户端和Zookeeper之间就会维护一个session,来表示该客户端还在排队等待获取锁。因此这个方案的潜在问题就在于一旦出现网络异常,或者客户端发生STW GC,那么就可能导致session关闭,从而导致临时节点被关闭,此时就会出现原来客户端持有的锁被删除了,如果有另外的客户端过来加锁的话可以成功获取,那么此时就出现并发安全问题了。因此在这种极端条件下,Zookeeper的分布式锁实现方案也不是100%保证安全的。
 

分布式锁方案到底选哪个?

通过上述几种分布式锁方案原理的阐述以及问题分析,每个方案都有自己的长处以及缺点。所以在实际项目落地的时候,我么需要结合实际来进行分布式锁方案的选择。比如如果平台中本身已经有Redis集群了,但是没有Zookeeper集群,那么我们就可以借助于现有的基础实施来落地分布式锁,不需要再去维护一套Zookeeper集群。

另外根据实际的业务场景,如果并发量并不是很高,也可以通过简单的数据库的分布式锁方案来实现
————————————————

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值