(五)库存超卖案例实战——使用zookeeper分布式锁解决“超卖”问题

前言

本节内容使用zookeeper实现分布式锁,完成并发访问“超卖”问题的解决。相对于redis分布式锁,zookeeper能够保证足够的安全性。关于zookeeper的安装内容这里不做介绍,开始本节内容之前先自行安装好zookeeper中间键服务。这里我们利用创建zookeeper路径节点的唯一性实现分布式锁。并同时演示如何使用Curator工具包,完成分布式锁。

正文

  • 在项目中添加zookeeper的pom依赖
<dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.7.2</version>
</dependency>
  • 创建zookeeper客户端工具,实现加锁和解锁方法 

- 创建ZookeeperClient客户端工具

package com.ht.atp.plat.util;import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;@Component
public class ZookeeperClient {/*** zookeeper连接地址*/private static final String connectString = "192.168.110.88:2181";/*** 分布式锁根路径*/private static final String ROOT_PATH = "/distributed";/*** zookeeper客户端*/private ZooKeeper zooKeeper;/*** 初始化zookeeper客户端*/@PostConstructpublic void init() {try {// 连接zookeeper服务器this.zooKeeper = new ZooKeeper(connectString, 30000, event -> System.out.println("获取链接成功!!"));// 创建分布式锁根节点if (this.zooKeeper.exists(ROOT_PATH, false) == null) {this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}} catch (Exception e) {System.out.println("获取链接失败!");e.printStackTrace();}}/*** 销毁zookeeper客户端*/@PreDestroypublic void destroy() {try {if (zooKeeper != null) {zooKeeper.close();}} catch (InterruptedException e) {e.printStackTrace();}}/*** 加锁** @param lockName*/public void lock(String lockName) {try {zooKeeper.create(ROOT_PATH + "/" + lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);} catch (Exception e) {// 重试try {Thread.sleep(200);lock(lockName);} catch (InterruptedException ex) {ex.printStackTrace();}}System.out.println("----------加锁成功------------");}/*** 解锁** @param lockName*/public void unlock(String lockName) {try {this.zooKeeper.delete(ROOT_PATH + "/" + lockName, 0);} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}System.out.println("----------解锁成功------------");}
}

- 使用临时节点EPHEMERAL加锁,这里使用临时节点是方便锁的自动释放,避免发生死锁问题

- 业务执行完成,删除临时节点,解锁

  • 实现“超卖”的业务方法,使用自定义的zookeeper工具类加锁
    @AutowiredZookeeperClient zookeeperClient;@Overridepublic void checkAndReduceStock() {zookeeperClient.lock("lock");// 查询库存WmsStock wmsStock = baseMapper.selectById(1L);// 验证库存大于0再扣减库存if (wmsStock != null && wmsStock.getStockQuantity() > 0) {wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1);baseMapper.updateById(wmsStock);}// 释放锁zookeeperClient.unlock("lock");}
  • 将数据库库存表的库存恢复为10000,分别启动7000,7001,7002服务

  •  启动jmeter压测工具,压测库存扣减接口,查看结果

- 库存扣减为0

- jmeter压测结果,平均访问时间502ms,请求吞吐量为每秒161

- 不存在并发“超卖问题”,但是接口访问吞吐量较低。由于在加锁过程中,获取不到锁,会无限自旋去获取锁,导致性能下降。

  •  优化分布式锁,使用zk的临时序列化节点实现分布式锁,避免锁的自旋操作

- 优化代码

package com.ht.atp.plat.util;import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;@Component
public class ZookeeperClientNoBlock {/*** zookeeper连接地址*/private static final String connectString = "192.168.110.88:2181";/*** 分布式锁根路径*/private static final String ROOT_PATH = "/distributed";/*** zookeeper客户端*/private ZooKeeper zooKeeper;/*** 初始化zookeeper客户端*/@PostConstructpublic void init() {try {// 连接zookeeper服务器this.zooKeeper = new ZooKeeper(connectString, 30000, event -> System.out.println("获取链接成功!!"));// 创建分布式锁根节点if (this.zooKeeper.exists(ROOT_PATH, false) == null) {this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}} catch (Exception e) {System.out.println("获取链接失败!");e.printStackTrace();}}/*** 销毁zookeeper客户端*/@PreDestroypublic void destroy() {try {if (zooKeeper != null) {zooKeeper.close();}} catch (InterruptedException e) {e.printStackTrace();}}/*** 创建锁** @param lockName*/public String lock(String lockName) {try {String realLock = zooKeeper.create(ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);return realLock;} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("----------加锁成功------------");return null;}/*** 检查锁** @param lockName*/public void checkLock(String lockName) {String preNode = getPreNode(lockName);// 如果该节点没有前一个节点,说明该节点时最小节点,放行执行业务逻辑if (StringUtils.isEmpty(preNode)) {return;}// 重新检查。是否获取到锁try {Thread.sleep(20);} catch (InterruptedException ex) {ex.printStackTrace();}checkLock(lockName);}/*** 获取指定节点的前节点** @param path* @return*/private String getPreNode(String path) {try {// 获取当前节点的序列化号Long curSerial = Long.valueOf(StringUtils.substringAfterLast(path, "-"));// 获取根路径下的所有序列化子节点List<String> nodes = this.zooKeeper.getChildren(ROOT_PATH, false);// 判空if (CollectionUtils.isEmpty(nodes)) {return null;}// 获取前一个节点Long flag = 0L;String preNode = null;for (String node : nodes) {// 获取每个节点的序列化号Long serial = Long.valueOf(StringUtils.substringAfterLast(node, "-"));if (serial < curSerial && serial > flag) {flag = serial;preNode = node;}}return preNode;} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}return null;}/*** 解锁** @param lockName*/public void unlock(String lockName) {try {this.zooKeeper.delete(lockName, 0);} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}System.out.println("----------解锁成功------------");}
}

- 使用临时序列化节点EPHEMERAL_SEQUENTIAL加锁,保证每个节点都可以加锁成功,避免自旋操作

- 通过判断当前节点是否是第一个节点,如果是第一个节点,才可执行后续的业务

- 解锁操作

  • 扣减库存业务实现方法
public void checkAndReduceStock() {//加锁String lock = zookeeperClientNoBlock.lock("lock");//检查锁zookeeperClientNoBlock.checkLock(lock);// 查询库存WmsStock wmsStock = baseMapper.selectById(1L);// 验证库存大于0再扣减库存if (wmsStock != null && wmsStock.getStockQuantity() > 0) {wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1);baseMapper.updateById(wmsStock);}// 释放锁zookeeperClientNoBlock.unlock(lock);}
  • 将扣减库存恢复为10000,重新启动7000,7001,7002服务,使用jmeter压测工具再次压测

- 库存扣减为0

- 压测结果:平均访问时间1597ms,请求访问吞吐量为每秒62

从优化结果来看,此种方式更加耗时,性能更差。将加锁操作改为非自旋操作,虽然加锁不在耗时,但是会自旋判断自己是否是最小的节点,依然存在耗时操作,且逻辑更为复杂。

  • 优化分布式锁,通过使用zookeeper的Watcher监听来实现阻塞锁

- 实现代码

package com.ht.atp.plat.util;import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.apache.zookeeper.proto.WatcherEvent;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.concurrent.CountDownLatch;@Component
public class ZookeeperClientBlockWatch {/*** zookeeper连接地址*/private static final String connectString = "192.168.110.88:2181";/*** 分布式锁根路径*/private static final String ROOT_PATH = "/distributed";/*** zookeeper客户端*/private ZooKeeper zooKeeper;/*** 初始化zookeeper客户端*/@PostConstructpublic void init() {try {// 连接zookeeper服务器this.zooKeeper = new ZooKeeper(connectString, 30000, event -> System.out.println("获取链接成功!!"));// 创建分布式锁根节点if (this.zooKeeper.exists(ROOT_PATH, false) == null) {this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}} catch (Exception e) {System.out.println("获取链接失败!");e.printStackTrace();}}/*** 销毁zookeeper客户端*/@PreDestroypublic void destroy() {try {if (zooKeeper != null) {zooKeeper.close();}} catch (InterruptedException e) {e.printStackTrace();}}/*** 创建锁** @param lockName*/public String lock(String lockName) {try {String realLock = zooKeeper.create(ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);return realLock;} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("----------加锁成功------------");return null;}/*** 检查锁** @param lockName*/public void checkLock(String lockName) {try {String preNode = getPreNode(lockName);// 如果该节点没有前一个节点,说明该节点时最小节点,放行执行业务逻辑if (!StringUtils.isEmpty(preNode)) {CountDownLatch countDownLatch = new CountDownLatch(1);if (this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {System.out.println("监控:"+ watchedEvent.getPath());countDownLatch.countDown();}}) == null) {return;}// 阻塞,减少自旋检查countDownLatch.await();}return;} catch (Exception e) {e.printStackTrace();// 重新检查。是否获取到锁try {Thread.sleep(20);} catch (InterruptedException ex) {ex.printStackTrace();}checkLock(lockName);}}/*** 获取指定节点的前节点** @param path* @return*/private String getPreNode(String path) {try {// 获取当前节点的序列化号Long curSerial = Long.valueOf(StringUtils.substringAfterLast(path, "-"));// 获取根路径下的所有序列化子节点List<String> nodes = this.zooKeeper.getChildren(ROOT_PATH, false);// 判空if (CollectionUtils.isEmpty(nodes)) {return null;}// 获取前一个节点Long flag = 0L;String preNode = null;for (String node : nodes) {// 获取每个节点的序列化号Long serial = Long.valueOf(StringUtils.substringAfterLast(node, "-"));if (serial < curSerial && serial > flag) {flag = serial;preNode = node;}}return preNode;} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}return null;}/*** 解锁** @param lockName*/public void unlock(String lockName) {try {this.zooKeeper.delete(lockName, 0);} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}System.out.println("----------解锁成功------------");}
}

- 检查加锁,加入前一个节点的监控判断,如果前一个节点的锁还没有释放,就使用CountDownLatch计数器阻塞程序,减少自旋检查,当监控到前一个节点的锁释放,则当前节点获取到锁,开始执行业务逻辑

  •  修改扣减库存业务实现方法为监控阻塞的方式
    @AutowiredZookeeperClientBlockWatch zookeeperClientBlockWatch;public void checkAndReduceStock() {//加锁String lock = zookeeperClientBlockWatch.lock("lock");//检查锁zookeeperClientBlockWatch.checkLock(lock);// 查询库存WmsStock wmsStock = baseMapper.selectById(1L);// 验证库存大于0再扣减库存if (wmsStock != null && wmsStock.getStockQuantity() > 0) {wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1);baseMapper.updateById(wmsStock);}// 释放锁zookeeperClientBlockWatch.unlock(lock);}
  •  将扣减库存恢复为10000,重新启动7000,7001,7002服务,使用jmeter压测工具再次压测

- 库存扣减为了0

- jmeter压测结果:平均访问时间441ms,请求访问吞吐量为每秒223

从优化结果来看,使用带监控的阻塞锁吞吐量更高,平均访问时间更小,性能比自旋加锁的方式性能更优。

  • 使用ThreadLocal优化分布式锁,将zookeeper的Watcher监听来实现阻塞锁优化为可重入分布式锁

- 优化代码

package com.ht.atp.plat.util;import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.concurrent.CountDownLatch;@Component
public class ZookeeperClientBlockWatchReentrant {/*** zookeeper连接地址*/private static final String connectString = "192.168.110.88:2181";/*** 分布式锁根路径*/private static final String ROOT_PATH = "/distributed";/*** zookeeper客户端*/private ZooKeeper zooKeeper;/*** ThreadLocal本地线程*/private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();/*** 初始化zookeeper客户端*/@PostConstructpublic void init() {try {// 连接zookeeper服务器this.zooKeeper = new ZooKeeper(connectString, 30000, event -> System.out.println("获取链接成功!!"));// 创建分布式锁根节点if (this.zooKeeper.exists(ROOT_PATH, false) == null) {this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}} catch (Exception e) {System.out.println("获取链接失败!");e.printStackTrace();}}/*** 销毁zookeeper客户端*/@PreDestroypublic void destroy() {try {if (zooKeeper != null) {zooKeeper.close();}} catch (InterruptedException e) {e.printStackTrace();}}/*** 创建锁** @param lockName*/public String lock(String lockName) {try {if (THREAD_LOCAL.get() == null || THREAD_LOCAL.get() == 0){String realLock = zooKeeper.create(ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);return realLock;}} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("----------加锁成功------------");return null;}/*** 检查锁** @param lockName*/public void checkLock(String lockName) {Integer flag = THREAD_LOCAL.get();if (flag != null && flag > 0) {THREAD_LOCAL.set(flag + 1);return;}try {String preNode = getPreNode(lockName);// 如果该节点没有前一个节点,说明该节点时最小节点,放行执行业务逻辑if (!StringUtils.isEmpty(preNode)) {CountDownLatch countDownLatch = new CountDownLatch(1);if (this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {System.out.println("监控:"+ watchedEvent.getPath());countDownLatch.countDown();}}) == null) {THREAD_LOCAL.set(1);return;}// 阻塞,减少自旋检查countDownLatch.await();}THREAD_LOCAL.set(1);return;} catch (Exception e) {e.printStackTrace();// 重新检查。是否获取到锁try {Thread.sleep(20);} catch (InterruptedException ex) {ex.printStackTrace();}checkLock(lockName);}}/*** 获取指定节点的前节点** @param path* @return*/private String getPreNode(String path) {try {// 获取当前节点的序列化号Long curSerial = Long.valueOf(StringUtils.substringAfterLast(path, "-"));// 获取根路径下的所有序列化子节点List<String> nodes = this.zooKeeper.getChildren(ROOT_PATH, false);// 判空if (CollectionUtils.isEmpty(nodes)) {return null;}// 获取前一个节点Long flag = 0L;String preNode = null;for (String node : nodes) {// 获取每个节点的序列化号Long serial = Long.valueOf(StringUtils.substringAfterLast(node, "-"));if (serial < curSerial && serial > flag) {flag = serial;preNode = node;}}return preNode;} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}return null;}/*** 解锁** @param lockName*/public void unlock(String lockName) {try {THREAD_LOCAL.set(THREAD_LOCAL.get() - 1);if (THREAD_LOCAL.get() == 0) {this.zooKeeper.delete(lockName, 0);THREAD_LOCAL.remove();}} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}System.out.println("----------解锁成功------------");}
}

- 使用ThreadLocal存储当前线程的操作

- 加锁:先检测本地线程是否存在或者值是否为0,如果不存在或者为0,则创建锁,避免重复创建

- 检查加锁:如果值大于0,代表可重入,值加1;如果不存在或者等于0,将值设置为1,代表第一次获取到锁

- 解锁:获取线程的值,并减少1,如果减少后的值变为0,代表锁已经不在占用,此时才释放锁,并且将当前线程的值存储移除;如果减少的值还是大于0,代表此时锁还在占用

  •  扣减库存业务代码
    @AutowiredZookeeperClientBlockWatchReentrant zookeeperClientBlockWatchReentrant;public void checkAndReduceStock() {//加锁String lock = zookeeperClientBlockWatchReentrant.lock("lock");//检查锁zookeeperClientBlockWatchReentrant.checkLock(lock);// 查询库存WmsStock wmsStock = baseMapper.selectById(1L);// 验证库存大于0再扣减库存if (wmsStock != null && wmsStock.getStockQuantity() > 0) {wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1);baseMapper.updateById(wmsStock);}// 释放锁zookeeperClientBlockWatchReentrant.unlock(lock);}
  • 将扣减库存恢复为10000,重新启动7000,7001,7002服务,使用jmeter压测工具再次压测

- 库存扣减为0

- jmeter压测结果:平均访问时间426ms,吞吐量每秒231

  •  使用zookeeper的Curator工具包实现分布式锁

- 引入pom依赖

<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>5.1.0</version>
</dependency>

- 创建CuratorFramework的bean工具类

package com.ht.atp.plat.config;import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorEvent;
import org.apache.curator.framework.api.CuratorEventType;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.WatchedEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** CuratorFramework工具类配置*/
@Slf4j
@Configuration
public class ZookeeperConfig {@Beanpublic CuratorFramework curatorFramework() {// ExponentialBackoffRetry是种重连策略,每次重连的间隔会越来越长,1000毫秒是初始化的间隔时间,3代表尝试重连次数。ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);// 创建客户端CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("192.168.110.88:2181", retryPolicy);// 添加watched 监听器curatorFramework.getCuratorListenable().addListener((CuratorFramework client, CuratorEvent event) -> {CuratorEventType type = event.getType();if (type == CuratorEventType.WATCHED) {WatchedEvent watchedEvent = event.getWatchedEvent();String path = watchedEvent.getPath();log.info(watchedEvent.getType() + " ----------------------------> " + path);// 重新设置该节点监听if (null != path) {client.checkExists().watched().forPath(path);}}});// 启动客户端curatorFramework.start();return curatorFramework;}
}

- 使用可重入锁,扣减库存业务方法

@Autowiredprivate CuratorFramework curatorFramework;public void checkAndReduceStock() throws Exception {//加锁InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/curator/lock");try {// 加锁mutex.acquire();// 查询库存WmsStock wmsStock = baseMapper.selectById(1L);// 验证库存大于0再扣减库存if (wmsStock != null && wmsStock.getStockQuantity() > 0) {wmsStock.setStockQuantity(wmsStock.getStockQuantity() - 1);baseMapper.updateById(wmsStock);}this.testSub(mutex);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁mutex.release();}}public void testSub(InterProcessMutex mutex) throws Exception {try {mutex.acquire();System.out.println("测试可重入锁。。。。");} catch (Exception e) {e.printStackTrace();}finally {// 释放锁mutex.release();}}
  • 将扣减库存恢复为10000,重新启动7000,7001,7002服务,使用jmeter压测工具再次压测 

- 库存扣减为0

- jmeter压测结果:平均访问时间497,吞吐量每秒198

- 能够解决并发访问“超卖问题”

结语

关于使用zookeeper分布式锁解决“超卖”问题的内容到这里就结束了,我们下期见。。。。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/178728.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CIM与MES

CIM系统&#xff0c;全称计算机集成制造系统&#xff08;Computer-Integrated Manufacturing&#xff09;&#xff0c;是一种集成了计算机技术、网络通讯技术和软件系统的制造自动化框架。CIM的主要目标是整合制造过程中的所有活动&#xff0c;包括生产管理、设备管理和品质管理…

《巴渝小将》少儿电视综艺走进江小白金色黄庄拍摄圆满成功!

巴渝小将&#xff0c;乘风破浪&#xff01; 张扬巴渝魅力&#xff0c;展示少年风采&#xff0c;本期拍摄我们来到了位于江津的江小白金色黄庄。 江小白金色黄庄位于永兴镇黄庄村&#xff0c;是一座充满诗意又不乏童趣的农文旅综合体&#xff0c;基于当地良好的酿酒高粱产业基础…

SpringBoot系列之自定义Jackson对象映射器格式日期数据

开发环境 JDK 1.8SpringBoot2.2.1Maven 3.2Mysql5.7.36开发工具 IntelliJ IDEAsmartGit 背景 在我之前的博客中&#xff0c;有对Springboot2.0集成Mybatis Plus做了比较详细的描述&#xff0c;现在这篇博客介绍&#xff0c;基于开源的jackson api来自定义ObjectMapping&…

生产环境使用boost::fiber

简介 boost::fiber是一类用户级线程&#xff0c;也就是纤程。其提供的例子与实际生产环境相距较远&#xff0c;本文将对其进行一定的改造&#xff0c;将其能够投入到生产环境。 同时由于纤程是具有传染性的&#xff0c;使用纤程的代码里也全部要用纤程封装&#xff0c;本文将对…

DBeaver 23.2.3发布,带来多项增强和修复

数据库管理工具DBeaver最新版本23.2.3已经发布。这个版本带来了一系列的增强和修复&#xff0c;提升了用户的使用体验和工作效率。 以下是DBeaver 23.2.3版本的一些亮点功能&#xff1a; 数据编辑器方面的改进&#xff1a; Excel (XLSX) 导出现在支持列自动拟合&#xff0c;…

Perl爬虫程序

以下是一个使用Perl爬虫程序&#xff0c;用于爬取图像。每行代码的中文解释如下&#xff1a; #!/usr/bin/perl ​ use strict; use warnings; use Mojo::UserAgent; use JSON; ​ # 创建一个Mojo::UserAgent实例 my $ua Mojo::UserAgent->new; ​ # 使用获取代理 my $prox…

Centos7快速重置root密码

1、重新启动Centos7&#xff0c;5秒内按向下方向键&#xff0c;使其停留在开机界面&#xff0c;如下图。 2、按’e’键&#xff0c;进入如下界面&#xff0c;移动向下方向键至“linux16”开头的行。然后按向右的方向键移动,找到“ro”并将其修改为“rw init/sysroot/bin/bash…

竞赛选题 深度学习实现行人重识别 - python opencv yolo Reid

文章目录 0 前言1 课题背景2 效果展示3 行人检测4 行人重识别5 其他工具6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习的行人重识别算法研究与实现 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c…

对象加锁原理

每个对象都有一个对象头&#xff0c;叫做mark word&#xff0c;以32位虚拟机为例&#xff0c;64为虚拟机的mark word是64位 64位虚拟机的markword占64位&#xff1a; 偏向锁&#xff1a; 当对象一个线程加锁以后&#xff0c;在这个对象的markword中记录这个线程的id&#xff0c…

【RGB-HMS:先验驱动:超分】

PRINET: A PRIOR DRIVEN SPECTRAL SUPER-RESOLUTION NETWORK &#xff08;PRINET&#xff1a;先验驱动的光谱超分辨网络&#xff09; 光谱超分辨率是指直接从RGB图像重建高光谱图像。近年来&#xff0c;卷积网络已经成功地用于这一任务。然而&#xff0c;很少有人考虑到高光谱…

nodelist 与 HTMLCollection 的区别

原地址 https://cloud.tencent.com/developer/article/2013289 节点与元素 根据 W3C 的 HTML DOM 标准&#xff0c;HTML 文档中的所有内容都是节点&#xff1a; 整个文档是一个文档节点每个 HTML 元素是元素节点HTML 元素内的文本是文本节点每个 HTML 属性是属性节点注释是注…

ES-初识ES

文章目录 介绍ElasticSearchElasticSearch的主要功能ElasticSearch的主要特性ElasticSearch的家族成员LogStashKibanaBeats ELK&#xff08;ElasticSearch LogStash Kibana&#xff09;的应用场景与数据库集成指标采集/日志分析 安装和配置ElasticSearch一、安装1、下载ES安装…

SOME/IP 协议介绍(一)

1. 引言和功能概述 本文档规定了可扩展面向服务基于IP的中间件&#xff08;SOME/IP&#xff09;——一种用于汽车/嵌入式RPC机制和底层序列化/传输格式的示例&#xff0c;作为由RTE调用的序列化器。 唯一有效的缩写是SOME/IP。其他缩写&#xff08;例如Some/IP&#xff09;是…

什么是Babel?它的主要作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

【51单片机】LED与独立按键(学习笔记)

一、点亮一个LED 1、LED介绍 LED&#xff1a;发光二极管 补&#xff1a;电阻读数 102 > 10 00 1k 473 > 47 000 2、Keil的使用 1、新建工程&#xff1a;Project > New Project Ctrl Shift N &#xff1a;新建文件夹 2、选型号&#xff1a;Atmel-AT89C52 3、xxx…

VBA之正则表达式(44)-- 拆分商品和规格

实例需求&#xff1a;商品组清单保存在A列中&#xff0c;现需要将其拆分为商品名称&#xff0c;保存在从B列开始的后续单元格中&#xff0c;部分商品包含规格&#xff0c;并且多种规格属性使用了逗号分隔&#xff0c;因此无法直接使用Excel分列功能完成数据拆分。 示例代码如下…

【错误解决方案】ModuleNotFoundError: No module named ‘torchvision.models.utils‘

1. 错误提示 在python程序&#xff0c;尝试导入一个名为torchvision.models.utils的模块&#xff0c;但Python提示找不到这个模块。 错误提示&#xff1a;ModuleNotFoundError: No module named torchvision.models.utils 2. 解决方案 1&#xff09;这可能是因为你还没有安装…

利用云计算和微服务架构开发可扩展的同城外卖APP

如今&#xff0c;同城外卖APP已经成为了人们点餐的主要方式之一。然而&#xff0c;要构建一款成功的同城外卖APP&#xff0c;不仅需要满足用户的需求&#xff0c;还需要具备可扩展性&#xff0c;以适应快速增长的用户和订单量。 一、了解同城外卖APP的需求 在着手开发同城外卖…

centos 7 kafka2.6单机安装及动态认证SASL SCRAM配置

目录 1.kfaka安装篇 1.1 安装jdk 1.2安装kafka 2.安全篇 2.1 kafka安全涉及3部份&#xff1a; 2.2 Kafka权限控制认证方式 2.3 SASL/SCRAM-SHA-256 配置实例 2.3.1 创建用户 2.3.2 创建 JAAS 文件及配置 3.测试 3.1 创建测试用户 3.2 配置JAAS 文件 3.2.1 生产者配…

C++归并排序算法的应用:计算右侧小于当前元素的个数

题目 给你一个整数数组 nums &#xff0c;按要求返回一个新数组 counts 。数组 counts 有该性质&#xff1a; counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。 示例 1&#xff1a; 输入&#xff1a;nums [5,2,6,1] 输出&#xff1a;[2,1,1,0] 解释&#xff1a; 5 …