分布式锁的几种实现方式

1、为何要使用分布式锁

为了保证一个方法或属性在高并发状况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的状况下,可使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。可是,随着业务发展的须要,原单体单机部署的系统被演化成分布式集群系统后,因为分布式系统多线程、多进程而且分布在不一样机器上,这将使原单机部署状况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就须要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!java

2、分布式锁应该具有哪些条件

在分析分布式锁的三种实现方式以前,先了解一下分布式锁应该具有哪些条件:
一、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
二、高可用的获取锁与释放锁;
三、高性能的获取锁与释放锁;
四、具有可重入特性;
五、具有锁失效机制,防止死锁;
六、具有非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。web

3、分布式锁的三种实现方式

目前几乎不少大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉咱们“任何一个分布式系统都没法同时知足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时知足两项。”因此,不少系统在设计之初就要对这三者作出取舍。在互联网领域的绝大多数的场景中,都须要牺牲强一致性来换取系统的高可用性,系统每每只须要保证“最终一致性”,只要这个最终时间是在用户能够接受的范围内便可。redis

在不少场景中,咱们为了保证数据的最终一致性,须要不少的技术方案来支持,好比分布式事务、分布式锁等。有的时候,咱们须要保证一个方法在同一时间内只能被同一个线程执行。
基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;
尽管有这三种方案,可是不一样的业务也要根据本身的状况进行选型,他们之间没有最好只有更适合!算法

1. 基于数据库的实现方式

基于数据库的实现方式的核心思想是:在数据库中建立一个表,表中包含方法名等字段,并在方法名字段上建立惟一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。sql

1.1 建立一个表:数据库

DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名', `desc` varchar(255) NOT NULL COMMENT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名', `desc` varchar(255) NOT NULL COMMENT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

1.2 想要执行某个方法,就使用这个方法名向表中插入数据:缓存

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName'); INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');

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

1.3 成功插入则获取锁,执行完成后删除对应的行数据释放锁:并发

delete from method_lock where method_name ='methodName'; delete from method_lock where method_name ='methodName';

注意:这只是使用基于数据库的一种方法,使用数据库实现分布式锁还有不少其余的玩法!
上面这种简单的实现有如下几个问题:分布式

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。
  2. 这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。
  3. 这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。
  4. 这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。

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

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

数据库实现分布式锁的优势:直接借助数据库,容易理解。

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

  1. 会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂。
  2. 操做数据库须要必定的开销,性能问题须要考虑。
  3. 使用数据库的行级锁并不必定靠谱,尤为是当咱们的锁表并不大的时候。

2. 基于Redis的实现方式

选用Redis实现分布式锁缘由:
(1)Redis有很高的性能;
(2)Redis命令对此支持较好,实现起来比较方便

缓存系统在实现的时候跟数据库的模式差很少,可是由于数据都是在缓存中,因此加锁和解锁都会比数据库快不少。

下面举例看看基于 Redis 的分布式锁实现。Redis 的分布式锁都是基于一个命令 – SETNX,也就是 SET IF NOT EXIST,若是不存在就写入。从 Redis 2.6.12 版本开始,Redis 的 SET 命令直接直接设置 NX 和 EX 属性,NX 即附带了 SETNX 数据,key 存在就没法插入,EX 是过时属性,能够设置过时时间。这样一个命令就能原子的完成加锁和设置过时时间。

pom文件是这样。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
public class RedisManager { public static JedisPool jedisPool; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * * 过时时间设置 * EX second :设置键的过时时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。 * PX millisecond :设置键的过时时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。 * * 执行条件设置 * NX :只在键不存在时,才对键进行设置操做。 SET key value NX 效果等同于 SETNX key value 。 * XX :只在键已经存在时,才对键进行设置操做。 */ static { //读取相关的配置 ResourceBundle resourceBundle = ResourceBundle.getBundle("redis"); int maxActive = Integer.parseInt(resourceBundle.getString("redis.pool.maxActive")); int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle")); int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait")); String ip = resourceBundle.getString("redis.ip"); int port = Integer.parseInt(resourceBundle.getString("redis.port")); JedisPoolConfig config = new JedisPoolConfig(); //设置最大链接数 config.setMaxTotal(maxActive); //设置最大空闲数 config.setMaxIdle(maxIdle); //设置超时时间 config.setMaxWaitMillis(maxWait); //初始化链接池 jedisPool = new JedisPool(config, ip, port); } public static boolean tryLock(String key,String value,int expireSecond){ Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireSecond); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } public static boolean releaseDistributedLock(String key,String value) { Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } public static void main(String[] args){ Printer.println(tryLock("A","B",100)); Printer.println(releaseDistributedLock("A","B")); } } public class RedisManager { public static JedisPool jedisPool; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * * 过时时间设置 * EX second :设置键的过时时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。 * PX millisecond :设置键的过时时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。 * * 执行条件设置 * NX :只在键不存在时,才对键进行设置操做。 SET key value NX 效果等同于 SETNX key value 。 * XX :只在键已经存在时,才对键进行设置操做。 */ static { //读取相关的配置 ResourceBundle resourceBundle = ResourceBundle.getBundle("redis"); int maxActive = Integer.parseInt(resourceBundle.getString("redis.pool.maxActive")); int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle")); int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait")); String ip = resourceBundle.getString("redis.ip"); int port = Integer.parseInt(resourceBundle.getString("redis.port")); JedisPoolConfig config = new JedisPoolConfig(); //设置最大链接数 config.setMaxTotal(maxActive); //设置最大空闲数 config.setMaxIdle(maxIdle); //设置超时时间 config.setMaxWaitMillis(maxWait); //初始化链接池 jedisPool = new JedisPool(config, ip, port); } public static boolean tryLock(String key,String value,int expireSecond){ Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireSecond); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } public static boolean releaseDistributedLock(String key,String value) { Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } public static void main(String[] args){ Printer.println(tryLock("A","B",100)); Printer.println(releaseDistributedLock("A","B")); } }

除此以外,Redis 的做者还实现了一个分布式锁算法,叫Redlock
以上实现方式一样存在几个问题:

  1. 这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在tair中,其余线程没法再得到到锁。
  2. 这把锁只能是非阻塞的,不管成功仍是失败都直接返回。
  3. 这把锁是非重入的,一个线程得到锁以后,在释放锁以前,没法再次得到该锁,由于使用到的key在tair中已经存在。没法再执行put操做。

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

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

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

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

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

  1. 性能好,实现起来较为方便。
  2. 使用缓存实现分布式锁的缺点
  3. 经过超时时间来控制锁的失效时间并非十分的靠谱。

3. 基于ZooKeeper的实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个惟一文件名。基于ZooKeeper实现分布式锁的步骤以下:

  1. 建立一个目录mylock;
  2. 线程A想获取锁就在mylock目录下建立临时顺序节点;
  3. 获取mylock目录下全部的子节点,而后获取比本身小的兄弟节点,若是不存在,则说明当前线程顺序号最小,得到锁;
  4. 线程B获取全部节点,判断本身不是最小节点,设置监听比本身次小的节点;
  5. 线程A处理完,删除本身的节点,线程B监听到变动事件,判断本身是否是最小的节点,若是是则得到锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优势:具有高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:由于须要频繁的建立和删除节点,性能上不如Redis方式。

4. 总结

上面的三种实现方式,没有在全部场合都是完美的,因此,应根据不一样的应用场景选择最适合的实现方式。

在分布式环境中,对资源进行上锁有时候是很重要的,好比抢购某一资源,这时候使用分布式锁就能够很好地控制资源。
固然,在具体使用中,还须要考虑不少因素,好比超时时间的选取,获取锁时间的选取对并发量都有很大的影响

参考:
http://www.hollischuang.com/archives/1716
https://blog.csdn.net/xlgen157387/article/details/79036337
https://toutiao.io/posts/8vnqlo/preview

相关文章
相关标签/搜索