基于zookeeper实现分布式锁介绍
前言
本文主要用于介绍常规分布式锁的使用及其原理,在主篇中进行了常规分布式锁的扫盲介绍,在子篇中介绍了现主流分布式锁框架的源码以及自写学习demo解析。
全部代码及介绍:https://gitee.com/FWEM/distributed-lock
文章主要分为以下两个部分:
1. Curator框架的InterProcessMutex分布式锁
1、基本实现思路
- 首先在集群中注册一个根节点
- 多个进程往该根节点下创建临时有序节点
- 序号最前的节点获得锁,执行相应业务流程后删除当前节点(释放锁)
- 非序号最前的节点监听(watch)前一节点,阻塞等待前一节点释放
- 前一节点释放锁后触发监听,唤醒某进程继续执行业务
原理图:
2、基本流程
3、核心代码
Client构建
String zkServerAddress = "localhost:2181";
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
CuratorFramework zkClient = CuratorFrameworkFactory.builder()
.connectString(zkServerAddress)
.sessionTimeoutMs(100000)
.connectionTimeoutMs(10000)
.retryPolicy(retryPolicy)
.build();
zkClient.start();
return zkClient;
基础使用方法
// 创建分布式锁,根节点为/curator/locks
InterProcessMutex mutex = new InterProcessMutex(zkClient, "/curator/locks");
// 尝试获取锁
mutex.acquire();
//执行业务
doSomething();
// 锁释放
mutex.release();
4、核心类分析
组成
该分布式锁主要包含了三个核心类InterProcessMutex、LockInternals、LockInternalsDriver,其中InterProcessMutex包含成员对象LockInternals,LockInternals里面包含了锁操作的实现逻辑以及LockInternalsDriver对象,LockInternalsDriver提供了一些协助锁操作完成的方法,相当于一个工具类。
InterProcessMutex
该类为分布式锁实现的顶层逻辑,实现了InterProcessLock接口,完成了LockInternals初始化的功能,对外提供了分布式锁操作的方法的
public interface InterProcessLock
{
// 尝试获取锁
public void acquire() throws Exception;
// 给定时间内获取锁
public boolean acquire(long time, TimeUnit unit) throws Exception;
// 释放锁
public void release() throws Exception;
// 当前是否获得锁
boolean isAcquiredInThisProcess();
}
字段说明
类型 | 名称 | 说明 |
---|---|---|
LockInternals | internals | 该类中实现分布式锁的核心逻辑 |
String | basePath | 分布式锁的根路径 |
ConcurrentMap<Thread, LockData> | threadData | 存储进程信息与锁信息的映射 |
String | LOCK_NAME | 子节点名称前缀 |
内部类
private static class LockData
{
// 线程信息
final Thread owningThread;
// 锁的全路径
final String lockPath;
// 该锁的重入次次数
final AtomicInteger lockCount = new AtomicInteger(1);
private LockData(Thread owningThread, String lockPath)
{
this.owningThread = owningThread;
this.lockPath = lockPath;
}
}
构造方法说明
// 该类存在三个构造方法,最终调用的构造方法如下,此处主要完成了internals的初始化,前面提到该类才是实现分布式锁逻辑的类。
/**
*
* @param client zookeeper连接
* @param path 锁根路径
* @param lockName 锁前缀
* @param maxLeases 最大租赁数,后续用于比对当前节点是否为排在最前的节点
* @param driver 工具类driver
*/
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver)
{
basePath = PathUtils.validatePath(path);
internals = new LockInternals(client, driver, path, lockName, maxLeases);
}
获取锁逻辑说明
// 外层获取锁的调用方法为acquire,此处存在两个acquire方法,一个是制定了锁阻塞等待时间的,超时如果没有获得锁将会被唤醒,获取锁失败。一个为不指定等待时间的,线程将会一直阻塞直到获取锁或连接超时抛出异常唤醒。代码如下,其主要调用了internalLock方法。
@Override
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
@Override
public boolean acquire(long time, TimeUnit unit) throws Exception
{
return internalLock(time, unit);
}
// internalLock方法内部逻辑首先获取当前线程信息,然后查看当前线程是否获得锁,已获得锁的话则可重入次数+1,还未获得锁则去尝试获得锁,获取成功则存入map中,记录线程与锁的映射信息
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
// 获取当前线程信息
Thread currentThread = Thread.currentThread();
// 查看当前线程是否已获得锁
LockData lockData = threadData.get(currentThread);
// 当前线程已获得锁
if ( lockData != null )
{
// 可重入次数+1
lockData.lockCount.incrementAndGet();
return true;
}
// 尝试获得锁,internals.attemptLock此处阻塞
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
// 记录线程与锁映射信息
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
// 获取锁失败
return false;
}
释放锁逻辑
// 释放锁使用release方法,此处首先获取锁{重入次数-1},这里分为三种情况:
// 1.{重入次数-1}>0表示还有重入锁未释放,则直接返回
// 2.{重入次数-1}<0表示重入次数有误,抛出异常
// 3.{重入次数-1}=0表示锁不存在重入可直接释放锁删除子节点并且清除线程与锁的映射信息
public void release() throws Exception
{
// 获取当前线程锁信息
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData == null )
{
// 当前线程没有获得锁则抛出异常
throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
}
// 获取当前线程锁的{重入次数-1}
int newLockCount = lockData.lockCount.decrementAndGet();
// 大于0表示还有重入锁未释放,直接返回
if ( newLockCount > 0 )
{
return;
}
// 小于0表示锁重入次数有误,抛出异常
if ( newLockCount < 0 )
{
throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
}
// 等于0时表示锁无重入,可直接删除子节点释放锁
try
{
// 删除子节点释放锁
internals.releaseLock(lockData.lockPath);
}
finally
{
// 清除线程与锁的映射关系
threadData.remove(currentThread);
}
}
LockInternals
前面说到该分布式锁的主要实现逻辑都在LockInternals中,下面开始介绍LockInternals的内部逻辑。
获取锁
前面在类InterProcessMutex介绍时注意到获取锁主要调用了internals.attemptLock(time, unit, getLockNodeBytes())方法,下面首先来分析该方法。
// 该方法两个核心方法是driver.createsTheLock以及internalLockLoop,driver.createsTheLock用于创建子节点并且返回节点创建成功的全路径,internalLockLoop执行尝试获取锁的逻辑,该方法获取锁成功则返回锁节点全路径。
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
// 获取当前时间
final long startMillis = System.currentTimeMillis();
// 是否设置阻塞超时时间
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
// 本地锁节点数组
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0;
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
while ( !isDone )
{
isDone = true;
try
{
// 创建锁节点,创建成功返回锁节点的全路径
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
// 尝试获取锁,内部阻塞,阻塞超时时间为millisToWait,获取成功返回true,
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e )
{
// 捕获节点找不到异常,这会发生在会话过期的时候,此时使用重试机制
if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
isDone = false;
}
else
{
throw e;
}
}
}
if ( hasTheLock )
{
// 返回锁节点全路径
return ourPath;
}
// 返回null获取锁失败
return null;
}
// 这里的LockInternalsDriver接口由StandardLockInternalsDriver实现,对于driver.createsTheLock方法不做过多的叙述,其内部就是注册一个锁子节点,注册成功则返回锁节点全路径。我们直接来看internalLockLoop的核心代码,其内部主要是逻辑是查看当前子节点是否为最前节点,如果是则获得锁,如果不是则设置对前一节点的监听,然后阻塞等待前一节点释放触发监听,唤醒线程。
while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
{
// 获得按子节点序号大小排好序的所有子节点集合
List<String> children = getSortedChildren();
// 获取当前子节点名称
String sequenceNodeName = ourPath.substring(basePath.length() + 1);
// 比较当前子节点序号是否最前,如果是最前节点则获得锁,且predicateResults的getsTheLock属性为true
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
// 查看predicateResults的getsTheLock属性是否为true,是则具备获得锁条件,反之则不能获得锁。
if ( predicateResults.getsTheLock() )
{
haveTheLock = true;
}
else
{
// 当前节点不为最前节点,此处获取前一节点的路径
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this)
{
try
{
// 监听前一节点,等待前一节点的释放
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
// 查看是否有设置阻塞超时时间
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; // timed out - delete our node
break;
}
// 阻塞线程,等待释放
wait(millisToWait);
}
else
{
// 阻塞线程,等待释放
wait();
}
}
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
}
}
释放锁
删除锁的逻辑也很简单,就是删除当前线程在根节点下的子节点即可
void releaseLock(String lockPath) throws Exception
{
revocable.set(null);
deleteOurPath(lockPath);
}
private void deleteOurPath(String ourPath) throws Exception
{
try
{
client.delete().guaranteed().forPath(ourPath);
}
catch ( KeeperException.NoNodeException e )
{
// ignore - already deleted (possibly expired session, etc.)
}
}
LockInternalsDriver
前面提到LockInternalsDriver主要提供了一些锁操作的方法,相当于一个锁操作的工具类,这里LockInternalsDriver接口由StandardLockInternalsDriver类实现,下面来看看其内部提供了哪些方法。
getsTheLock
/**
* 该方法主要用于查看当前节点是否为最前节点,如果不为最前节点则返回前一节点的路径
*
* @param client zookeeper连接
* @param children 所有子节点根据序号升序排序后的集合
* @param sequenceNodeName 当前子节点名称
* @param maxLeases 最大租赁数,后续用于比对当前节点是否为排在最前的节点
*/
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
// 查看当前子节点的index
int ourIndex = children.indexOf(sequenceNodeName);
// 查看当前节点是否存在
validateOurIndex(sequenceNodeName, ourIndex);
// 比对当前节点是否为最前节点,这里的maxLeases默认是1,如果ourIndex为0则为最前节点
boolean getsTheLock = ourIndex < maxLeases;
// 如果当前节点不为最前节点则获取前一节点设置监听
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
// 返回PredicateResults对象,其中属性getsTheLock表示是否获得锁,pathToWatch表明需要监控的节点路径
// 1.当getsTheLock为true,则pathToWatch为null
// 2.当getsTheLock为true,则pathToWatch为前一节点路径
return new PredicateResults(pathToWatch, getsTheLock);
}
其他方法都比较简单这里直接说明其作用
方法名 | 作用 |
---|---|
createsTheLock() | 创建子节点,创建成功则返回当前节点全路径 |
fixForSorting() | 对当前子节点名称进行substring操作,返回可比较的序号 (如将 _c_c19219f1-760b-497f-905a-176ff211904a-lock-0000000002截取,返回 0000000002) |
validateOurIndex() | 检查传入节点路径是否存在 |
至此,基于Curator的InterProcessMutex实现的分布式排他锁的源码已全部介绍完毕,其使用方法也很简单,在确保数据库ACID的前提下能获得很好的效果。
2. 基于Zookeeper的Java Api实现
下面通过自写一个基于zookeeper实现的分布式锁来来加深对于InterProcessMutex实现的分布式锁的理解,主要介绍一些编写代码时遇到的困难及困惑,通过查看InterProcessMutex原代码是如何实现的来进行代码编写。以下代码为不可重入锁,仅针对核心方法进行复现。
1、问题剖析
基本实现思路及流程和InterProcessMutex内部基本一致,下面根据分布式锁的创建、获取、释放来列出一些需要注意的问题。
锁创建
问题: 首先对于锁创建分为根节点创建以及子节点创建,这时需要考虑到根节点以及子节点的创建类型,需要达到的效果是子节点能手动删除、会话超时删除、失去心跳自动删除。
思考: 一开始按照以上思路,创建永久根节点以及临时有序子节点基本能满足需求,但是此时会有个问题就是当锁资源过多时集群会存在很多之前使用过的根节点,这些根节点不能自动清除
解决方案: 这时去查看了一下源码中节点的创建,发现InterProcessMutex内部根节点的创建使用zookeeper3.5.3版本后新增的容器节点(Container),在根节点无子节点存在的情况下会自动删除
源码:
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
String ourPath;
if ( lockNodeBytes != null )
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
}
else
{
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
return ourPath;
}
自写代码:
// 创建当前锁节点
public void createLock() throws KeeperException, InterruptedException {
// 首先查看是否存在根节点,不存在则创建
Stat stat = zkClient.exists(LOCK_ROOT_PATH, false);
if (stat == null) {
// 这里创建容器节点,当根节点下没有子节点时自动删除
zkClient.create(LOCK_ROOT_PATH, "rootPath".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.CONTAINER);
}
// 创建EPHEMERAL_SEQUENTIAL类型节点
String currentLockPath = zkClient.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME,
pid.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
log.info("【当前线程:" + pid + "】 节点创建: " + currentLockPath);
this.currentLockPath = currentLockPath;
}
锁获取
问题: 对于锁的获取时,如果当前节点不为第一节点时,需要监听前一节点等待释放再唤醒线程,因此需要设置阻塞及唤醒。
// 监听器
private Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
log.info("上一节点:" + event.getPath() + "释放");
synchronized (this) {
notifyAll();
}
}
};
// 获取前一节点路径
String preCurrentLockPath = currentLockPaths.get(index - 1);
Stat stat = zkClient.exists(LOCK_ROOT_PATH + "/" + preCurrentLockPath, watcher);
if (stat != null) { // 阻塞当前进程,直到preCurrentLockPath释放锁,被watcher观察到,notifyAll后,attemptLock
log.info(" 等待前锁释放,preCurrentLockPath:" + preCurrentLockPath);
synchronized (watcher) {
watcher.wait();
}
}
// 假如前一个节点不存在了,比如说执行完毕,或者执行节点掉线,重新获取锁
attemptLock();
思考: 一开始按照以上思路,本地测试五个并发的时候时没有什么问题的,后面通过查看源码发现这里这样写会存在一个情景,当设置完对前一子节点监听后,此时代码还未执行watcher.wait(),此时如果前一节点删除,触发了监听器,那么则会先执行notifyAll()然后再执行watcher.wait()造成死锁。
解决方案: 这时去查看了一下源码中关于线程阻塞的逻辑是怎么实现的,发现InterProcessMutex内部对于线程的阻塞是这样实现的,一是将设置监听器以及wait()方法放在同步块中,二是设置阻塞等待时间,当然这个等待时间是可选的,实际上我们选取任意一种方案都可解决这一问题。
源码:
synchronized(this)
{
try
{
// 设置监听
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
// 可选阻塞超时时间
if ( millisToWait != null )
{
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 )
{
doDelete = true; // timed out - delete our node
break;
}
// 带超时时间阻塞
wait(millisToWait);
}
else
{
// 不带超时时间阻塞
wait();
}
}
catch ( KeeperException.NoNodeException e )
{
// it has been deleted (i.e. lock released). Try to acquire again
}
}
自写代码:
// 获取前一节点
String preCurrentLockPath = currentLockPaths.get(index - 1);
synchronized (watcher) {
// 监控当前节点的上一节点
Stat stat = zkClient.exists(LOCK_ROOT_PATH + "/" + preCurrentLockPath, watcher);
// 假如前一个节点不存在了,比如说执行完毕,或者执行节点掉线,重新获取锁
if (stat != null) { // 阻塞当前进程,直到preCurrentLockPath释放锁,被watcher观察到,notifyAll后,attemptLock
log.info(" 等待前锁释放,preCurrentLockPath:" + preCurrentLockPath);
// 这里如果同步块不包括监听设置,则存在一种情况就是在进入synchronized执行watcher.wait()之前前锁释放了,直接触发了watcher的notifyAll后在执行watcher.wait(),这样会导致死锁
// 解决方案一:将设置监听与等待操作放在同步块中,防止发生死锁,这里使用方案一
// 解决方案二:wait方法设置等待时间,防止发生死锁
watcher.wait();
}
}
attemptLock();
2、样例
**描述:**数据库中设定某商品基本信息(名为外科口罩,数量为10),多进程对该商品进行抢购,当商品数量为0时结束抢购。
创建数据库表
# 创建数据库表
create table `database_lock_2`(
`id` BIGINT NOT NULL AUTO_INCREMENT,
`good_name` VARCHAR(256) NOT NULL DEFAULT "" COMMENT '商品名称',
`good_count` INT NOT NULL COMMENT '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表2';
# 插入原始数据
insert into database_lock_2 (good_name,good_count) values ('医用口罩',10);
代码清单
程序入口
package pers.zifeng.distributed.lock.zookeeper.event;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import pers.zifeng.distributed.lock.common.MysqlService;
import java.lang.management.ManagementFactory;
/**
* @author: zf
* @date: 2021/05/27 10:16:07
* @version: 1.0.0
* @description: 使用curator中的InterProcessMutex实现分布式锁
* 执行流程:多进程向某个容器根节点注册临时有序节点,注册成功后查看当前节点是否为首节点,是则执行业务,否则监听上一节点阻塞,等待上一节点释放再执行业务
*/
@Slf4j
public class InterProcessMutexLock {
private static final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
public void buyMaskWithLock(CuratorFramework zkClient) throws Exception {
while (true) {
// 创建分布式锁,根节点为/curator/locks
InterProcessMutex mutex = new InterProcessMutex(zkClient, "/curator/locks");
log.info("【当前进程:" + pid + "】 创建节点成功,尝试获取锁!");
// 尝试获取锁
mutex.acquire();
log.info("【当前进程:" + pid + "】 获取锁成功!");
// 抢购口罩
if (!MysqlService.buyMask()) {
break;
}
// 锁释放
mutex.release();
log.info("【当前进程:" + pid + "】 锁释放!");
}
}
private static CuratorFramework getZkClient() {
String zkServerAddress = "localhost:2181";
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
CuratorFramework zkClient = CuratorFrameworkFactory.builder()
.connectString(zkServerAddress)
.sessionTimeoutMs(100000)
.connectionTimeoutMs(10000)
.retryPolicy(retryPolicy)
.build();
zkClient.start();
return zkClient;
}
public static void main(String[] args) {
CuratorFramework zkClient = null;
try {
zkClient = getZkClient();
InterProcessMutexLock interProcessMultiLock = new InterProcessMutexLock();
interProcessMultiLock.buyMaskWithLock(zkClient);
} catch (Exception e) {
log.error("抢购口罩失败!", e);
} finally {
if (zkClient != null) {
zkClient.close();
}
MysqlService.close();
}
}
}
分布式锁逻辑类
package pers.zifeng.distributed.lock.zookeeper.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.Collections;
import java.util.List;
zf
/**
* @author: zf
* @date: 2021/05/26 14:06:42
* @version: 1.0.0
* @description: zookeeper分布式锁类
*/
@Slf4j
public class ZookeeperLookService {
// zookeeper连接
private ZooKeeper zkClient;
// 根节点
private static final String LOCK_ROOT_PATH = "/DistributeLocks";
// 当前节点前缀
private static final String LOCK_NODE_NAME = "Lock_";
// 当前节点路径
private String currentLockPath;
// 当前线程id
private static final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
// 监控当前节点的上一节点
private final Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
log.info("上一节点:" + event.getPath() + "释放");
synchronized (this) {
notifyAll();
}
}
};
public ZookeeperLookService() throws IOException {
zkClient = new ZooKeeper("localhost:2181", 10000, event -> {
if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
log.error("当前节点:" + event.getPath() + "连接断开!");
}
});
}
// 创建当前锁节点
public void createLock() throws KeeperException, InterruptedException {
// 首先查看是否存在根节点,不存在则创建
Stat stat = zkClient.exists(LOCK_ROOT_PATH, false);
if (stat == null) {
// 这里创建容器节点,当根节点下没有子节点时自动删除
zkClient.create(LOCK_ROOT_PATH, "rootPath".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.CONTAINER);
}
// 创建EPHEMERAL_SEQUENTIAL类型节点
String currentLockPath = zkClient.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME,
pid.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
log.info("【当前进程:" + pid + "】 节点创建: " + currentLockPath);
this.currentLockPath = currentLockPath;
}
public void attemptLock() throws Exception {
// 获取Lock所有子节点,按照节点序号排序
List<String> currentLockPaths;
currentLockPaths = zkClient.getChildren(LOCK_ROOT_PATH, false);
// 排序
Collections.sort(currentLockPaths);
int index = currentLockPaths.indexOf(currentLockPath.substring(LOCK_ROOT_PATH.length() + 1));
// 如果currentLockPath是序号最小的节点,则获取锁
if (index == 0) {
log.info("【当前进程:" + pid + "】 锁获得, currentLockPath: " + currentLockPath);
} else if (index < 0) {
throw new Exception("【当前进程:" + pid + "】 锁不存在, currentLockPath: " + currentLockPath);
} else {
// currentLockPath不是序号最小的节点,监控前一个节点
String preCurrentLockPath = currentLockPaths.get(index - 1);
synchronized (watcher) {
Stat stat = zkClient.exists(LOCK_ROOT_PATH + "/" + preCurrentLockPath, watcher);
// 假如前一个节点不存在了,比如说执行完毕,或者执行节点掉线,重新获取锁
if (stat != null) { // 阻塞当前进程,直到preCurrentLockPath释放锁,被watcher观察到,notifyAll后,attemptLock
log.info(" 等待前锁释放,preCurrentLockPath:" + preCurrentLockPath);
// 这里如果同步块不包括监听设置,则存在一种情况就是在进入synchronized执行watcher.wait()之前前锁释放了,直接触发了watcher的notifyAll后在执行watcher.wait(),这样会导致死锁
// 解决方案一:将设置监听与等待操作放在同步块中,防止发生死锁,这里使用方案一
// 解决方案二:wait方法设置等待时间,防止发生死锁
watcher.wait();
}
}
attemptLock();
}
}
// 释放锁
public void releaseLock() throws KeeperException, InterruptedException {
zkClient.delete(currentLockPath, -1);
log.info("【当前进程:" + pid + "】 锁释放:" + currentLockPath);
}
// 关闭连接
public void closeConnect() throws InterruptedException {
zkClient.close();
}
}
业务执行类
package pers.zifeng.distributed.lock.common;
import lombok.extern.slf4j.Slf4j;
import pers.zifeng.distributed.lock.utils.JDBCUtils;
import java.lang.management.ManagementFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/**
* @author: zf
* @date: 2021/05/14 16:49:01
* @version: 1.0.0
* @description: 提供mysql服务与数据库交互, 用于reids与zookepper分布式锁的业务操作
*/
@Slf4j
public class MysqlService {
private static final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
private static Connection connection;
private static Statement statement;
private static ResultSet resultset;
static {
try {
connection = JDBCUtils.getConnection();
statement = connection.createStatement();
resultset = null;
} catch (SQLException sqlException) {
log.error("连接数据库失败!", sqlException);
}
}
/**
* 获取资源
*
* @param id 资源id
* @return resultSet
*/
public static ResultSet getGoodCount(int id) throws SQLException {
String sql = "select * from database_lock_2 where id=" + id;
try {
//获取数据库连接
resultset = statement.executeQuery(sql);
return resultset;
} catch (Exception e) {
throw e;
}
}
/**
* 修改资源
*
* @param id 资源id
* @return 修改状态
*/
public static boolean setGoodCount(int id, int goodCount) throws SQLException {
String sql = "update database_lock_2 set good_count = good_count-1 where id=" + id + " and good_count=" + goodCount;
try {
//获取数据库连接
int stat = statement.executeUpdate(sql);
return stat == 1;
} catch (Exception e) {
log.error("修改库存信息失败!,e");
return false;
}
}
/**
* 抢购一个口罩
*
* @return 是否抢购成功
* @throws SQLException sql异常
*/
public static boolean buyMask() throws SQLException {
ResultSet resultSet = null;
try {
int goodCount = 0;
log.info("【当前进程:" + pid + "】 开始购买口罩!");
Thread.sleep(5 * 1000);
resultSet = MysqlService.getGoodCount(1);
while (resultSet.next()) {
goodCount = resultSet.getInt("good_count");
}
if (goodCount <= 0) {
log.info("抢购失败,当前口罩余量为0!");
return false;
} else {
// 此处注意要保证数据库操作原子性,这里加上乐观锁防止数据越界问题
if (MysqlService.setGoodCount(1, goodCount)) {
log.info("【当前进程:" + pid + "】 抢购商品成功,剩余库存为:" + --goodCount);
} else {
log.error("【当前进程:" + pid + "】 抢购商品失败,商品数量已被修改!");
}
}
Thread.sleep(5 * 1000);
return true;
} catch (Exception e) {
log.error("抢购口罩失败!", e);
return false;
} finally {
if (resultSet != null) {
resultSet.close();
}
}
}
public static void close() {
log.info("当前进程:" + ManagementFactory.getRuntimeMXBean().getName().split("@")[0] + ",关闭了数据库连接!");
JDBCUtils.close(resultset, statement, connection);
}
}
这里对于数据库配置以及Connection构建代码省略,有兴趣可以直接查看git中项目。
三个进程执行情况
注意事项:
- 该锁为阻塞锁
- 该demo设计为非可重入锁
- 对数据库操作要保证原子性防止业务假死恢复后造成数据不一致问题
- 根节点的创建使用zookeeper3.5.3版本后的容器节点,保证根节点在无子节点的时候会自动清除
- 设置监听阻塞时要避免出现死锁的情况
3.总结
对于实际的生产中,因为zookeeper本身就是一个分布式协调框架,锁健壮性、易用性较强,锁等待使用阻塞的方式耗用资源较少,因此基于zookeeper实现的分布式锁用到很多,但是使用中需要考虑到多资源下zk集群的压力会较高。