ZooKeeper 实战(五) Curator实现分布式锁

文章目录

ZooKeeper 实战(五) Curator实现分布式锁

1.简介

1.1.分布式锁概念

分布式锁是一种用于实现分布式系统中的同步机制的技术。它允许在多个进程或线程之间实现互斥访问共享资源,以避免并发访问时的数据不一致问题。分布式锁的主要目的是在分布式系统中提供类似于全局锁的效果,以确保在任何时刻只有一个进程或线程可以访问特定的资源。

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

1.2.Curator 分布式锁的实现方式

curator-recipes中实现的锁有五种:

1.3.分布式锁接口

org.apache.curator.framework.recipes.locks.InterProcessLock 该接口为分布式锁的行为规范,定义了获取锁和释放锁方法。

java 复制代码
public interface InterProcessLock
{
    /**
     * 阻塞获取锁(如果未获取到锁,则一直阻塞)
     */
    public void acquire() throws Exception;

    /**
     * 非阻塞获取锁
     *
     * @param time 超时时间
     * @param unit 时间单位
     * @return 如果在超时时间内获取到锁则返回true,否则返回false
     * @throws Exception ZK errors, connection interruptions
     */
    public boolean acquire(long time, TimeUnit unit) throws Exception;

    /**
     * 释放锁。每次获取锁之后必须调用该方法释放锁(acquire和release成对出现)
     */
    public void release() throws Exception;

    /**
     * 判断该线程是否获取到锁
     * @return true/false
     */
    boolean isAcquiredInThisProcess();
}

2.准备工作

首先,需要读者掌握 Ideal 同一项目启动多个Service的能力,详细教程可参考博主另一篇博客,博主创建了两个启动实例,一个端口号为8888,另一个9981,此处随意只要不与其他服务的端口号冲突即可。

另外,在启动项目之前,请根据先前所写的教程启动zookeeper的单机服务器 ,参考ZooKeeper 实战(一) 超详细的单机与集群部署教程,并创建一个存储数据节点:路径/ahao/data,数据内容40。(可参照如下操作,由于博主已经创建过,所以重新设置了一遍)

3.分布式可重入锁

可重入锁是一种自我保护的锁,允许同一进程或线程多次获得相同的锁,而不会造成死锁。

3.1.锁对象

类路径:org.apache.curator.framework.recipes.locks.InterProcessMutex

公开构造方法如下:

java 复制代码
     /**
     * @param client 当前客户端实例
     * @param path   锁节点路径
     */
    public InterProcessMutex(CuratorFramework client, String path)
      
     /**
     * @param client 当前客户端实例
     * @param path   锁节点路径
     * @param driver 锁驱动实例(工具类,只要提供两个方法:#createsTheLock 创建锁节点,#getsTheLock 获取当前锁)
     */
    public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)

3.2.非重入式抢占锁

由于非重入式抢占锁的场景对于5种分布式锁实现方式均适用,并且测试场景均一样,所以本节介绍完非重入式抢占锁的测试场景之后,对应的另4种实现方式的该测试场景将不在赘述。

测试场景:有两台服务实例 C1,C2。C1和C2个有两个线程(线程池调度)并发执行,对同一共享资源(/ahao/data中的数据)进行减1 操作。

测试代码
java 复制代码
/**
 * @Name: CuratorDemoApplication
 * @Description:
 * @Author: ahao
 * @Date: 2024/1/10 3:29 PM
 */
@Slf4j
@SpringBootApplication
public class CuratorDemoApplication implements ApplicationRunner{

    @Autowired
    private CuratorFramework client;

    public static void main(String[] args) {
        SpringApplication.run(CuratorDemoApplication.class,args);
    }

  	// 存储数据节点的路径
    final String dataPath = "/ahao/data";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("。。。。。。。。。。。。。容器初始化完毕。。。。。。。。。。。。。。");
				// 锁节点路径
        String lockPath = "/ahao/lock";
        TimeUnit.SECONDS.sleep(3);

        // 创建可重入锁
        InterProcessMutex mutex = new InterProcessMutex(client,lockPath);
        // 创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
      
      	// 保证并发执行,当前时间的秒针部分为0则结束循环
        int seconds = LocalDateTime.now().getSecond();
        while (seconds != 0){
            seconds = LocalDateTime.now().getSecond();
        }
      
        // 提交两个任务
        for (int i = 0; i < 2; i++) {
            executorService.submit(() -> {
                while (share(mutex)) {
                    try {
                        // 睡眠0.5秒
                        TimeUnit.MILLISECONDS.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }

    }

    /**
     * 用来模拟临界资源的方法
     */
    public boolean share(final InterProcessMutex mutex){
        boolean b = true;
        try {
            // 获取锁
            if (mutex.acquire(3, TimeUnit.SECONDS)) {
                // 获取数据节点中的值
                byte[] bytes = client.getData().forPath(dataPath);
                String s = new String(bytes);
                Integer integer = Integer.valueOf(s);
                if(integer > 0){
                    // 设置新值
                    client.setData().forPath(dataPath,String.valueOf(integer-1).getBytes(StandardCharsets.UTF_8));
                    log.info("当前值:{}",integer);
                    b = true;
                }else {
                    log.info("任务已完成。。。。");
                    b = false;
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
              	// 释放锁
                mutex.release();
                return b;
            } catch (Exception e) {
                log.error("释放锁失败");
                throw new RuntimeException(e);
            }
        }
    }
}
输出日志

从下方日志可见,没有出现重复的数值,保证了分布式系统中实现互斥访问共享资源,避免并发访问时的数据不一致问题。

实例c1:

sh 复制代码
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:40
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:39
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:36
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:35
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:32
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:31
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:28
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:27
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:24
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:23
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:20
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:19
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:16
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:15
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:12
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:11
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:8
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:7
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:4
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:3
2024-01-15 INFO 65348 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。
2024-01-15 INFO 65348 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。

实例c2:

sh 复制代码
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:38
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:37
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:34
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:33
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:30
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:29
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:26
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:25
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:22
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:21
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:18
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:17
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:14
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:13
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:10
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:9
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:6
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:5
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 当前值:2
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 当前值:1
2024-01-15 INFO 65387 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。
2024-01-15 INFO 65387 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。

3.3.重入式抢占锁

测试场景:有两台服务实例 C1,C2。C1和C2个有两个线程(线程池调度)并发执行,对同一共享资源(/ahao/data中的数据)进行连续两次的减1 操作,并且第一次操作成功后才能进行第二次操作。

测试代码

具体的减1操作抽离成方法,以便接下来的测试。

java 复制代码
		@Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("。。。。。。。。。。。。。容器初始化完毕。。。。。。。。。。。。。。");

        String lockPath = "/ahao/lock";
        TimeUnit.SECONDS.sleep(3);

        // 创建可重入锁
        InterProcessMutex mutex = new InterProcessMutex(client,lockPath);
        // 创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 保证并发执行,当前时间的秒针部分为0则结束循环
        int seconds = LocalDateTime.now().getSecond();
        while (seconds != 0){
            seconds = LocalDateTime.now().getSecond();
        }

        // 提交两个任务
        for (int i = 0; i < 2; i++) {
            executorService.submit(() -> {
                // 循环执行
                while (share(mutex,1)) {
                    try {
                        // 睡眠0.5秒
                        TimeUnit.MILLISECONDS.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }

    }

    /**
     * 用来模拟临界资源的方法
     */
    public boolean share(final InterProcessLock mutex, int n){
        boolean b = true;
        try {
            // 获取锁
            if (mutex.acquire(3, TimeUnit.SECONDS)) {
                // 减1操作
                b = doLock(n);
                // 最多执行三次
                if (b && n < 3){
                    b = share(mutex,n+1);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                mutex.release();
                return b;
            } catch (Exception e) {
                log.error("释放锁失败");
                throw new RuntimeException(e);
            }
        }
    }

    // 减1操作
    private boolean doLock(int n) throws Exception {
        // 获取数据节点中的值
        byte[] bytes = client.getData().forPath(dataPath);
        String s = new String(bytes);
        Integer integer = Integer.valueOf(s);
        // 判断是否为0
        if(integer > 0){
            // 设置新值
            client.setData().forPath(dataPath,String.valueOf(integer-1).getBytes(StandardCharsets.UTF_8));
            log.info("第{}次加锁当前值:{}",n,integer);
            return true;
        }else {
            log.info("任务已完成。。。。");
            return false;
        }
    }
输出日志

由此可见,通过日志可以发现,每次获取到锁的线程都是连续执行三次并且重复获取锁并释放。

实例c1:

sh 复制代码
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:34
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:33
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:32
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:31
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:30
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:29
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:22
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:21
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:20
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:19
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:18
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:17
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:10
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:9
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:8
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:7
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:6
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:5
2024-01-16 INFO 91120 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。
2024-01-16 INFO 91120 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。

实例c2:

sh 复制代码
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:40
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:39
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:38
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:37
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:36
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:35
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:28
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:27
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:26
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:25
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:24
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:23
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:16
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:15
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:14
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:13
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:12
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:11
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:4
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第2次加锁当前值:3
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 第3次加锁当前值:2
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 第1次加锁当前值:1
2024-01-16 INFO 91115 --- [pool-3-thread-2] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。
2024-01-16 INFO 91115 --- [pool-3-thread-1] com.ahao.demo.CuratorDemoApplication     : 任务已完成。。。。

4.分布式非可重入锁

org.apache.curator.framework.recipes.locks.InterProcessMutex 类似,只是不可重入。

4.1.锁对象

类路径:InterProcessSemaphoreMutex

公开构造方法如下:

java 复制代码
 		/**
     * @param client 当前客户端实例
     * @param path   锁节点路径
     */		
		public InterProcessSemaphoreMutex(CuratorFramework client, String path);

4.2.重入式抢占锁

测试场景:有一台服务实例 C1。C1中的main线程进行连续两次的加锁操作。

测试代码
java 复制代码
/**
 * @Name: CuratorDemoApplication
 * @Description:
 * @Author: ahao
 * @Date: 2024/1/10 3:29 PM
 */
@Slf4j
@SpringBootApplication
public class CuratorDemoApplication implements ApplicationRunner{

    @Autowired
    private CuratorFramework client;

    public static void main(String[] args) {
        SpringApplication.run(CuratorDemoApplication.class,args);
    }

    String dataPath = "/ahao/data";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("。。。。。。。。。。。。。容器初始化完毕。。。。。。。。。。。。。。");

        String lockPath = "/ahao/lock";
        TimeUnit.SECONDS.sleep(3);

        // 创建非可重入锁
        InterProcessSemaphoreMutex mutex = new InterProcessSemaphoreMutex(client, lockPath);
        // 调用测试方法
        share(mutex);

    }


    /**
     * 用来模拟临界资源的方法
     */
    public void share(final InterProcessLock mutex){
        try {
            // 获取锁
            if (mutex.acquire(5, TimeUnit.SECONDS)) {
                log.info("第一次加锁成功");
            }
            // 再次获取锁
            if (!mutex.acquire(5, TimeUnit.SECONDS)) {
                log.info("第二次加锁失败了");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                // 获取多少次锁就要释放多少次
                mutex.release();
                mutex.release();
            } catch (Exception e) {
                log.error("释放锁失败:{}",e.getStackTrace());
                throw new RuntimeException(e);
            }
        }
    }

}
输出日志

本次测试仅需要启动一个实例c1即可,用于测试分布式非可重入锁多次加锁的场景。根据以下输出日志可知,第一次加锁成功,在第二次加锁时超时失败了,导致之后在第二次释放锁时出现异常。

sh 复制代码
2024-01-16 INFO 45025 --- [           main] com.ahao.demo.CuratorDemoApplication     : 第一次加锁成功
2024-01-16 INFO 45025 --- [           main] com.ahao.demo.CuratorDemoApplication     : 第二次加锁失败了
2024-01-16 ERROR 45025 --- [           main] com.ahao.demo.CuratorDemoApplication     : 释放锁失败:org.apache.curator.shaded.com.google.common.base.Preconditions.checkState(Preconditions.java:444)
2024-01-16 INFO 45025 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2024-01-16 ERROR 45025 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:762)
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:749)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292)
	at com.ahao.demo.CuratorDemoApplication.main(CuratorDemoApplication.java:41)
Caused by: java.lang.RuntimeException: java.lang.IllegalStateException: Not acquired
	at com.ahao.demo.CuratorDemoApplication.share(CuratorDemoApplication.java:83)
	at com.ahao.demo.CuratorDemoApplication.run(CuratorDemoApplication.java:56)
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:759)
	... 5 common frames omitted
Caused by: java.lang.IllegalStateException: Not acquired
	at org.apache.curator.shaded.com.google.common.base.Preconditions.checkState(Preconditions.java:444)
	at org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex.release(InterProcessSemaphoreMutex.java:68)
	at com.ahao.demo.CuratorDemoApplication.share(CuratorDemoApplication.java:80)
	... 7 common frames omitted

5.分布式可重入读写锁

读写锁顾名思义,包含两把锁:读锁和写锁。当写锁未生效(未被获取)时,读锁能够被多个线程获取使用。但是写锁只能被一个线程获取持有。 只有当写锁释放时,读锁才能被持有。可重入表示一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁,读锁可以重入读锁。 这也意味着写锁可以降级成读锁, 比如请求写锁 --->读锁 ---->释放写锁。 从读锁升级成写锁是不行的。可重入读写锁是"公平的",每个实例将按请求的顺序获取锁。

5.1.锁对象

类路径:org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock

公开构造方法如下:

java 复制代码
    
		/**
     * @param client 当前客户端实例
     * @param path   锁节点路径
     */		
		public InterProcessReadWriteLock(CuratorFramework client, String basePath)

  	/**
    *  @param client 当前客户端实例
    * @param path   锁节点路径
    * @param lockData 存储在锁节点的数据内容
    */
    public InterProcessReadWriteLock(CuratorFramework client, String basePath, byte[] lockData)

5.2.读锁和写锁的竞争

测试场景:有两台服务实例 C1,C2。C1和C2个有3个线程(2个读线程和1个写线程)并发执行,其中写线程进行加1操作并重复执行5次,每次都加写锁,而读线程进行查询操作并重复执行10次每次都加读锁。

测试代码
java 复制代码
@Slf4j
@SpringBootApplication
public class CuratorDemoApplication implements ApplicationRunner {

    @Autowired
    private CuratorFramework client;

    public static void main(String[] args) {
        SpringApplication.run(CuratorDemoApplication.class, args);
    }

    String dataPath = "/ahao/data";

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("。。。。。。。。。。。。。容器初始化完毕。。。。。。。。。。。。。。");

        String lockPath = "/ahao/lock";
        TimeUnit.SECONDS.sleep(3);

        InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, lockPath);
        // 获取读锁
        InterProcessMutex readLock = readWriteLock.readLock();
        // 获取写锁
        InterProcessMutex writeLock = readWriteLock.writeLock();

        // 保证并发执行,当前时间的秒针部分为30的整数倍则结束循环
        int seconds = LocalDateTime.now().getSecond();
        while (seconds/30 != 0){
            seconds = LocalDateTime.now().getSecond();
        }

        for (int j = 0; j < 2; j++) {
            // 读线程
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        // 加锁
                        if (readLock.acquire(3, TimeUnit.SECONDS)) {
                            doLock(false);
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        // 释放锁
                        try {
                            readLock.release();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }, "读线程"+j).start();
        }

        // 写线程
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    // 加锁
                    if (writeLock.acquire(3, TimeUnit.SECONDS)) {
                        doLock(true);
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    try {
                        writeLock.release();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }

            }
        }, "写线程").start();

    }

    /**
     * 加操作
     * @param isAdd true 表示加1,false 表示不加,查询数据
     * @return
     * @throws Exception
     */
    public void doLock(boolean isAdd) throws Exception {
        // 获取数据节点中的值
        byte[] bytes = client.getData().forPath(dataPath);
        Integer integer = Integer.valueOf(new String(bytes));
        if (isAdd) {
            // 设置新值
            client.setData().forPath(dataPath, String.valueOf(integer + 1).getBytes(StandardCharsets.UTF_8));
            log.info("加1操作后:{}", integer);
        } else {
            log.info("查询数据:{}", integer);
        }
    }   
}
输出日志

可以观察到,读线程所查询的数据存在重复数据,说明了在同一时刻可以加多个读锁,而写线程不会出现重复数据,只能有一个线程可以获取到写锁。

实例c1:

sh 复制代码
2024-01-16  INFO 64462 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:40
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:41
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:42
2024-01-16  INFO 64462 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:42
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:43
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:44
2024-01-16  INFO 64462 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:44
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:45
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:46
2024-01-16  INFO 64462 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:46
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:47
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:48
2024-01-16  INFO 64462 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:48
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:49
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64462 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50

实例c2:

sh 复制代码
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:40
2024-01-16  INFO 64453 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:41
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:42
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:42
2024-01-16  INFO 64453 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:43
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:44
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:44
2024-01-16  INFO 64453 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:45
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:46
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:46
2024-01-16  INFO 64453 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:47
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:48
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:48
2024-01-16  INFO 64453 --- [  写线程] com.ahao.demo.CuratorDemoApplication     : 加1操作后:49
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程1] com.ahao.demo.CuratorDemoApplication     : 查询数据:50
2024-01-16  INFO 64453 --- [ 读线程0] com.ahao.demo.CuratorDemoApplication     : 查询数据:50

6.共享信号量

对 JUC 熟悉的读者应该了解Semaphore(信号量)。Semaphore是一种用于实现同步的对象,通常用于限制对共享资源的并发访问。Semaphore可以用来控制进入临界区的线程数量,通过使用Semaphore,可以防止过多线程同时访问共享资源,从而避免出现资源竞争和死锁等问题。

而Curator中的Semaphore和JUC中的Semaphore,如出一辙,只是一个应用于分布式场景,一个应用于进程(服务器)内部。

6.1.锁对象

类路径:org.apache.curator.framework.recipes.locks.InterProcessSemaphoreV2

公开构造方法如下:

java 复制代码
    /**
     * @param client 当前客户端实例
     * @param path   锁节点路径
     * @param maxLeases 允许线程进入临界区的最大数量(许可证数量)
     */
    public InterProcessSemaphoreV2(CuratorFramework client, String path, int maxLeases);
    /**
     * @param client 当前客户端实例
     * @param path   锁节点路径
     * @param count  用于监听许可证数量
     */
    public InterProcessSemaphoreV2(CuratorFramework client, String path, SharedCountReader count);

6.2.信号量抢占

测试场景:有两台服务实例 C1,C2。C1和C2个有3个线程并发执行。但是有2个许可证,每个线程一次只能获取一个许可证,获取成功则执行4s耗时操作(睡眠4s),然后释放锁。

测试代码
java 复制代码
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("。。。。。。。。。。。。。容器初始化完毕。。。。。。。。。。。。。。");
        String lockPath = "/ahao/lock";
        TimeUnit.SECONDS.sleep(3);

        // 创建共享信号量
        InterProcessSemaphoreV2 semaphoreV2 = new InterProcessSemaphoreV2(client,lockPath,2);

        // 保证并发执行,当前时间的秒针部分为30的整数倍则结束循环
        int seconds = LocalDateTime.now().getSecond();
        while (seconds/30 != 0){
            seconds = LocalDateTime.now().getSecond();
        }

        // 创建线程争夺许可证
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                Lease acquire = null;
                try {
                    acquire = semaphoreV2.acquire(5, TimeUnit.SECONDS);
                    if (acquire != null){
                        log.info("抢到许可证,参与竞争的节点:{}",semaphoreV2.getParticipantNodes());
                        // 睡眠4秒
                        TimeUnit.SECONDS.sleep(4);
                    }else {
                        log.info("抢到许可证失败");
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    if (acquire != null){
                        semaphoreV2.returnLease(acquire);
                    }
                }
            }, "线程"+i).start();
        }
        
    }
输出日志

实例c1:

sh 复制代码
2024-01-16  INFO 4319 --- [ 线程2] com.ahao.demo.CuratorDemoApplication     : 抢到许可证
2024-01-16  INFO 4319 --- [ 线程1] com.ahao.demo.CuratorDemoApplication     : 抢到许可证
2024-01-16  INFO 4319 --- [ 线程0] com.ahao.demo.CuratorDemoApplication     : 抢到许可证失败

实例c2:

sh 复制代码
2024-01-16  INFO 4307 --- [ 线程0] com.ahao.demo.CuratorDemoApplication     : 抢到许可证
2024-01-16  INFO 4307 --- [ 线程2] com.ahao.demo.CuratorDemoApplication     : 抢到许可证
2024-01-16  INFO 4307 --- [ 线程1] com.ahao.demo.CuratorDemoApplication     : 抢到许可证失败

7.多共享锁

表示将多个锁合并为一个锁。在获取多共享锁时,必须获取其内部所有的锁,才算获取成功,否则释放所有已获取的锁。同样调用释放锁方法时,会释放所有的锁。

7.1.锁对象

类路径:org.apache.curator.framework.recipes.locks.InterProcessMultiLock

公开构造方法如下:

java 复制代码
		/**
     * @param client 当前客户端实例
     * @param path   多个锁节点路径
     */
		public InterProcessMultiLock(CuratorFramework client, List<String> paths);
  
		/**
     * @param locks 多个锁对象
     */
		public InterProcessMultiLock(List<InterProcessLock> locks);

7.2.获取共享锁

测试场景:有一台服务实例 C1,启动3个线程并发执行,抢占同一个共享锁。

测试代码
java 复制代码
@Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("。。。。。。。。。。。。。容器初始化完毕。。。。。。。。。。。。。。");
        String lockPath = "/ahao/lock";
        String lockPath2 = "/ahao/lock2";
        TimeUnit.SECONDS.sleep(3);

      	// 创建锁1
        InterProcessMutex mutex = new InterProcessMutex(client,lockPath);
      	// 创建锁2
        InterProcessMutex mutex2 = new InterProcessMutex(client,lockPath2);
        // 创建共享锁
        InterProcessMultiLock multiLock = new InterProcessMultiLock(List.of(mutex,mutex2));

        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                try {
                    if (multiLock.acquire(5, TimeUnit.SECONDS)) {
                        log.info("获取到锁");
                        TimeUnit.SECONDS.sleep(3);
                    }else {
                        log.info("获取失败");
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    try {
                        multiLock.release();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            },"线程"+i).start();
        }

    }
输出日志

可见在第3个线程获取锁时,由于没有获取到/ahao/lock2对应的锁对象导致的超时。

sh 复制代码
2024-01-16  INFO 14352 --- [ 线程1] com.ahao.demo.CuratorDemoApplication     : 获取到锁
2024-01-16  INFO 14352 --- [ 线程2] com.ahao.demo.CuratorDemoApplication     : 获取到锁
2024-01-16  INFO 14352 --- [ 线程0] com.ahao.demo.CuratorDemoApplication     : 获取失败
Exception in thread "线程0" java.lang.RuntimeException: java.lang.Exception: java.lang.IllegalMonitorStateException: You do not own the lock: /ahao/lock2
	at com.ahao.demo.CuratorDemoApplication.lambda$run$0(CuratorDemoApplication.java:70)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.Exception: java.lang.IllegalMonitorStateException: You do not own the lock: /ahao/lock2
	at org.apache.curator.framework.recipes.locks.InterProcessMultiLock.release(InterProcessMultiLock.java:169)
	at com.ahao.demo.CuratorDemoApplication.lambda$run$0(CuratorDemoApplication.java:68)
	... 1 more
Caused by: java.lang.IllegalMonitorStateException: You do not own the lock: /ahao/lock2
	at org.apache.curator.framework.recipes.locks.InterProcessMutex.release(InterProcessMutex.java:140)
	at org.apache.curator.framework.recipes.locks.InterProcessMultiLock.release(InterProcessMultiLock.java:158)
	... 2 more
相关推荐
阿里云云原生9 小时前
LLM 不断提升智能下限,MCP 不断提升创意上限
云原生
阿里云云原生9 小时前
GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩
云原生
数据智能老司机12 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机12 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
云上艺旅13 小时前
K8S学习之基础七十四:部署在线书店bookinfo
学习·云原生·容器·kubernetes
数据智能老司机13 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
IT成长日记13 小时前
【Kafka基础】Kafka工作原理解析
分布式·kafka
州周15 小时前
kafka副本同步时HW和LEO
分布式·kafka
爱的叹息17 小时前
主流数据库的存储引擎/存储机制的详细对比分析,涵盖关系型数据库、NoSQL数据库和分布式数据库
数据库·分布式·nosql
千层冷面17 小时前
RabbitMQ 发送者确认机制详解
分布式·rabbitmq·ruby