当前位置: 首页 > news >正文

第五节 zookeeper集群与分布式锁_2

1.分布式锁概述

1.1 什么是分布式锁

1)要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁。

  • 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。

    image.png

    线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。

  • 分布式锁:分布式锁,即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题,而分布式锁,就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是,分布式系统中竞争共享资源的最小粒度从线程升级成了进程。

    分布式锁是在分布式或者集群环境下, 多进程可见,并且互斥的锁。

2)分布式锁介绍

  • 传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制,但是原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效。提出分布式锁的概念,是为了解决跨机器的互斥机制来控制共享资源的访问。

    image.png

  • 分布式场景下解决并发问题,需要应用分布式锁技术。如上图所示,分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。

1.2 分布式锁的设计原则

Redis官网上对使用分布式锁提出至少需要满足如下三个要求:

  • 互斥(属于安全性): 在任何给定时刻,只有一个客户端可以持有锁。

  • 无死锁(属于有效性): 即如果一个线程已经持有了锁,那么它可以多次获取该锁而不会发生死锁。

  • 容错性(属于有效性): 如果一个线程获取了锁,那么即使崩溃或者失去连接,锁也必须被释放。

除此之外,分布式锁的设计中还可以需要考虑:

  • 加锁解锁的同源性:A加的锁,不能被B解锁。

  • 获取锁非阻塞: 如果获取不到锁,不能无限期等待(在某个服务来获取锁时,假设该锁已经被另一个服务获取,我们要能直接返回失败,不能一直等待。)。

  • 锁失效机制:假设某个应用获取到锁之后,一直没有来释放锁,可能服务本身已经挂掉了,不能一直不释放,导致其他服务一直获取不到锁。

  • 高性能: 加锁解锁是高性能的,加锁时间一般是几毫秒。(我们这个分布式锁,可能会有很多的服务器来获取,所以加锁解锁一定是需要高能的)。

  • 高可用: 为了避免单点故障,锁需要有一定的容错方式。例如锁服务本身就是一个集群的形式。

1.3 分布式锁的实现方式

分布式锁的使用流程: 加锁 -----》 执行业务逻辑 ----》释放锁

  • 基于数据库实现分布式锁

  • 基于 redis 实现分布式锁

  • 基于 zookeeper实现分布式锁

2.基于mysql实现分布式锁

基于Mysql实现分布式锁,适用于对性能要求不高,并且不希望因为要使用分布式锁而引入新组件

可以利用唯一键索引不能重复插入的特点实现。

2.1 基于唯一索引实现

2.1.1 实现思路

  1. 创建锁表,内部存在字段表示资源名及资源描述,同一资源名使用数据库唯一性限制。

  2. 多个进程同时往数据库锁表中写入对某个资源的占有记录,当某个进程成功写入时则表示其获取锁成功

  3. 其他进程由于资源字段唯一性限制插入失败陷入自旋并且失败重试。

  4. 当执行完业务后持有该锁的进程则删除该表内的记录,此时回到步骤一。

image.png

2.1.2 创建数据库以及表

  1. 在mysql下创建数据库,名为: distribute_lock (这里使用navicat创建)

image.png

  1. 多个进程同时往表中插入记录(锁资源为1,描述为测试锁),插入成功则执行流程,执行完流程后删除其在数据库表中的记录。

create table `database_lock`(`id` BIGINT NOT NULL AUTO_INCREMENT,`resource` INT NOT NULL COMMENT '锁资源',`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',PRIMARY KEY (`id`),UNIQUE KEY `resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

2.1.3 创建maven工程

  1. 创建maven工程,distribute-lock,引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion>
​<groupId>com.mashibing</groupId><artifactId>zk-client1</artifactId><version>1.0-SNAPSHOT</version>
​<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.10</version><scope>test</scope></dependency>
​<!--curator--><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>4.0.0</version></dependency>
​<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>4.0.0</version></dependency><!--日志--><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.21</version></dependency>
​<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.21</version></dependency>
​<!-- zookeeper --><dependency><groupId>com.101tec</groupId><artifactId>zkclient</artifactId><version>0.10</version></dependency>
​<!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.6</version></dependency>
​<!-- jdbc --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.48</version></dependency>
​</dependencies>
​<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><source>1.8</source><target>1.8</target></configuration></plugin></plugins></build>
</project>
  1. 添加数据库配置文件 db.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/distribute_lock?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
user=root
password=123456
  1. 创建包结构

基础包结构: com.mashibing.lock , 在lock包下创建 mysql

com.mashibing.lock.mysql
  1. 导入工具类

  • PropertiesReader

@Slf4j
public class PropertiesReader {
​// Properties缓存文件private static final Map<String, Properties> propertiesCache = new HashMap<String, Properties>();
​public static Properties getProperties(String propertiesName) throws IOException {if (propertiesCache.containsKey(propertiesName)) {return propertiesCache.get(propertiesName);}loadProperties(propertiesName);return propertiesCache.get(propertiesName);}
​private synchronized static void loadProperties(String propertiesName) throws IOException {FileReader fileReader = null;
​try {// 创建Properties集合类Properties pro = new Properties();// 获取src路径下的文件--->ClassLoader类加载器ClassLoader classLoader = PropertiesReader.class.getClassLoader();URL resource = classLoader.getResource(propertiesName);// 获取配置路径String path = resource.getPath();// 读取文件fileReader = new FileReader(path);// 加载文件pro.load(fileReader);// 初始化propertiesCache.put(propertiesName, pro);} catch (IOException e) {log.error("读取Properties文件失败,Properties名为:" + propertiesName);throw e;} finally {try {if (fileReader != null) {fileReader.close();}} catch (IOException e) {log.error("fileReader关闭失败!", e);}}}
}
  • JDBCUtils

@Slf4j
public class JDBCUtils {
​private static String url;private static String user;private static String password;
​static {//读取文件,获取值try {Properties properties = PropertiesReader.getProperties("db.properties");url = properties.getProperty("url");user = properties.getProperty("user");password = properties.getProperty("password");String driver = properties.getProperty("driver");//4.注册驱动Class.forName(driver);} catch (IOException | ClassNotFoundException e) {log.error("初始化jdbc连接失败!", e);}}
​/*** 获取连接** @return 连接对象*/public static Connection getConnection() throws SQLException {return DriverManager.getConnection(url, user, password);}
​/*** 释放资源** @param rs* @param st* @param conn*/public static void close(ResultSet rs, Statement st, Connection conn) {if (rs != null) {try {rs.close();} catch (SQLException e) {e.printStackTrace();}}if (st != null) {try {st.close();} catch (SQLException e) {e.printStackTrace();}}if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}
}

2.1.4 数据库操作类

  • com.mashibing.lock.mysql.service

/*** MySQL 锁操作类(加锁+释放锁)*/
@Slf4j
public class MySQLDistributedLockService {
​private static Connection connection;private static Statement statement;private static ResultSet resultSet;
​static{try {connection = JDBCUtils.getConnection();statement = connection.createStatement();resultSet = null;} catch (SQLException e) {log.error("数据库连接失败!");}}
​/*** 锁表 - 获取锁* @param resource      资源* @param description   锁描述* @return  是否操作成功*/public static boolean tryLock(int resource,String description){
​String sql = "insert into database_lock (resource,description) values (" + resource + ", '" + description + "');";
​//获取数据库连接try {int stat = statement.executeUpdate(sql);return stat == 1;} catch (SQLException e) {return false;}}
​/*** 锁表-释放锁* @return*/public static boolean releaseLock(int resource) throws SQLException {String sql = "delete from database_lock where resource = " + resource;//获取数据库连接int stat = statement.executeUpdate(sql);return stat == 1;}
​/*** 关闭连接*/public static void close(){log.info("当前线程: " + ManagementFactory.getRuntimeMXBean().getName().split("@")[0] +",关闭了数据库连接!");JDBCUtils.close(resultSet,statement,connection);}
}

2.1.5 创建LockTable

/*** mysql分布式锁*      执行流程: 多进程抢占数据库某个资源,然后执行业务,执行完释放资源*      锁机制: 单一进程获取锁时,则其他进程提交失败*/
@Slf4j
public class LockTable extends Thread {
​@Overridepublic void run() {super.run();
​//获取Java虚拟机的进程IDString pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];try{while(true){log.info("当前进程PID:" + pid + ",尝试获取锁资源!");if(MySQLDistributedLockService.tryLock(1,"测试锁")){log.info("当前进程PID:" + pid + ",获取锁资源成功!");
​//sleep模拟业务处理过程log.info("开始处理业务!");Thread.sleep(10*1000);log.info("业务处理完成!");
​MySQLDistributedLockService.releaseLock(1);log.info("当前进程PID: " + pid + ",释放了锁资源!");break;}else{log.info("当前进程PID: " + pid + ",获取锁资源失败!");Thread.sleep(2000);}}}catch (Exception e){log.error("抢占锁发生错误!",e);}finally {MySQLDistributedLockService.close();}}
​// 程序入口public static void main(String[] args) {
​new LockTable().start();}
}

2.1.6 分布式锁测试

  • 运行时开启并行执行选项,每次运行三个或三个以上进程. Allow parallel run 运行并行执行

image.png

  • 三个进程的执行情况

image.png

注意事项:

  • 该锁为非阻塞的

  • 当某进程持有锁并且挂死时候会造成资源一直不释放的情况,造成死锁,因此需要维护一个定时清理任务去清理持有过久的锁

  • 要注意数据库的单点问题,最好设置备库,进一步提高可靠性

  • 该锁为非可重入锁,如果要设置成可重入锁需要添加数据库字段记录持有该锁的设备信息以及加锁次数

2.2 基于乐观锁

2.2.1 需求分析

需求: 数据库中设定某商品基本信息(名为外科口罩,数量为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);

2.2.2 实现思路

  • 每次执行业务前首先进行数据库查询,查询当前的需要修改的资源值(或版本号)。

  • 进行资源的修改操作,并且修改前进行资源(或版本号)的比对操作,比较此时数据库中的值是否和上一步查询结果相同。

  • 查询结果相同则修改对应资源值,不同则回到第一步。

image.png

2.2.3 代码实现

  1. MySQLDistributedLockService 中,添加对乐观锁的操作

    /*** 乐观锁-获取资源* @param id 资源ID* @return result*/public static ResultSet getGoodCount(int id) throws SQLException {String sql = "select  * from  database_lock_2 where id = " + id;//查询数据resultSet = statement.executeQuery(sql);return resultSet;}/*** 乐观锁-修改资源* @param id        资源ID* @param goodCount 资源* @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;int stat = statement.executeUpdate(sql);return stat == 1;}/*** 乐观锁-开启事务自动提交*/public static void AutoCommit(){try {connection.setAutoCommit(true);} catch (SQLException e) {log.error("开启自动提交!",e);}}
  1. 创建OptimisticLock,模拟并发操作分布式锁

/*** mysql分布式锁-乐观锁*  执行流程: 多进程抢购同一商品,每次抢购成功商品数量-1,商品数据量为0时退出*  锁机制: 单一进程获取锁时,则其他进程提交失败*/
@Slf4j
public class OptimisticLock extends Thread{@Overridepublic void run() {super.run();String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];ResultSet resultSet = null;String goodName = null;int goodCount = 0;try {while(true){log.info("当前线程:" + pid + ",开始抢购商品!");//获取当前商品信息resultSet = MySQLDistributedLockService.getGoodCount(1);while (resultSet.next()){goodName = resultSet.getString("good_name");goodCount = resultSet.getInt("good_count");}log.info("获取库存成功,当前商品名为:" + goodName + ",当前库存剩余量为:" + goodCount);//模拟执行业务操作Thread.sleep(2*3000);if(0 == goodCount){log.info("抢购失败,当前库存为0! ");break;}//修改库存信息,库存量-1if(MySQLDistributedLockService.setGoodCount(1,goodCount)){log.info("当前线程:" + pid + " 抢购商品:" + goodName + "成功,剩余库存为:" + (goodCount -1));//模拟延迟,防止锁每次被同一进程获取Thread.sleep(2 * 1000);}else{log.error("抢购商品:" + goodName +"失败,商品数量已被修改");}}}catch (Exception e){log.error("抢购商品发生错误!",e);}finally {if(resultSet != null){try {resultSet.close();} catch (SQLException e) {e.printStackTrace();log.error("关闭Result失败!" , e);}}MySQLDistributedLockService.close();}}public static void main(String[] args) {new OptimisticLock().start();}
}

2.3.4 代码测试

开启三个进程,查看执行情况

  • 9 7 4 1

image.png

  • 8 5 2

image.png

  • 6 3

image.png

注意事项:

  • 该锁为非阻塞的

  • 该锁对于业务具有侵入式,如果设置版本号校验则增加的额外的字段,增加了数据库冗余

  • 当并发量过高时会有大量请求访问数据库的某行记录,对数据库造成很大的写压力

  • 因此乐观锁适用于并发量不高,并且写操作不频繁的场景

2.3 基于悲观锁

2.3.1 实现思路

  • 关闭jdbc连接自动commit属性

  • 每次执行业务前先使用查询语句后接for update表示锁定该行数据(注意查询条件如果未命中主键或索引,此时将会从行锁变为表锁)

  • 执行业务流程修改表资源

  • 执行commit操作

image.png

2.3.2 代码实现

  1. MySQLDistributedLockService 中,添加对悲观锁的操作

    /*** 悲观锁-获取资源* @param id 资源ID* @return result*/public static ResultSet getGoodCount2(int id) throws SQLException {String sql = "select  * from  database_lock_2 where id = " + id + "for update";//查询数据resultSet = statement.executeQuery(sql);return resultSet;}/*** 悲观锁-修改资源* @param id        资源ID* @return  修改状态*/public static boolean setGoodCount2(int id) throws SQLException {String sql = "update database_lock_2 set good_count = good_count - 1 where id =" + id;int stat = statement.executeUpdate(sql);return stat == 1;}/*** 悲观锁-关闭事务自动提交*/public static void closeAutoCommit(){try {connection.setAutoCommit(false);} catch (SQLException e) {log.error("关闭自动提交失败!",e);}}/*** 悲观锁-提交事务*/public static void commit(String pid,String goodName,int goodCount) throws SQLException {connection.commit();log.info("当前线程:" + pid + "抢购商品: " + goodName + "成功,剩余库存为:" + (goodCount-1));}/*** 悲观锁-回滚*/public static void rollBack() throws SQLException {connection.rollback();}
  1. 创建PessimisticLock,模拟并发操作分布式锁

/*** mysql 分布式锁-悲观锁*     执行流程:多个进程抢占同一个商品,执行业务完毕则通过connection.commit() 释放锁*     锁机制:单一进程获取锁时,则其他进程将阻塞等待*/
@Slf4j
public class PessimisticLock extends Thread {@Overridepublic void run() {super.run();ResultSet resultSet = null;String goodName = null;int goodCount = 0;String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];//关闭自动提交MySQLDistributedLockService.closeAutoCommit();try{while(true){log.info("当前线程:" + pid + "");//获取库存resultSet = MySQLDistributedLockService.getGoodCount2(1);while (resultSet.next()) {goodName = resultSet.getString("good_name");goodCount = resultSet.getInt("good_count");}log.info("获取库存成功,当前商品名称为:" + goodName + ",当前库存剩余量为:" + goodCount);// 模拟执行业务事件Thread.sleep(2 * 1000);if (0 == goodCount) {log.info("抢购失败,当前库存为0!");break;}// 抢购商品if (MySQLDistributedLockService.setGoodCount2(1)) {// 模拟延时,防止锁每次被同一进程获取MySQLDistributedLockService.commit(pid, goodName, goodCount);Thread.sleep(2 * 1000);} else {log.error("抢购商品:" + goodName + "失败!");}}}catch (Exception e){//抢购失败log.error("抢购商品发生错误!",e);try {MySQLDistributedLockService.rollBack();} catch (SQLException ex) {log.error("回滚失败! ",e);}}finally {if(resultSet != null){try {resultSet.close();} catch (SQLException e) {log.error("Result关闭失败!",e);}}MySQLDistributedLockService.close();}}public static void main(String[] args) {new PessimisticLock().start();}
}

2.3.3 代码测试

开启三个进程,查看执行情况

  • 9 6 3 0

image.png

  • 8 5 2

image.png

  • 7 4 1

image.png

注意事项:

  • 该锁为阻塞锁

  • 每次请求存在额外加锁的开销

  • 在并发量很高的情况下会造成系统中存在大量阻塞的请求,影响系统的可用性

  • 因此悲观锁适用于并发量不高,读操作不频繁的写场景

总结:

  • 在实际使用中,由于受到性能以及稳定性约束,对于关系型数据库实现的分布式锁一般很少被用到。但是对于一些并发量不高、系统仅提供给内部人员使用的单一业务场景可以考虑使用关系型数据库分布式锁,因为其复杂度较低,可靠性也能够得到保证。

3.基于Zookeeper分布式锁

3.1 Zookeeper分布式锁应用场景

image.png

  • 全部的订单服务在调用 createId 接口前都往 ZooKeeper 的注册中心的指定目录写入注册信息(如 /lock/server 01)和绑定值改变事件

  • 全部的订单服务判断自己往注册中心指定目录写入的注册信息是否是全部注册信息中的第一条?如果是,调用 createId 接口(不是第一条就等着)。调用结束后,去注册中心移除自己的信息

  • ZooKeeper 注册中心信息改变后,通知所有的绑定了值改变事件的订单服务执行第 2 条

3.2 Zookeeper分布式锁分析

客户端(对zookeeper集群而言)向zookeeper集群进行了上线注册并在一个永久节点下创建有序的临时子节点后,根据编号顺序,最小顺序的子节点获取到锁,其他子节点由小到大监听前一个节点。

image.png

当拿到锁的节点处理完事务后,释放锁,后一个节点监听到前一个节点释放锁后,立刻申请获得锁,以此类推

image.png

过程解析

  • 第一部分:客户端在zookeeper集群创建带序号的、临时的节点

  • 第二部分:判断节点是否是最小的节点,如果是,获取到锁,如果不是,监听前一个节点

3.3 分布式锁实现

1)创建 Distributedlock类, 获取与zookeeper的连接

  • 构造方法中获取连接

  • 添加 CountDownLatch (闭锁)

    CountDownLatch是具有synchronized机制的一个工具,目的是让一个或者多个线程等待,直到其他线程的一系列操作完成。

    CountDownLatch初始化的时候,需要提供一个整形数字,数字代表着线程需要调用countDown()方法的次数,当计数为0时,线程才会继续执行await()方法后的其他内容。

/*** 分布式锁*/
public class DistributedLock {private ZooKeeper client;// 连接信息private String connectString = "192.168.58.200:2181,192.168.58.200:2182,192.168.58.200:2183";// 超时时间private int sessionTimeOut = 30000;private CountDownLatch countDownLatch = new CountDownLatch(1);//1. 在构造方法中获取连接public DistributedLock() throws Exception {client = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {}});//等待Zookeeper连接成功,连接完成继续往下走countDownLatch.await();//2. 判断节点是否存在Stat stat = client.exists("/locks", false);if(stat == null){//创建一下根节点client.create("/locks","locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}}//3.对ZK加锁public void zkLock(){//创建 临时带序号节点//判断 创建的节点是否是最小序号节点,如果是 就获取到锁;如果不是就监听前一个节点}//4.解锁public void unZkLock(){//删除节点}
}

2)对zk加锁

/*** 分布式锁*/
public class DistributedLock {private ZooKeeper client;// 连接信息private String connectString = "192.168.58.200:2181,192.168.58.200:2182,192.168.58.200:2183";// 超时时间private int sessionTimeOut = 30000;// 等待zk连接成功private CountDownLatch countDownLatch = new CountDownLatch(1);// 等待节点变化private CountDownLatch waitLatch = new CountDownLatch(1);//当前节点private String currentNode;//前一个节点路径private String waitPath;//1. 在构造方法中获取连接public DistributedLock() throws Exception {client = new ZooKeeper(connectString, sessionTimeOut, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {//countDownLatch 连上ZK,可以释放if(watchedEvent.getState() == Event.KeeperState.SyncConnected){countDownLatch.countDown();}//waitLatch 需要释放 (节点被删除并且删除的是前一个节点)if(watchedEvent.getType() == Event.EventType.NodeDeleted &&watchedEvent.getPath().equals(waitPath)){waitLatch.countDown();}}});//等待Zookeeper连接成功,连接完成继续往下走countDownLatch.await();//2. 判断节点是否存在Stat stat = client.exists("/locks", false);if(stat == null){//创建一下根节点client.create("/locks","locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}}//3.对ZK加锁public void zkLock(){//创建 临时带序号节点try {currentNode = client.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);//判断 创建的节点是否是最小序号节点,如果是 就获取到锁;如果不是就监听前一个节点List<String> children = client.getChildren("/locks", false);//如果创建的节点只有一个值,就直接获取到锁,如果不是,监听它前一个节点if(children.size() == 1){return;}else{//先排序Collections.sort(children);//获取节点名称String nodeName = currentNode.substring("/locks/".length());//通过名称获取该节点在集合的位置int index = children.indexOf(nodeName);//判断if(index == -1){System.out.println("数据异常");}else if(index == 0){//就一个节点,可以获取锁return;}else{//需要监听前一个节点变化waitPath = "/locks/" + children.get(index-1);client.getData(waitPath,true,null);//等待监听执行waitLatch.await();return;}}} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}}
}

3)zk删除锁

    //4.解锁public void unZkLock() throws KeeperException, InterruptedException {//删除节点client.delete(currentNode,-1);}

4)测试

public class DistributedLockTest {public static void main(String[] args) throws Exception {final DistributedLock lock1 = new DistributedLock();final DistributedLock lock2 = new DistributedLock();new Thread(new Runnable() {@Overridepublic void run() {try {lock1.zkLock();System.out.println("线程1 启动 获取到锁");Thread.sleep(5 * 1000);lock1.unZkLock();System.out.println("线程1 释放锁");} catch (InterruptedException | KeeperException e) {e.printStackTrace();}}}).start();new Thread(new Runnable() {@Overridepublic void run() {try {lock2.zkLock();System.out.println("线程2 启动 获取到锁");Thread.sleep(5 * 1000);lock2.unZkLock();System.out.println("线程2 释放锁");} catch (InterruptedException | KeeperException e) {e.printStackTrace();}}}).start();}
}

3.4 Curator框架实现分布式锁案例

3.4.1 InterProcessMutex介绍

Apache Curator 内置了分布式锁的实现: InterProcessMutex

  • InterProcessMutex有两个构造方法

public InterProcessMutex(CuratorFramework client, String path)
{this(client, path, new StandardLockInternalsDriver());
}public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
{this(client, path, LOCK_NAME, 1, driver);
}
  • 参数说明如下

参数说明
clientcurator中zk客户端对象
path抢锁路径,同一个锁path需一致
driver可自定义lock驱动实现分布式锁
  • 主要方法如下

//获取锁,若失败则阻塞等待直到成功,支持重入
public void acquire() throws Exception//超时获取锁,超时失败
public boolean acquire(long time, TimeUnit unit) throws Exception//释放锁
public void release() throws Exception
  • 注意点,调用acquire()方法后需相应调用release()来释放锁

3.4.2 实现思路

image.png

3.4.3 分布式锁测试

  • 9 6 3 0

image.png

  • 8 5 2

image.png

  • 7 4 1

相关文章:

第五节 zookeeper集群与分布式锁_2

1.分布式锁概述 1.1 什么是分布式锁 1&#xff09;要介绍分布式锁&#xff0c;首先要提到与分布式锁相对应的是线程锁。 线程锁&#xff1a;主要用来给方法、代码块加锁。当某个方法或代码使用锁&#xff0c;在同一时刻仅有一个线程执行该方法或该代码段。 线程锁只在同一J…...

Shell脚本——提取目录名和文件名

目录 一、${} 1.${var##*/} 2.${var##*.} 3.${var#*.} 4.${var%/*} 5.${var%%.*} 6.总结 二、basename和dirname 1.basename 2.dirname 在许多场景下&#xff0c;我们都需要对文件名称或者文件所在的目录进行操作&#xff0c;已达到我们业务目的。通常的操作是由路径…...

wps使用方法(包括:插入倒三角符号,字母上面加横线,将word中的所有英文设置为time new roman)

倒三角符号 字母上面加横线 将word中的所有英文设置为time new roman ctrla选中全文...

备战蓝桥杯---图论之最小生成树

首先&#xff0c;什么是最小生成树&#xff1f; 他就是无向图G中的所有生成树中树枝权值总和最小的。 如何求&#xff1f; 我们不妨采用以下的贪心策略&#xff1a; Prim算法&#xff08;复杂度&#xff1a;&#xff08;nm)logm)&#xff1a; 我们对于把上述的点看成两个集…...

爬虫-华为云空间备忘录导出到docx-selenium控制浏览器行为-python数据处理

背景适用情况介绍 老的荣耀手机属于华为云系统&#xff0c;家里人换了新荣耀手机属于荣耀云系统无法通过云空间将备忘录转移到新手机&#xff0c;不想让他们一个一个搞&#xff0c;于是整了一晚上想办法爬取下来。从网页抓取下来&#xff0c;然后存到docx文档中&#xff08;包…...

网络安全的新防线:主动进攻,预防为先

进攻性安全&#xff08;Offensive security&#xff09;是指一系列主动安全策略&#xff0c;这些策略与恶意行为者在现实世界的攻击中使用的策略相同&#xff0c;区别在于其目的是加强而非损害网络安全。常见的进攻性安全方法包括红队、渗透测试和漏洞评估。 进攻性安全行动通常…...

基于java springboot+mybatis学生学科竞赛管理管理系统设计和实现

基于java springbootmybatis学生学科竞赛管理管理系统设计和实现 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承接各…...

秒懂百科,C++如此简单丨第二十一天:栈和队列

目录 前言 Everyday English 栈&#xff08;Stack&#xff09; 图文解释 实现添加删除元素 实现查看清空栈 完整代码 运行示例 栈的选择题 队列&#xff08;Queue&#xff09; 图文解释 队列的基本用法 完整代码 运行结果 队列的好处 结尾 前言 今天我们将…...

STM32-开发环境之STM32CubeMX

目录 STM32CubeMX介绍 STM32CubeMX特性 应用场景 其他事项 STM32CubeMX介绍 STM32CubeMX是ST公司&#xff08;意法半导体&#xff09;推出的一款图形化工具&#xff0c;也是配置和初始化C代码生成器。它主要服务于STM32微控制器的配置和开发。 STM32CubeMX特性 1.直观选…...

[晓理紫]CCF系列会议截稿时间订阅

CCF系列会议截稿时间订阅 VX 关注{晓理紫}&#xff0c;每日更新最新CCF系列会议信息&#xff0c;如感兴趣&#xff0c;请转发给有需要的同学&#xff0c;谢谢支持&#xff01;&#xff01; 如果你感觉对你有所帮助&#xff0c;请关注我&#xff0c;每日准时为你推送最新CCF会议…...

重复导航到当前位置引起的。Vue Router 提供了一种机制,阻止重复导航到相同的路由路径。

代码&#xff1a; <!-- 侧边栏 --><el-col :span"12" :style"{ width: 200px }"><el-menu default-active"first" class"el-menu-vertical-demo" select"handleMenuSelect"><el-menu-item index"…...

如何在 Angular 中使用 Flex 布局

介绍 Flex Layout 是一个组件引擎&#xff0c;允许您使用 CSS Flexbox 创建页面布局&#xff0c;并提供一组指令供您在模板中使用。 该库是用纯 TypeScript 编写的&#xff0c;因此不需要外部样式表。它还提供了一种在不同断点上指定不同指令以创建响应式布局的方法。 在本教…...

通俗的讲解什么是机器学习之损失函数

想象一下&#xff0c;你在玩一个靶心射击的游戏&#xff0c;你的目标是尽可能让箭簇命中靶心。在这个游戏中&#xff0c;损失函数可以看作是测量你的箭簇与靶心距离的规则。损失函数的值越小&#xff0c;意味着你的箭簇离靶心越近&#xff0c;你的射击技能越好。 在机器学习中…...

快速搭建PyTorch环境:Miniconda一步到位

快速搭建PyTorch环境&#xff1a;Miniconda一步到位 &#x1f335;文章目录&#x1f335; &#x1f333;一、为何选择Miniconda搭建PyTorch环境&#xff1f;&#x1f333;&#x1f333;二、Miniconda安装指南&#xff1a;轻松上手&#x1f333;&#x1f333;三、PyTorch与Minic…...

图灵日记之java奇妙历险记--抽象类和接口

目录 抽象类概念抽象类语法 接口概念规则使用特性实现多个接口接口的继承接口使用实例Clonable接口和深拷贝抽象类和接口的区别 Object类 抽象类 概念 在面向对象的概念中,所有对象都是通过类来描述的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够…...

批量给元素添加进场动画;获取文本光标位置;项目国际化

批量给元素添加进场动画 api及参数参考&#xff1a;https://juejin.cn/post/7310977323484971071 简单实现&#xff1a; addAnimationClass(){//交叉观察器if (window?.IntersectionObserver) {//获取所有需要添加进场动画的元素&#xff0c;放到一个数组let items [...do…...

解决:docker创建Redis容器成功,但无法启动Redis容器、也无报错提示

解决&#xff1a;docker创建Redis容器成功&#xff0c;但无法启动Redis容器、也无报错提示 一问题描述&#xff1a;1.docker若是直接简单使用run命令&#xff0c;但不挂载容器数据卷等参数&#xff0c;则可以启动Redis容器2.docker复杂使用run命令&#xff0c;使用指定redis.co…...

Jlink+OpenOCD+STM32 Vscode 下载和调试环境搭建

对于 Mingw 的安装比较困难&#xff0c;国内的网无法正常在线下载组件&#xff0c; 需要手动下载 x86_64-8.1.0-release-posix-seh-rt_v6-rev0.7z 版本的软件包&#xff0c;添加环境变量&#xff0c;并将 mingw32-make.exe 名字改成 make.exe。 对于 OpenOCD&#xff0c;需要…...

单片机在物联网中的应用

单片机&#xff0c;这个小巧的电子设备&#xff0c;可能听起来有点技术性&#xff0c;但它实际上是物联网世界中的一个超级英雄。简单来说&#xff0c;单片机就像是各种智能设备的大脑&#xff0c;它能让设备“思考”和“行动”。由于其体积小、成本低、功耗低、易于编程等特点…...

16.Qt 工具栏生成

目录 前言&#xff1a; 技能&#xff1a; 内容&#xff1a; 1. 界面添加 2. 信号槽 功能实现 参考&#xff1a; 前言&#xff1a; 基于QMainWindow&#xff0c;生成菜单下面的工具栏&#xff0c;可以当作菜单功能的快捷键&#xff0c;也可以完成新的功能 直接在UI文件中…...

【Linux内核】从0开始入门Linux Kernel源码

&#x1f308; 博客个人主页&#xff1a;Chris在Coding &#x1f3a5; 本文所属专栏&#xff1a;[Linux内核] ❤️ 前置学习专栏&#xff1a;[Linux学习]从0到1 ⏰ 我们仍在旅途 ​ 目录 …...

SQL Service 2008 的安装与配置

点击添加当前用户...

Apache POI | Java操作Excel文件

目录 1、介绍 2、代码示例 2.1、将数据写入Excel文件 2.2、读取Excel文件中的数据 &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅长web应用开发、数据结构和算法&#xff0c;初步…...

vue 学习definproperty方法

definproperty方法是Vue很重要的一个底层方法&#xff0c;掌握他的原理很重要&#xff0c;下面通过代码说明问题&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>回顾Object.defineproperty方法</title&…...

react 实现路由拦截

简单介绍下项目背景&#xff0c;我这里做了一个demo&#xff0c;前端使用mock数据&#xff0c;然后实现简单的路由拦截&#xff0c;校验session是否包含用户作为已登录的依据&#xff0c;react-router-dom是v6。不像vue可以设置登录拦截beforeenter&#xff0c;react需要我们自…...

数据分析(一) 理解数据

1. 描述性统计&#xff08;summary&#xff09; 对于一个新数据集&#xff0c;首先通过观察来熟悉它&#xff0c;可以打印数据相关信息来大致观察数据的常规特点&#xff0c;比如数据规模&#xff08;行数列数&#xff09;、数据类型、类别数量&#xff08;变量数目、取值范围…...

什么是 Flet?

什么是 Flet&#xff1f; Flet 是一个框架&#xff0c;允许使用您喜欢的语言构建交互式多用户 Web、桌面和移动应用程序&#xff0c;而无需前端开发经验。 您可以使用基于 Google 的 Flutter 的 Flet 控件为程序构建 UI。Flet 不只是“包装”Flutter 小部件&#xff0c;而是…...

多模态(三)--- BLIP原理与源码解读

1 BLIP简介 BLIP: Bootstrapping Language-Image Pre-training for Unified Vision-Language Understanding and Generation 传统的Vision-Language Pre-training &#xff08;VLP&#xff09;任务大多是基于理解的任务或基于生成的任务&#xff0c;同时预训练数据多是从web获…...

掌握高性能SQL的34个秘诀多维度优化与全方位指南

掌握高性能SQL的34个秘诀&#x1f680;多维度优化与全方位指南 本篇文章从数据库表结构设计、索引、使用等多个维度总结出高性能SQL的34个秘诀&#xff0c;助你轻松掌握高性能SQL 表结构设计 字段类型越小越好 满足业务需求的同时字段类型越小越好 字段类型越小代表着记录占…...

美国纳斯达克大屏怎么投放:投放完成需要多长时间-大舍传媒Dashe Media

陕西大舍广告传媒有限公司&#xff08;Shaanxi Dashe Advertising Media Co., Ltd&#xff09;&#xff0c;简称大舍传媒&#xff08;Dashe Media&#xff09;&#xff0c;是纳斯达克在中国区的总代理&#xff08;China General Agent&#xff09;。与纳斯达克合作已经有八年的…...

【MySQL】多表关系的基本学习

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-3oES1ZdkKIklfKzq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…...

Springboot之接入gRPC

1、maven依赖 <properties><!-- grpc --><protobuf.version>3.5.1</protobuf.version><protobuf-plugin.version>0.6.1</protobuf-plugin.version><grpc.version>1.42.1</grpc.version><os-maven-plugin.version>1.6.0…...

2023年中国数据智能管理峰会(DAMS上海站2023):核心内容与学习收获(附大会核心PPT下载)

随着数字经济的飞速发展&#xff0c;数据已经渗透到现代社会的每一个角落&#xff0c;成为驱动企业创新、提升治理能力、促进经济发展的关键要素。在这样的背景下&#xff0c;2023年中国数据智能管理峰会&#xff08;DAMS上海站2023&#xff09;应运而生&#xff0c;汇聚了众多…...

DS:八大排序之堆排序、冒泡排序、快速排序

创作不易&#xff0c;友友们给个三连吧&#xff01;&#xff01; 一、堆排序 堆排序已经在博主关于堆的实现过程中详细的讲过了&#xff0c;大家可以直接去看&#xff0c;很详细,这边不介绍了 DS&#xff1a;二叉树的顺序结构及堆的实现-CSDN博客 直接上代码&#xff1a; …...

Sora:继ChatGPT之后,OpenAI的又一力作

关于Sora的报道&#xff0c;相信很多圈内朋友都已经看到了来自各大媒体铺天盖地的宣传了&#xff0c;这次&#xff0c;对于Sora的宣传&#xff0c;绝不比当初ChatGPT的宣传弱。自OpenAI发布了GPT4之后&#xff0c;就已经有很多视频生成模型了&#xff0c;不过这些模型要么生成的…...

阅读笔记(BMSB 2018)Video Stitching Based on Optical Flow

参考文献 Xie C, Zhang X, Yang H, et al. Video Stitching Based on Optical Flow[C]//2018 IEEE International Symposium on Broadband Multimedia Systems and Broadcasting (BMSB). IEEE, 2018: 1-5. 摘要 视频拼接在计算机视觉中仍然是一个具有挑战性的问题&#xff0…...

Ubuntu学习笔记-Ubuntu搭建禅道开源版及基本使用

文章目录 概述一、Ubuntu中安装1.1 复制下载安装包路径1.2 将安装包解压到ubuntu中1.3 启动服务1.4 设置开机自启动 二、禅道服务基本操作2.1 启动&#xff0c;停止&#xff0c;重启&#xff0c;查看服务状态2.2 开放端口2.3 访问和登录禅道 卜相机关 卜三命、相万生&#xff0…...

《苍穹外卖》知识梳理6-缓存商品,购物车功能

苍穹外卖实操笔记六—缓存商品&#xff0c;购物车功能 一.缓存菜品 可以使用redis进行缓存&#xff1b;另外&#xff0c;在实现缓存套餐时可以使用spring cache提高开发效率&#xff1b;   通过缓存数据&#xff0c;降低访问数据库的次数&#xff1b; 使用的缓存逻辑&#…...

[NSSCTF]-Web:[SWPUCTF 2021 新生赛]easy_sql解析

查看网页 有提示&#xff0c;参数是wllm&#xff0c;并且要我们输入点东西 所以&#xff0c;我们尝试以get方式传入 有回显&#xff0c;但似乎没啥用 从上图看应该是字符型漏洞&#xff0c;单引号字符注入 先查看字段数 /?wllm2order by 3-- 没回显 报错了&#xff0c;说明…...

vue3 codemirror yaml文件编辑器插件

需求&#xff1a;前端编写yaml配置文件 &#xff0c;检查yaml语法 提供语法高亮 。 默认内容从后端接口获取 显示在前端 &#xff0c; 前端在codemirror 插件中修改文件内容 &#xff0c;并提交修改 后端将提交的内容写入服务器配置文件中 。 codemirror 通过ref 后期编辑器…...

力扣经典题:环形链表的检测与返回

1.值得背的题 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode *detectCycle(struct ListNode *head) {struct ListNode*fasthead;struct ListNode*slowhead;while(fast!NULL&&fast->…...

【web | CTF】BUUCTF [BJDCTF2020]Easy MD5

天命&#xff1a;好像也挺实用的题目&#xff0c;也是比较经典吧 天命&#xff1a;把php的MD5漏洞都玩了一遍 第一关&#xff1a;MD5绕过 先声明一下&#xff1a;这题的MD5是php&#xff0c;不是mysql的MD5&#xff0c;把我搞迷糊了 一进来题目啥也没有&#xff0c;那么就要看…...

spring boot Mybatis Plus分页

文章目录 Mybatis Plus自带分页和PageHelper有什么区别&#xff1f;Mybatis Plus整合PageHelper分页 springboot自定义拦截器获取分页参数spring boot下配置mybatis-plus分页插件单表分页查询自定义sql分页查询PageHelper 参考 Mybatis Plus自带分页和PageHelper有什么区别&…...

elementui 中 el-date-picker 控制选择当前年之前或者之后的年份

文章目录 需求分析 需求 对 el-date-picker控件做出判断控制 分析 给 el-date-picker 组件添加 picker-options 属性&#xff0c;并绑定对应数据 pickerOptions html <el-form-item label"雨量年份&#xff1a;" prop"date"><el-date-picker …...

GlusterFS:开源分布式文件系统的深度解析与应用场景实践

引言 在当今大数据时代背景下&#xff0c;企业对存储系统的容量、性能和可靠性提出了前所未有的挑战。GlusterFS作为一款开源的、高度可扩展的分布式文件系统&#xff0c;以其独特的无中心元数据设计和灵活的卷管理机制&#xff0c;在众多场景中脱颖而出&#xff0c;为解决大规…...

第6个-滚动动画

Day 6 - Scroll Animation 1. 演示效果 2. 分析思路 布局 所有的内容进行水平垂直居中&#xff0c;可以使用**margin:0 auto;&#xff0c;也可以使用flex**布局&#xff1a; body {background-color: #efedd6;display: flex;flex-direction: column;justify-content: center…...

配置oracle连接管理器(cman)

Oracle Connection Manager是一个软件组件&#xff0c;可以在oracle客户端上指定安装这个组件&#xff0c;Oracle连接管理器代理发送给数据库服务器的请求&#xff0c;在连接管理器中&#xff0c;我们可以通过配置各种规则来控制会话访问。 简而言之&#xff0c;不同于专用连接…...

[N-142]基于springboot,vue停车场管理系统

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 项目采用前后端分离 前端技术&#xff1a;vueelementUI 服务端技术&#xff1a;springbootmybatis-plus 本项目分为普通用户和管理员…...

DAY53:动态规划(买股票的最佳时机)

Leetcode: 121 买卖股票的最佳时机 代码随想录 1、确定下标和含义 dp[i][0]表示当天持有股票所得的最多现金 do[i][1]表示当天不持有股票的最多现金 2、递推公式 &#xff08;1&#xff09;如果第i天持有股票即dp[i][0]&#xff0c; 那么可以由两个状态推出来 第i-1天就…...

快速实现用户认证:使用Python和Flask配合PyJWT生成与解密Token的教程及示例代码

生成token 与解密 token 和 拦截器 #学习交流 访问 # https://v.iiar.cnimport jwt import datetime from models import XUser from flask import request, jsonify from functools import wrapsSECRET_KEY XPay# 创建token def generate_token(user_id):try:payload {exp:…...