分布式锁讲解:
在讲分布式锁之前,我们先了解一下什么是分布式锁,为什么要使用分布式锁。
- 什么是分布式锁?
分布式锁是在分布式系统中所使用的一种锁机制,是作用于多个JVM之间的锁机制。 - 为什么要使用分布式锁?
为了防止在分布式系统中,多个JVM操作同一块资源而造成资源协调问题,例如上图。
3. 分布式锁实现原理
其实所有的锁实现原理都是一样的,分三步走:
第一步:获取锁
第二步:等待获取
第三步:释放锁
只是不同的中间件基于各自的特点,写法不一样,但是实现思路基本是一致的,比如基于redis,由于redis的key不能重复,因此key可以作为锁,且只有一把,而基于zk的原理则是,zk的节点路径不能重复,是唯一的,因此锁也是唯一的。
- 基于redis实现分布式锁
按照上面三部,我们来写写实现思路:
第一步:获取锁,多个JVM去redis上创建一个key,这个key就是锁,由于redis是单线程的,因此只有一个JVM能创建成功,谁创建成功了,谁就获取了锁,没有创建成功的则继续等待,而且key是不能重复的,因此锁是唯一的,我们使用redis的setIfAsbent命令,创建成功返回true,否则返回false
第二步:等待获取,我们可以设置等待时间,在时间内则继续等待,否则获取锁失败,也可以一直等待,这里是超时就失败
第三步:释放锁,业务逻辑处理完成后,将锁释放,此时其他的JVM继续获取锁
上代码:
**
## 获取锁的源码:
**
引包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
/**
* 获取锁
*
* @param lockKey 锁的key值
* @param waitTime 已经有一个jvm获取了锁,其他jvm的等待时间
* @param timeOut 超出设置的等待时间,比如释放锁的方法报错等异常导致没有释放掉锁,为了防止死锁,设置超时时间,超出时间,则自动释放
*/
public String lock(String lockKey, long waitTime, long timeOut) {
// 获取锁的最终时间
long endTime = System.currentTimeMillis() + waitTime;
// 当前时间小于最终截止时间,则其他的jvm继续等待
while (System.currentTimeMillis() < endTime) {
String lockValue = UUID.randomUUID().toString();
// 为true则说明锁之前还未创建,现在新创建
if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue)) {
redisTemplate.expire(lockKey, timeOut, TimeUnit.MILLISECONDS);
// 退出循环
return lockValue;
}
// 其他jvm线程继续等待超时
}
return null;
}
**
## 释放锁的源码:
**
/**
* 释放锁
*
* @param lockKey 锁的key值
* @param lockValue value值
* @return
*/
public boolean unlock(String lockKey, String lockValue) {
try {
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
return redisTemplate.delete(lockKey);
}
} catch (Exception e) {
System.out.println("释放锁异常");
}
return false;
}
- 基于zookeeper实现分布式锁
按照上面三部,我们来写写实现思路:
第一步:获取锁,多个JVM连接zk去创建临时节点,临时节点是不能重复的,谁创建了临时节点,则表明谁获取了锁,没有获取锁的JVM则继续等待
第二步:等待获取,我们可以设置等待时间,在时间内则继续等待,否则获取锁失败,也可以一直等待,这里是一直等待
第三步:释放锁,业务逻辑处理完成后,断开客户端与zk的连接,则临时节点会被删除,相当于释放了锁,则其他JVM可以继续去创建临时节点(锁)
上代码:
**
## 源码:
**
引包:
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
/**
*
* 创建zk连接
*/
public class ZookeeperLink {
private static ZkClient zkClient = null;
public static ZkClient createZkClient() {
if (zkClient != null) {
zkClient.close();
}
zkClient = new ZkClient("127.0.0.1:2181", 5000);
return zkClient;
}
}
/**
*
* 获取锁和释放锁的类
*/
public class ZkLock {
/**
* 1.获取锁:谁创建了临时节点谁就获取了锁 2.释放锁:删除掉临时节点就释放了锁 3.等待:未获取锁等待重试循环一直获取锁
*/
private static final String PATH = "/zkKey";
private CountDownLatch countDownLatch = new CountDownLatch(1);
/**
* 获取锁
*/
public boolean getLock() {
// 调用获取锁方法,成功则返回true
if (tryLock()) {
System.out.println("获取到了锁");
return true;
} else {
// 等待获取锁,并进行事件监听,只要锁被释放了,就立即去重新获取锁,调用getLock()
waitLock();
return getLock();
}
}
/**
* 尝试创建节点
*
* @return
*/
private boolean tryLock() {
try {
ZookeeperLink.createZkClient().createEphemeral(PATH);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 等待获取锁
*
*/
private void waitLock() {
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
countDownLatch.countDown();
}
};
// 做事件监听,节点被删除会立即通知
ZookeeperLink.createZkClient().subscribeDataChanges(PATH, iZkDataListener);
try {
countDownLatch.await();
// 取消事件监听
ZookeeperLink.createZkClient().unsubscribeDataChanges(PATH, iZkDataListener);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 释放锁
*/
public void unLock() {
ZookeeperLink.createZkClient().close();
}
}
以上就是分布式锁的实现思路,这只是其中的一种,获取不到锁就等待,也可以用另外一种思路,比如重试机制,只需要把上面的代码稍稍修改一下,比如,重试3次,每次间隔3秒钟,实现思路就是,在获取锁的方法上加个for循环,循环三次,每次循环完线程休眠3秒,就可以实现重试机制了。
最后如果redis不了解原理的可以参考我的另一篇文章:
https://blog.csdn.net/weixin_43727615/article/details/115548679