分布式锁的多种完成办法

现在简直许多大型网站及运用都是分布式布置的,分布式场景中的数据一致性问题一向是一个比较重要的论题。分布式的CAP理论告知咱们“任何一个分布式体系都无法一起满意一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能一起满意两项。”所以,许多体系在规划之初就要对这三者做出取舍。在互联网范畴的绝大多数的场景中,都需求献身强一致性来交换体系的高可用性,体系往往只需求确保“终究一致性”,只需这个终究时刻是在用户能够承受的范围内即可。

在许多场景中,咱们为了确保数据的终究一致性,需求许多的技能计划来支撑,比方分布式事务、分布式锁等。有的时分,咱们需求确保一个办法在同一时刻内只能被同一个线程履行。在单机环境中,Java中其实供给了许多并发处理相关的API,可是这些API在分布式场景中就力不从心了。也便是说单纯的Java Api并不能供给分布式锁的才能。所以针对分布式锁的完成现在有多种计划。

针对分布式锁的完成,现在比较常用的有以下几种计划:

  • 依据数据库完成分布式锁
  • 依据缓存完成分布式锁
  • 依据Zookeeper完成分布式锁

在剖析这几种完成计划之前咱们先来想一下,咱们需求的分布式锁应该是怎么样的?(这儿以办法锁为例,资源锁同理)

  • 能够确保在分布式布置的运用集群中,同一个办法在同一时刻只能被一台机器上的一个线程履行。
  • 这把锁要是一把可重入锁(防止死锁)
  • 这把锁最好是一把堵塞锁(依据事务需求考虑要不要这条)
  • 有高可用的获取锁和开释锁功用
  • 获取锁和开释锁的功用要好
  • 依据数据库完成分布式锁

依据数据库表

要完成分布式锁,最简略的办法或许便是直接创立一张锁表,然后经过操作该表中的数据来完成了。

当咱们要锁住某个办法或资源时,咱们就在该表中添加一条记载,想要开释锁的时分就删去这条记载。

创立这样一张数据库表:

CREATE TABLE `methodLock` (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '确认的办法名',
 `desc` varchar(1024) NOT NULL DEFAULT '补白信息',
 `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时刻,主动生成',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='确认中的办法';

当咱们想要锁住某个办法时,履行以下SQL:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

因为咱们对method_name做了仅有性束缚,这儿假如有多个恳求一起提交到数据库的话,数据库会确保只需一个操作能够成功,那么咱们就能够认为操作成功的那个线程取得了该办法的锁,能够履行办法体内容。

当办法履行完毕之后,想要开释锁的话,需求履行以下Sql:

delete from methodLock where method_name ='method_name'

上面这种简略的完成有以下几个问题:

1、这把锁强依靠数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致事务体系不行用。2、这把锁没有失效时刻,一旦解锁操作失利,就会导致锁记载一向在数据库中,其他线程无法再取得到锁。3、这把锁只能对错堵塞的,因为数据的insert操作,一旦刺进失利就会直接报错。没有取得锁的线程并不会进入排队行列,要想再次取得锁就要再次触发取得锁操作。4、这把锁对错重入的,同一个线程在没有开释锁之前无法再次取得该锁。因为数据中数据现已存在了。当然,咱们也能够有其他办法处理上面的问题。

  • 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
  • 没有失效时刻?只需做一个守时使命,每隔一守时刻把数据库中的超时数据整理一遍。
  • 非堵塞的?搞一个while循环,直到insert成功再回来成功。
  • 非重入的?在数据库表中加个字段,记载当时取得锁的机器的主机信息和线程信息,那么下次再获取锁的时分先查询数据库,假如当时机器的主机信息和线程信息在数据库能够查到的话,直接把锁分配给他就能够了。

依据数据库排他锁

除了能够经过增删操作数据表中的记载以外,其实还能够凭借数据中自带的锁来完成分布式的锁。

咱们还用刚刚创立的那张数据库表。能够经过数据库的排他锁来完成分布式锁。 依据MySql的InnoDB引擎,能够运用以下办法来完成加锁操作:

public boolean lock(){
   connection.setAutoCommit(false)
   while(true){
       try{
           result = select * from methodLock where method_name=xxx for update;
           if(result==null){
               return true;
           }
       }catch(Exception e){

       }
       sleep(1000);
   }
   return false;
}

在查询句子后边添加for update,数据库会在查询过程中给数据库表添加排他锁(这儿再多提一句,InnoDB引擎在加锁的时分,只需经过索引进行检索的时分才会运用行级锁,不然会运用表级锁。这儿咱们希望运用行级锁,就要给method_name添加索引,值得注意的是,这个索引必定要创立成仅有索引,不然会呈现多个重载办法之间无法一起被拜访的问题。重载办法的话主张把参数类型也加上。)。当某条记载被加上排他锁之后,其他线程无法再在该行记载上添加排他锁。

咱们能够认为取得排它锁的线程即可取得分布式锁,当获取到锁之后,能够履行办法的事务逻辑,履行完办法之后,再经过以下办法解锁:

public void unlock(){
   connection.commit();
}

经过connection.commit()操作来开释锁。

这种办法能够有用的处理上面说到的无法开释锁和堵塞锁的问题。

  • 堵塞锁? for update句子会在履行成功后当即回来,在履行失利时一向处于堵塞状态,直到成功。
  • 确认之后服务宕机,无法开释?运用这种办法,服务宕机之后数据库会自己把锁开释掉。

可是仍是无法直接处理数据库单点和可重入问题。

这儿还或许存在别的一个问题,尽管咱们对method_name 运用了仅有索引,而且显现运用for update来运用行级锁。可是,MySql会对查询进行优化,即便在条件中运用了索引字段,可是否运用索引来检索数据是由 MySQL 经过判别不同履行计划的价值来决议的,假如 MySQL 认为全表扫功率更高,比方对一些很小的表,它就不会运用索引,这种状况下 InnoDB 将运用表锁,而不是行锁。假如发作这种状况就悲惨剧了。。。

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

总结

总结一下运用数据库来完成分布式锁的办法,这两种办法都是依靠数据库的一张表,一种是经过表中的记载的存在状况确认当时是否有锁存在,别的一种是经过数据库的排他锁来完成分布式锁。

数据库完成分布式锁的长处

直接凭借数据库,简略了解。

数据库完成分布式锁的缺陷

会有各式各样的问题,在处理问题的过程中会使整个计划变得越来越杂乱。

操作数据库需求必定的开支,功用问题需求考虑。

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

依据缓存完成分布式锁

相比较于依据数据库完成分布式锁的计划来说,依据缓存来完成在功用方面会体现的更好一点。而且许多缓存是能够集群布置的,能够处理单点问题。

现在有许多老练的缓存产品,包含Redis,memcached以及咱们公司内部的Tair。

这儿以Tair为例来剖析下运用缓存完成分布式锁的计划。关于Redis和memcached在网络上有许多相关的文章,而且也有一些老练的结构及算法能够直接运用。

依据Tair的完成分布式锁其实和Redis相似,其间首要的完成办法是运用TairManager.put办法来完成。

public boolean trylock(String key) {
   ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0);
   if (ResultCode.SUCCESS.equals(code))
       return true;
   else
       return false;
}
public boolean unlock(String key) {
   ldbTairManager.invalid(NAMESPACE, key);
}

以上完成办法相同存在几个问题:

1、这把锁没有失效时刻,一旦解锁操作失利,就会导致锁记载一向在tair中,其他线程无法再取得到锁。2、这把锁只能对错堵塞的,不管成功仍是失利都直接回来。3、这把锁对错重入的,一个线程取得锁之后,在开释锁之前,无法再次取得该锁,因为运用到的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客户端,这个客户端中封装了一个可重入的锁服务。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
   try {
       return interProcessMutex.acquire(timeout, unit);
   } catch (Exception e) {
       e.printStackTrace();
   }
   return true;
}
public boolean unlock() {
   try {
       interProcessMutex.release();
   } catch (Throwable e) {
       log.error(e.getMessage(), e);
   } finally {
       executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
   }
   return true;
}

Curator供给的InterProcessMutex是分布式锁的完成。acquire办法用户获取锁,release办法用于开释锁。

运用ZK完成的分布式锁如同完全符合了本文最初咱们对一个分布式锁的全部希望。可是,其实并不是,Zookeeper完成的分布式锁其实存在一个缺陷,那便是功用上或许并没有缓存服务那么高。因为每次在创立锁和开释锁的过程中,都要动态创立、毁掉瞬时节点来完成锁功用。ZK中创立和删去节点只能经过Leader服务器来履行,然后将数据同不到全部的Follower机器上。

其实,运用Zookeeper也有或许带来并发问题,仅仅并不常见罢了。考虑这样的状况,因为网络颤动,客户端可ZK集群的session衔接断了,那么zk认为客户端挂了,就会删去暂时节点,这时分其他客户端就能够获取到分布式锁了。就或许发生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支撑多种重试战略。屡次重试之后还不行的话才会删去暂时节点。(所以,挑选一个适宜的重试战略也比较重要,要在锁的粒度和并发之间找一个平衡。)

总结

运用Zookeeper完成分布式锁的长处

有用的处理单点问题,不行重入问题,非堵塞问题以及锁无法开释的问题。完成起来较为简略。

运用Zookeeper完成分布式锁的缺陷

功用上不如运用缓存完成分布式锁。 需求对ZK的原理有所了解。

三种计划的比较

上面几种办法,哪种办法都无法做到完美。就像CAP相同,在杂乱性、可靠性、功用等方面无法一起满意,所以,依据不同的运用场景挑选最适合自己的才是王道。

从了解的难易程度视点(从低到高)

数据库 > 缓存 > Zookeeper

从完成的杂乱性视点(从低到高)

Zookeeper >= 缓存 > 数据库

从功用视点(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性视点(从高到低)

Zookeeper > 缓存 > 数据库

vwin娱乐场

宣布我的谈论

撤销谈论
表情 插代码

Hi,您需求填写昵称和邮箱!

  • 必填项
  • 必填项