对于Java中的锁大家必定都很相熟,在Java中synchronized关键字和ReentrantLock可重入锁在咱们的代码中是常常见的,个别咱们用其在多线程环境中管制对资源的并发拜访,然而随着分布式的疾速倒退,本地的加锁往往不能满足咱们的须要,在咱们的分布式环境中下面加锁的办法就会失去作用。为了在分布式环境中也能实现本地锁的成果,人们提出了分布式锁的概念。
分布式锁
分布式锁场景
个别须要应用分布式锁的场景如下:
- 效率:应用分布式锁能够防止不同节点反复雷同的工作,比方防止反复执行定时工作等;
- 正确性:应用分布式锁同样能够防止毁坏数据正确性,如果两个节点在同一条数据下面操作,可能会呈现并发问题。
分布式锁特点
一个欠缺的分布式锁须要满足以下特点:
- 互斥性:互斥是所得根本个性,分布式锁须要按需要保障线程或节点级别的互斥。;
- 可重入性:同一个节点或同一个线程获取锁,能够再次重入获取这个锁;
- 锁超时:反对锁超时开释,避免某个节点不可用后,持有的锁无奈开释;
- 高效性:加锁和解锁的效率高,能够反对高并发;
- 高可用:须要有高可用机制预防锁服务不可用的状况,如减少降级;
- 阻塞性:反对阻塞获取锁和非阻塞获取锁两种形式;
- 公平性:反对偏心锁和非偏心锁两种类型的锁,偏心锁能够保障装置申请锁的程序获取锁,而非偏心锁不能够。
分布式锁的实现
分布式锁常见的实现有三种实现,下文咱们会一一介绍这三种锁的实现形式:
- 基于数据库的分布式锁;
- 基于Redis的分布式锁;
- 基于Zookeeper的分布式锁。
基于数据库的分布式锁
基于数据库的分布式锁能够有不同的实现形式,本文会介绍作者在理论生产中应用的一种数据库非阻塞分布式锁的实现计划。
计划概览
咱们下面列举出了分布式锁须要满足的特点,应用数据库实现分布式锁也须要满足这些特点,上面咱们来一一介绍实现办法:
- 互斥性:通过数据库update的原子性达到两次获取锁之间的互斥性;
- 可重入性:在数据库中保留一个字段存储以后锁的持有者;
- 锁超时:在数据库中存储锁的获取工夫点和超时时长;
- 高效性:数据库自身能够反对比拟高的并发;
- 高可用:能够减少主从数据库逻辑,晋升数据库的可用性;
- 阻塞性:能够通过看门狗轮询的形式实现线程的阻塞;
- 公平性:能够增加锁队列,不过不倡议,实现起来比较复杂。
表结构设计
数据库的表名为lock,各个字段的定义如下所示:
字段名名称 | 字段类型 | 阐明 |
---|---|---|
lock_key | varchar | 锁的惟一标识符号 |
lock_time | timestample | 加锁的工夫 |
lock_duration | integer | 锁的超时时长,单位能够业务自定义,通常为秒 |
lock_owner | varchar | 锁的持有者,能够是节点或线程的惟一标识,不同可重入粒度的锁有不同的含意 |
locked | boolean | 以后锁是否被占有 |
获取锁的SQL语句
获取锁的SQL语句分不同的状况,如果锁不存在,那么首先须要创立锁,并且创立锁的线程能够获取锁:
<code class="sql">insert into lock(lock_key,lock_time,lock_duration,lock_owner,locked) values ('xxx',now(),1000,'ownerxxx',true)
如果锁曾经存在,那么就尝试更新锁的信息,如果更新胜利则示意获取锁胜利,更新失败则示意获取锁失败。
<code class="sql">update lock set locked = true, lock_owner = 'ownerxxxx', lock_time = now(), lock_duration = 1000 where lock_key='xxx' and( lock_owner = 'ownerxxxx' or locked = false or date_add(lock_time, interval lock_duration second) > now())
开释锁的SQL语句
当用户应用完锁须要开释的时候,能够间接更新locked标识位为false。
<code class="sql">update lock set locked = false, where lock_key='xxx' and lock_owner = 'ownerxxxx' and locked = true
看门狗
通过下面的步骤,咱们能够实现获取锁和开释锁,那么看门狗又是做什么的呢?
大家设想一下,如果用户获取锁到开释锁之间的工夫大于锁的超时工夫,是不是会有问题?是不是可能会呈现多个节点同时获取锁的状况?这个时候就须要看门狗了,看门狗能够通过定时工作一直刷新锁的获取事件,从而在用户获取锁到开释锁期间放弃始终持有锁。
基于Redis的分布式锁
Redis的Java客户端Redisson实现了分布式锁,咱们能够通过相似ReentrantLock的加锁-开释锁的逻辑来实现分布式锁。
<code class="java">RLock disLock = redissonClient.getLock("DISLOCK"); disLock.lock(); try { // 业务逻辑 } finally { // 无论如何, 最初都要解锁 disLock.unlock(); }
Redisson分布式锁的底层原理
如下图为Redisson客户端加锁和开释锁的逻辑:
加锁机制
从上图中能够看进去,Redisson客户端须要获取锁的时候,要发送一段Lua脚本到Redis集群执行,为什么要用Lua脚本呢?因为一段简单的业务逻辑,能够通过封装在Lua脚本中发送给Redis,保障这段简单业务逻辑执行的原子性。
Lua源码剖析:如下为Redisson加锁的lua源码,接下来咱们会对源码进行剖析。
源码入参:Lua脚本有三个输出参数:KEYS[1]、ARGV[1]和ARGV[2],含意如下:
- KEYS[1]代表的是加锁的Key,例如RLock lock = redisson.getLock(“myLock”)中的“myLock”;
- ARGV[1]代表的就是锁Key的默认生存工夫,默认30秒;
- ARGV[2]代表的是加锁的客户端的ID,相似于上面这样的:8743c9c0-0795-4907-87fd-6c719a6b4586:1。
Lua脚本及加锁步骤如下代码块所示,能够看出其大抵原理为:
- 锁不存在的时候,创立锁并设置过期工夫;
- 锁存在的时候,如果是重入场景则刷新锁的过期事件;
- 否则返回加锁失败和锁的过期工夫。
<code class="lua">-- 判断锁是不是存在 if (redis.call('exists', KEYS[1]) == 0) then -- 增加锁,并且设置客户端和初始锁重入次数 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置锁的超时事件 redis.call('pexpire', KEYS[1], ARGV[1]); -- 返回加锁胜利 return nil; end; -- 判断以后锁的持有者是不是申请锁的请求者 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 以后锁被请求者持有,重入锁,减少锁重入次数 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 刷新锁的过期工夫 redis.call('pexpire', KEYS[1], ARGV[1]); -- 返回加锁胜利 return nil; end; -- 返回以后锁的过期工夫 return redis.call('pttl', KEYS[1]);
看门狗逻辑
客户端1加锁的锁Key默认生存工夫才30秒,如果超过了30秒,客户端1还想始终持有这把锁,怎么办呢?只有客户端1加锁胜利,就会启动一个watchdog看门狗,这个后盾线程,会每隔10秒检查一下,如果客户端1还持有锁Key,就会一直的缩短锁Key的生存工夫。
开释锁机制
如果执行lock.unlock(),就能够开释分布式锁,此时的业务逻辑也是非常简单的。就是每次都对myLock数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,阐明这个客户端曾经不再持有锁了,此时就会用:“del myLock”命令,从Redis里删除这个Key。
而另外的客户端2就能够尝试实现加锁了。这就是所谓的分布式锁的开源Redisson框架的实现机制。
个别咱们在生产零碎中,能够用Redisson框架提供的这个类库来基于Redis进行分布式锁的加锁与开释锁。
Redisson分布式锁的缺点
Redis分布式锁会有个缺点,就是在Redis哨兵模式下:
- 客户端1对某个master节点写入了redisson锁,此时会异步复制给对应的slave节点。然而这个过程中一旦产生master节点宕机,主备切换,slave节点从变为了master节点。
- 客户端2来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁实现了加锁。
- 零碎在业务语义上肯定会呈现问题,导致各种脏数据的产生。
这个缺点导致在哨兵模式或者主从模式下,如果master实例宕机的时候,可能导致多个客户端同时实现加锁。
基于Zookeeper的分布式锁
Zookeeper实现的分布式锁实用于引入Zookeeper的服务,如下所示,有两个服务注册到Zookeeper,并且都须要获取Zookeeper上的分布式锁,流程式什么样的呢?
步骤1
假如客户端A抢先一步,对ZK发动了加分布式锁的申请,这个加锁申请是用到了ZK中的一个非凡的概念,叫做“长期程序节点”。简略来说,就是间接在”my_lock”这个锁节点下,创立一个程序节点,这个程序节点有ZK外部自行保护的一个节点序号。
- 比方第一个客户端来获取一个程序节点,ZK外部会生成名称xxx-000001。
- 而后第二个客户端来获取一个程序节点,ZK外部会生成名称xxx-000002。
最初一个数字都是顺次递增的,从1开始逐次递增。ZK会保护这个程序。所以客户端A先发动申请,就会生成进去一个程序节点,如下所示:
客户端A发动了加锁申请,会先加锁的node下生成一个长期程序节点。因为客户端A是第一个发动申请,所以节点名称的最初一个数字是”1″。客户端A创立完整程序节后,会查问锁上面所有的节点,依照开端数字升序排序,判断以后节点的是不是第一个节点,如果是第一个节点则加锁胜利。
步骤2
客户端A都加完锁了,客户端B过去想要加锁了,此时也会在锁节点下创立一个长期程序节点,节点名称的最初一个数字是”2″。
客户端B会判断加锁逻辑,查问锁节点下的所有子节点,按序号顺序排列,此时第一个是客户端A创立的那个程序节点,序号为”01″的那个。所以加锁失败。加锁失败了当前,客户端B就会通过ZK的API对他的程序节点的上一个程序节点加一个监听器。ZK人造就能够实现对某个节点的监听。
步骤3
客户端A加锁之后,可能解决了一些代码逻辑,而后就会开释锁。Zookeeper开释锁其实就是把客户端A创立的程序节点zk_random_000001
删除。
删除客户端A的节点之后,Zookeeper会负责告诉监听这个节点的监听器,也就是客户端B之前增加监听器。客户端B的监听器晓得了上一个程序节点被删除,也就是排在他之前的某个客户端开释了锁。此时,就会客户端B会从新尝试去获取锁,也就是获取锁节点下的子节点汇合,判断本身是不是第一个节点,从而获取锁。
三种锁的优缺点
基于数据库的分布式锁:
- 数据库并发性能较差;
- 阻塞式锁实现比较复杂;
- 偏心锁实现比较复杂。
基于Redis的分布式锁:
- 主从切换的状况下可能呈现多客户端获取锁的状况;
- Lua脚本在单机上具备原子性,主从同步时不具备原子性。
基于Zookeeper的分布式锁:
- 须要引入Zookeeper集群,比拟重量级;
- 分布式锁的可重入粒度只能是节点级别;
参考文档
分布式锁
三种分布式锁比照
分布式锁的三种实现的比照
我是御狐神,欢送大家关注我的微信公众号:wzm2zsd
本文最先公布至微信公众号,版权所有,禁止转载!