分布式锁 原理及实现方式

1、原理html

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。若是不一样的系统或是同一个系统的不一样主机之间共享了一个或一组资源,那么访问这些资源的时候,每每须要经过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种状况下,就须要使用分布式锁了。redis

在平时的实际项目开发中,咱们每每不多会去在乎分布式锁,而是依赖于关系型数据库固有的排他性来实现不一样进程之间的互斥,但大型分布式系统的性能瓶颈每每集中在数据库操做上。算法

 

在单机环境中,Java中其实提供了不少并发处理相关的API,可是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。数据库

 

其实秒杀类场景最主要的是执行秒杀操做要单线程的,提到单线程,确定会想到synchronized关键字,可是他有两个致命缺点:一、没法作到细粒度控制,二、只适合单点不适用集群。
因此大多数项目只能采用分布式锁的实现方式。
 
 
针对分布式锁的实现,目前比较经常使用的有如下几种方案:
    基于数据库实现分布式锁
    基于缓存(redis,memcached,tair)实现分布式锁
    基于Zookeeper实现分布式锁
 
分布式锁主要有基于缓存如redis、基于zookeeper、基于数据库的实现。
 

在分析这几种实现方案以前咱们先来想一下,咱们须要的分布式锁应该是怎么样的?(这里以方法锁为例,资源锁同理)apache

    

能够保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。缓存

  

能够保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。服务器

    这把锁要是一把可重入锁(避免死锁)网络

    这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)session

    有高可用的获取锁和释放锁功能并发

    获取锁和释放锁的性能要好

 

 

 

 

2、实现方式

基于数据库实现分布式锁

基于数据库表

要实现分布式锁,最简单的方式可能就是直接建立一张锁表,而后经过操做该表中的数据来实现了。

当咱们要锁住某个方法或资源时,咱们就在该表中增长一条记录,想要释放锁的时候就删除这条记录。

建立这样一张数据库表:

当咱们想要锁住某个方法时,执行如下SQL:

由于咱们对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为操做成功的那个线程得到了该方法的锁,能够执行方法体内容。

当方法执行完毕以后,想要释放锁的话,须要执行如下Sql:

上面这种简单的实现有如下几个问题:

一、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。

二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。

三、这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。

四、这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。

固然,咱们也能够有其余方式解决上面的问题。

  • 数据库是单点?搞两个数据库,数据以前双向同步。一旦挂掉快速切换到备库上。
  • 没有失效时间?只要作一个定时任务,每隔必定时间把数据库中的超时数据清理一遍。
  • 非阻塞的?搞一个while循环,直到insert成功再返回成功。
  • 非重入的?在数据库表中加个字段,记录当前得到锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,若是当前机器的主机信息和线程信息在数据库能够查到的话,直接把锁分配给他就能够了。

基于数据库排他锁

除了能够经过增删操做数据表中的记录之外,其实还能够借助数据中自带的锁来实现分布式的锁。

咱们还用刚刚建立的那张数据库表。能够经过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可使用如下方法来实现加锁操做:

在查询语句后面增长for update,数据库会在查询过程当中给数据库表增长排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有经过索引进行检索的时候才会使用行级锁,不然会使用表级锁。这里咱们但愿使用行级锁,就要给method_name添加索引,值得注意的是,这个索引必定要建立成惟一索引,不然会出现多个重载方法之间没法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁以后,其余线程没法再在该行记录上增长排他锁。

咱们能够认为得到排它锁的线程便可得到分布式锁,当获取到锁以后,能够执行方法的业务逻辑,执行完方法以后,再经过如下方法解锁:

经过connection.commit()操做来释放锁。

这种方法能够有效的解决上面提到的没法释放锁和阻塞锁的问题。

  • 阻塞锁? for update语句会在执行成功后当即返回,在执行失败时一直处于阻塞状态,直到成功。
  • 锁定以后服务宕机,没法释放?使用这种方式,服务宕机以后数据库会本身把锁释放掉。

可是仍是没法直接解决数据库单点和可重入问题。

这里还可能存在另一个问题,虽然咱们对method_name 使用了惟一索引,而且显示使用for update来使用行级锁。可是,MySql会对查询进行优化,即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。若是发生这种状况就悲剧了。。。


还有一个问题,就是咱们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库链接。一旦相似的链接变得多了,就可能把数据库链接池撑爆

总结

总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是经过表中的记录的存在状况肯定当前是否有锁存在,另一种是经过数据库的排他锁来实现分布式锁。

数据库实现分布式锁的优势

直接借助数据库,容易理解。

数据库实现分布式锁的缺点

会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂。

操做数据库须要必定的开销,性能问题须要考虑。

使用数据库的行级锁并不必定靠谱,尤为是当咱们的锁表并不大的时候。


基于缓存实现分布式锁

相比较于基于数据库实现分布式锁的方案来讲,基于缓存来实如今性能方面会表现的更好一点。并且不少缓存是能够集群部署的,能够解决单点问题。

目前有不少成熟的缓存产品,包括Redis,memcached以及咱们公司内部的Tair。

这里以Tair为例来分析下使用缓存实现分布式锁的方案。关于Redis和memcached在网络上有不少相关的文章,而且也有一些成熟的框架及算法能够直接使用。

基于Tair的实现分布式锁其实和Redis相似,其中主要的实现方式是使用TairManager.put方法来实现。

以上实现方式一样存在几个问题:

一、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在tair中,其余线程没法再得到到锁。

二、这把锁只能是非阻塞的,不管成功仍是失败都直接返回。

三、这把锁是非重入的,一个线程得到锁以后,在释放锁以前,没法再次得到该锁,由于使用到的key在tair中已经存在。没法再执行put操做。

固然,一样有方式能够解决。

  • 没有失效时间?tair的put方法支持传入失效时间,到达时间以后数据会自动删除。
  • 非阻塞?while重复执行。
  • 非可重入?在一个线程获取到锁以后,把当前主机信息和线程信息保存起来,下次再获取以前先检查本身是否是当前锁的拥有者。

可是,失效时间我设置多长时间为好?如何设置的失效时间过短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。若是设置的时间太长,其余获取锁的线程就可能要平白的多等一段时间。这个问题使用数据库实现分布式锁一样存在


总结

可使用缓存来代替数据库来实现分布式锁,这个能够提供更好的性能,同时,不少缓存服务都是集群部署的,能够避免单点问题。而且不少缓存服务都提供了能够用来实现分布式锁的方法,好比Tair的put方法,redis的setnx方法等。而且,这些缓存服务也都提供了对数据的过时自动删除的支持,能够直接设置超时时间来控制锁的释放。

使用缓存实现分布式锁的优势

性能好,实现起来较为方便。

使用缓存实现分布式锁的缺点

经过超时时间来控制锁的失效时间并非十分的靠谱。


基于Zookeeper实现分布式锁

基于zookeeper临时有序节点能够实现的分布式锁。

大体思想即为:每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。同时,其能够避免服务宕机致使的锁没法释放,而产生的死锁问题。

来看下Zookeeper能不能解决前面提到的问题。

  • 锁没法释放?使用Zookeeper能够有效的解决锁没法释放的问题,由于在建立锁的时候,客户端会在ZK中建立一个临时节点,一旦客户端获取到锁以后忽然挂掉(Session链接断开),那么这个临时节点就会自动删除掉。其余客户端就能够再次得到锁。

  • 非阻塞锁?使用Zookeeper能够实现阻塞的锁,客户端能够经过在ZK中建立顺序节点,而且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端能够检查本身建立的节点是否是当前全部节点中序号最小的,若是是,那么本身就获取到锁,即可以执行业务逻辑了。

  • 不可重入?使用Zookeeper也能够有效的解决不可重入的问题,客户端在建立节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就能够了。若是和本身的信息同样,那么本身直接获取到锁,若是不同就再建立一个临时的顺序节点,参与排队。

  • 单点问题?使用Zookeeper能够有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就能够对外提供服务。

能够直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。

Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。

使用ZK实现的分布式锁好像彻底符合了本文开头咱们对一个分布式锁的全部指望。可是,其实并非,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并无缓存服务那么高。由于每次在建立锁和释放锁的过程当中,都要动态建立、销毁瞬时节点来实现锁功能。ZK中建立和删除节点只能经过Leader服务器来执行,而后将数据同不到全部的Follower机器上。

其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的状况,因为网络抖动,客户端可ZK集群的session链接断了,那么zk觉得客户端挂了,就会删除临时节点,这时候其余客户端就能够获取到分布式锁了。就可能产生并发问题。这个问题不常见是由于zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。屡次重试以后还不行的话才会删除临时节点。(因此,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)


总结

使用Zookeeper实现分布式锁的优势

有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。实现起来较为简单。

使用Zookeeper实现分布式锁的缺点

性能上不如使用缓存实现分布式锁。 须要对ZK的原理有所了解。


三种方案的比较

上面几种方式,哪一种方式都没法作到完美。就像CAP同样,在复杂性、可靠性、性能等方面没法同时知足,因此,根据不一样的应用场景选择最适合本身的才是王道。

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

 
 
 
 
 
 
 
 
 
参考:https://www.cnblogs.com/austinspark-jessylu/p/8043726.html
相关文章
相关标签/搜索