分布式锁的应用与实现原理

2018-03-16  本文已影响483人  千淘萬漉

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发环境下的线程安全方法,在JVM层面以通过Synchronized关键字或者Lock接口,可协调资源像原子性一样操作。但是在分布式环境下,跨JVM无法这些都无法发挥作用,只能寻求公证人(第三方服务)进行协调,典型的场景如在进行Quartz调度的时候,如果碰到集群就可能会用类似的应用场景,见下图:

Quartz如何保证多个节点的应用只进行一次调度(即某一时刻的调度任务只由其中一台服务器执行)?针对这种场景,目前比较常用的有以下几种方案:

在分析这几种实现方案之前我们先来想一下,我们需要的分布式锁应该是怎么样的?(这里以方法锁为例,资源锁同理)
可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

source: 《分布式锁的几种实现方式》

一、基于数据库实现分布式锁


1.基于数据库表

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

创建这样一张数据库表:

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'

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

当然解决方案也是有的:

虽然直接借助数据库,容易理解。但是会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂,而且操作数据库需要一定的开销,性能问题需要考虑。

二、基于Redis实现分布式锁(推荐)

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。目前有很多成熟的缓存产品,包括Redis,memcached以及Tair。在这里就以常见的Redis来讲讲其实现:

Quartz定时器到时间被触发,程序开始先争取一个redis锁来执行任务。redis的命令可以采用如下:

SET key value [EX seconds] [PX milliseconds] [NX|XX]
   public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

总的来说,执行上面的set()方法就只会导致两种结果:

  1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端
  2. 已有锁存在,不做任何操作。

加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。

当然除了setnx(),还有基于Redlock做分布式锁、基于Redisson做分布式锁,Redis做分布式锁是性能最好的(优于Zookeeper锁),但是可靠性方面逊于ZK,且通过超时时间来控制锁的失效时间并不是十分的靠谱。

source:Redis分布式锁的正确实现方式

三、基于Zookeeper实现分布式锁(推荐)

基于Zookeeper的临时有序节点特性可以实现分布式锁,原理是:每个客户端对某个方法加锁时,在 Zookeeper上与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

ZK锁可分为排他锁和共享读锁两种情况:

排他锁流程

以上两种锁方式在加锁解锁操作上都类似的,获取锁上都可以通过客户端通过调用create方法创建表示锁的临时节点,可以认为创建成功的客户端获得了锁,同时可以让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况。释放锁,两种情况都可以让锁释放:1、当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除;2、正常执行完业务逻辑,客户端主动删除自己创建的临时节点。

注意:当集群规模比较大时,系统中将有大量的 “Watcher通知” 和 “子节点列表获取” 这个操作重复执行,这些 操作会对Zookeeper造成巨大的性能影响和网络冲击,引发“羊群效应”,获取锁的方式可以优化。

总结:在可靠性方面,ZK锁是优于Redis,释放锁上无需失效时间来保证更为稳定靠谱,但是牺牲了一定的性能,且实现相对复杂。

source: 《利用Zookeeper实现 - 分布式锁》

上一篇下一篇

猜你喜欢

热点阅读