对于锁你们确定不会陌生,在Java中synchronized关键字和ReentrantLock可重入锁在咱们的代码中是常常见的,通常咱们用其在多线程环境中控制对资源的并发访问,可是随着分布式的快速发展,本地的加锁每每不能知足咱们的须要,在咱们的分布式环境中上面加锁的方法就会失去做用。因而人们为了在分布式环境中也能实现本地锁的效果,也是纷纷各出其招,今天让咱们来聊一聊通常分布式锁实现的套路。java
Martin Kleppmann是英国剑桥大学的分布式系统的研究员,以前和Redis之父Antirez进行过关于RedLock(红锁,后续有讲到)是否安全的激烈讨论。Martin认为通常咱们使用分布式锁有两个场景:node
当咱们肯定了在不一样节点上须要分布式锁,那么咱们须要了解分布式锁到底应该有哪些特色:mysql
咱们了解了一些特色以后,咱们通常实现分布式锁有如下几个方式:git
下面分开介绍一下这些分布式锁的实现原理。github
首先来讲一下Mysql分布式锁的实现原理,相对来讲这个比较容易理解,毕竟数据库和咱们开发人员在平时的开发中息息相关。对于分布式锁咱们能够建立一个锁表:redis
lock通常是阻塞式的获取锁,意思就是不获取到锁誓不罢休,那么咱们能够写一个死循环来执行其操做: 算法
mysqlLock.lcok内部是一个sql,为了达到可重入锁的效果那么咱们应该先进行查询,若是有值,那么须要比较node_info是否一致,这里的node_info能够用机器IP和线程名字来表示,若是一致那么就加可重入锁count的值,若是不一致那么就返回false。若是没有值那么直接插入一条数据。伪代码以下: sql
须要注意的是这一段代码须要加事务,必需要保证这一系列操做的原子性。数据库
tryLock()是非阻塞获取锁,若是获取不到那么就会立刻返回,代码能够以下: 编程
unlock的话若是这里的count为1那么能够删除,若是大于1那么须要减去1。
咱们有可能会遇到咱们的机器节点挂了,那么这个锁就不会获得释放,咱们能够启动一个定时任务,经过计算通常咱们处理任务的通常的时间,好比是5ms,那么咱们能够稍微扩大一点,当这个锁超过20ms没有被释放咱们就能够认定是节点挂了而后将其直接释放。
前面咱们介绍的都是悲观锁,这里想额外提一下乐观锁,在咱们实际项目中也是常常实现乐观锁,由于咱们加行锁的性能消耗比较大,一般咱们会对于一些竞争不是那么激烈,可是其又须要保证咱们并发的顺序执行使用乐观锁进行处理,咱们能够对咱们的表加一个版本号字段,那么咱们查询出来一个版本号以后,update或者delete的时候须要依赖咱们查询出来的版本号,判断当前数据库和查询出来的版本号是否相等,若是相等那么就能够执行,若是不等那么就不能执行。这样的一个策略很像咱们的CAS(Compare And Swap),比较并交换是一个原子操做。这样咱们就能避免加select * for update行锁的开销。
ZooKeeper也是咱们常见的实现分布式锁方法,相比于数据库若是没了解过ZooKeeper可能上手比较难一些。ZooKeeper是以Paxos算法为基础分布式应用程序协调服务。Zk的数据节点和文件目录相似,因此咱们能够用此特性实现分布式锁。咱们以某个资源为目录,而后这个目录下面的节点就是咱们须要获取锁的客户端,未获取到锁的客户端注册须要注册Watcher到上一个客户端,能够用下图表示。
Curator封装了Zookeeper底层的Api,使咱们更加容易方便的对Zookeeper进行操做,而且它封装了分布式锁的功能,这样咱们就不须要再本身实现了。
Curator实现了可重入锁(InterProcessMutex),也实现了不可重入锁(InterProcessSemaphoreMutex)。在可重入锁中还实现了读写锁。
InterProcessMutex是Curator实现的可重入锁,咱们能够经过下面的一段代码实现咱们的可重入锁:
咱们利用acuire进行加锁,release进行解锁。
加锁的流程具体以下:
解锁的具体流程:
Curator提供了读写锁,其实现类是InterProcessReadWriteLock,这里的每一个节点都会加上前缀:
private static final String READ_LOCK_NAME = "__READ__";
private static final String WRITE_LOCK_NAME = "__WRIT__";
复制代码
根据不一样的前缀区分是读锁仍是写锁,对于读锁,若是发现前面有写锁,那么须要将watcher注册到和本身最近的写锁。写锁的逻辑和咱们以前4.2分析的依然保持不变。
Zookeeper不须要配置锁超时,因为咱们设置节点是临时节点,咱们的每一个机器维护着一个ZK的session,经过这个session,ZK能够判断机器是否宕机。若是咱们的机器挂掉的话,那么这个临时节点对应的就会被删除,因此咱们不须要关心锁超时。
你们在网上搜索分布式锁,恐怕最多的实现就是Redis了,Redis由于其性能好,实现起来简单因此让不少人都对其十分青睐。
熟悉Redis的同窗那么确定对setNx(set if not exist)方法不陌生,若是不存在则更新,其能够很好的用来实现咱们的分布式锁。对于某个资源加锁咱们只须要
setNx resourceName value
复制代码
这里有个问题,加锁了以后若是机器宕机那么这个锁就不会获得释放因此会加入过时时间,加入过时时间须要和setNx同一个原子操做,在Redis2.8以前咱们须要使用Lua脚本达到咱们的目的,可是redis2.8以后redis支持nx和ex操做是同一原子操做。
set resourceName value ex 5 nx
复制代码
Javaer都知道Jedis,Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission经过Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了没有更新,而Redission最新版本是2018.10月更新。
Redission封装了锁的实现,其继承了java.util.concurrent.locks.Lock的接口,让咱们像操做咱们的本地Lock同样去操做Redission的Lock,下面介绍一下其如何实现分布式锁。
Redission不只提供了Java自带的一些方法(lock,tryLock),还提供了异步加锁,对于异步编程更加方便。 因为内部源码较多,就不贴源码了,这里用文字叙述来分析他是如何加锁的,这里分析一下tryLock方法:
对于咱们的unlock方法比较简单也是经过lua脚本进行解锁,若是是可重入锁,只是减1。若是是非加锁线程解锁,那么解锁失败。
Redission还有公平锁的实现,对于公平锁其利用了list结构和hashset结构分别用来保存咱们排队的节点,和咱们节点的过时时间,用这两个数据结构帮助咱们实现公平锁,这里就不展开介绍了,有兴趣能够参考源码。
咱们想象一个这样的场景当机器A申请到一把锁以后,若是Redis主宕机了,这个时候从机并无同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis做者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。
经过上面的代码,咱们须要实现多个Redis集群,而后进行红锁的加锁,解锁。具体的步骤以下:
能够看见RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减小Redis某个集群出故障,形成分布式锁出现问题的几率。
上面咱们介绍过红锁,可是Martin Kleppmann认为其依然不安全。有关于Martin反驳的几点,我认为其实不只仅局限于RedLock,前面说的算法基本都有这个问题,下面咱们来讨论一下这些问题:
对于这三个问题,在网上包括Redis做者在内发起了不少讨论。
对于这个问题能够看见基本全部的都会出现问题,Martin给出了一个解法,对于ZK这种他会生成一个自增的序列,那么咱们真正进行对资源操做的时候,须要判断当前序列是不是最新,有点相似于咱们乐观锁。固然这个解法Redis做者进行了反驳,你既然都能生成一个自增的序列了那么你彻底不须要加锁了,也就是能够按照相似于Mysql乐观锁的解法去作。
我本身认为这种解法增长了复杂性,当咱们对资源操做的时候须要增长判断序列号是不是最新,不管用什么判断方法都会增长复杂度,后面会介绍谷歌的Chubby提出了一个更好的方案。
Martin以为RedLock不安全很大的缘由也是由于时钟的跳跃,由于锁过时强依赖于时间,可是ZK不须要依赖时间,依赖每一个节点的Session。Redis做者也给出了解答:对于时间跳跃分为人为调整和NTP自动调整。
这一块不是他们讨论的重点,我本身以为,对于这个问题的优化能够控制网络调用的超时时间,把全部网络调用的超时时间相加,那么咱们锁过时时间其实应该大于这个时间,固然也能够经过优化网络调用好比串行改为并行,异步化等。能够参考个人两个文章: 并行化-你的高并发大杀器,异步化-你的高并发大杀器
你们搜索ZK的时候,会发现他们都写了ZK是Chubby的开源实现,Chubby内部工做原理和ZK相似。可是Chubby的定位是分布式锁和ZK有点不一样。Chubby也是使用上面自增序列的方案用来解决分布式不安全的问题,可是他提供了多种校验方法:
本文主要讲了多种分布式锁的实现方法,以及他们的一些优缺点。最后也说了一下有关于分布式锁的安全的问题,对于不一样的业务须要的安全程度彻底不一样,咱们须要根据本身的业务场景,经过不一样的维度分析,选取最适合本身的方案。
最后这篇文章被我收录于JGrowing,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:github.com/javagrowing… 麻烦给个小星星哟。
最后打个广告,若是你以为这篇文章对你有文章,能够关注个人技术公众号,也能够加入个人技术交流群进行更多的技术交流。你的关注和转发是对我最大的支持,O(∩_∩)O。