1.背景
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
2.分布式锁的条件
2.1、分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2.2、高可用的获取锁与释放锁;
2.3、高性能的获取锁与释放锁;
2.4、可重入特性;
2.5、防止死锁;
2.6、非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
3.几种分布式锁
基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;
4.redis 的实现方式
4.1 相关依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
4.2 加锁和解锁工具类
public class RedisUtil {
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;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
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;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
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(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
4.3 测试
@Test void contextLoads() { Jedis jedis = new Jedis("localhost"); boolean b = RedisUtil.tryGetDistributedLock(jedis, "23", "123", 2); System.out.println(b); try { sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } boolean c = RedisUtil.tryGetDistributedLock(jedis, "23", "124", 2); System.out.println(b); boolean b0 = RedisUtil.releaseDistributedLock(jedis, "23", "123"); System.out.println(b0+"--b0"); System.out.println("c="+c); boolean b1 = RedisUtil.releaseDistributedLock(jedis, "23", "124"); System.out.println("b1="+b1); }
5.zookeeper的实现方式
5.1 定义
zookeeper 实现分布式锁是基于 zookeeper 数据存储结构,zookeeper 的数据存储结构类似于一棵树。其中的节点叫 Znode。
Znode 有四种类型,分别为 持久节点(PERSISTENT)、持久节点顺序节点(PERSISTENT_SEQUENTIAL) 、临时节点(EPHEMERAL)、临时顺序节点(EPHEMERAL_SEQUENTIAL)。
而 zookeeper 实现分布式锁原理是依据临时顺序节点(EPHEMERAL_SEQUENTIAL) 来实现的。下面我们来学习一个 zookeeper 是如何利用临时顺序节点实现分布式锁的。
5.2 分布式锁原理:
首先,我们创建一个持久节点 Lock,当客户端想要拿到锁时,需要创建一个临时节点 lock1。
用户1 创建了临时节点 lock1 并拿到了锁。这时,假设又来了个用户2 想要拿锁。
用户2 也在 Lock 下创建了临时节点 lock2,遍历 Lock 判断自己的 lock2 前面是否还有节点,如果没有,说明自己是第一个,就顺利拿锁。如果有则表明锁被人拿了,用户2将会注册一个 Watcher去监听 lock1 是否存在。
这时,又来了一个 用户3 想要拿锁,在 Lock 下创建了一个 lock3 节点,结果也发现自己不是最前面的一个。便会注册一个 Watcher 去监听上一个节点,也就是 lock2 判断其是存在。
用户1 的业务执行完毕,可以释放锁了。执行完毕后,用户1 调用节点删除的指令,将临时节点 lock1 删除。
由于 用户2 一直在监听着 lock1,当 lock1 释放锁删除节点后,顺理成章的拿到了锁。而 lock3 还在监听着 lock2,等着它释放锁。
上面是正常业务执行完毕的情况下释放锁。假设,执行到中途,客户端崩溃了,与 zookeeper 断开了连接,又会如何呢?
临时节点的特性:仅当创建者会话有效时才得以保存,如果客户端崩溃了,那创建的会话是无效的,那样与客户端相关联的节点也会被删除。
监听 lock2 的客户端 用户3 发现节点 lock2 被删除,那样他也可以拿到锁了。
最后,当用户3 也释放锁,整个分布式锁原理就这样结束了。