Curator基本使用

文章目录

    • 1. 基本操作
      • 1.1 建立连接
      • 1.2 创建结点
      • 1.3 查询结点
        • 查询数据
        • 查询子结点
        • 查看结点信息
      • 1.4 修改结点
        • 普通修改
        • 带乐观锁的修改
      • 1.5 删除
        • 删除单个结点
        • 删除带子结点的结点
        • 必须成功的删除
        • 带回调函数的删除
    • 2. 监听器事件
      • 2.1 NodeCache单一结点连续监听
      • 2.2 PathChildrenCache监听子结点
      • 2.3 TreeCache监听当前结点+子结点
    • 3. Curator 分布式锁
      • 3.1 zookeeper分布式锁原理
      • 3.2 Curator 封装的五种分布式锁
      • 3.3 模拟12306卖票实现可重入锁
      • 3.4 读写锁InterProcessReadWriteLock

Apache Curator官网

Apache Curator官网简介:

Apache Curator is a Java/JVM client library for Apache ZooKeeper, a distributed coordination service. It includes a highlevel API framework and utilities to make using Apache ZooKeeper much easier and more reliable. It also includes recipes for common use cases and extensions such as service discovery and a Java 8 asynchronous DSL.

是一个 Java/JVM 客户端库,用于 Apache ZooKeeper 服务,一个分布式协调服务。它包括一个高级的 API 框架和实用工具,使得使用 google Apache ZooKeeper 变得更加容易和可靠。它还包括常见用例和扩展的处方,例如服务发现和 java8异步 DSL。

Curator 就是帮我们封装了zookeeper的常用操作,节省我们的代码量,例如我们只需要写不超过五行代码就可以使用zookeeper实现分布式锁,还有session超时重连,主从选举,分布式计数器,等适用于各种复杂的zookeeper场景的API封装,接下来让我们看看如何使用

首先我们需要导入pom依赖,Curator依赖的版本只能比zookeeper依赖的版本高

<properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><lombok.version>1.18.22</lombok.version><slf4j.version>1.7.36</slf4j.version><junit.version>4.7</junit.version><logback.version>1.2.3</logback.version><zookeeper.version>3.7.0</zookeeper.version><curator-framework.version>4.0.1</curator-framework.version><curator-recipes.version>4.0.1</curator-recipes.version>
</properties>
<dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>${slf4j.version}</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>${logback.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version><scope>compile</scope></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>${zookeeper.version}</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>${curator-framework.version}</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>${curator-recipes.version}</version></dependency>
</dependencies>

1. 基本操作

1.1 建立连接

public class Test01_connection {private CuratorFramework client;@Beforepublic void setUp() {String url = "zk的ip地址:端口号";/** Create a new client** @param connectString       连接url,集群用逗号分割* @param sessionTimeoutMs    会话超时时间 单位:毫秒,默认60秒* @param connectionTimeoutMs 连接超时时间 单位:毫秒,默认15秒* @param retryPolicy         重试策略* @return client*///重试策略,每隔3秒重试一次,最多重试10次ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(3000, 10);// 第一种:通过构造器构造
//        CuratorFramework client = CuratorFrameworkFactory.newClient(url,
//                60 * 1000, 15 * 1000, retryPolicy);// 第二种:builder模式链式编程client = CuratorFrameworkFactory.builder().connectString(url).sessionTimeoutMs(60 * 1000)  //会话超时时间.connectionTimeoutMs(15 * 1000).retryPolicy(retryPolicy)     //掉线重连策略.namespace("Test01_connection") //默认一个根目录,以后的所有创建都会在此目录下进行.build();client.start();//开启连接}@Afterpublic void after(){if(client != null){client.close();}}
}

1.2 创建结点

创建节点:create 持久 临时 顺序 数据

  1. 基本创建 :create().forPath(“”)
  2. 创建节点 带有数据:create().forPath(“”,data)
  3. 设置节点的类型:create().withMode().forPath(“”,data)
  4. 创建多级节点 /app1/p1 :create().creatingParentsIfNeeded().forPath(“”,data)

普通结点:默认持久无序

@Test
public void testCreate() throws Exception {client.create().forPath("/app1","curator创建的结点".getBytes());
}

在这里插入图片描述

创建临时无序结点

CreateMode是一个枚举类型,用来表示创建结点的类型,包括有序、无序、持久、临时

单词中文含义
PERSISTENT(persistent)持久
EQUENTIAL(ephemeral)临时
SEQUENTIAL(sequential)有序
public enum CreateMode {PERSISTENT(0, false, false, false, false),              //持久PERSISTENT_SEQUENTIAL持久(2, false, true, false, false), //持久且有序EPHEMERAL(1, true, false, false, false),                //临时EPHEMERAL_SEQUENTIAL(3, true, true, false, false),      //临时且有序CONTAINER(4, false, false, true, false),                //容器结点PERSISTENT_WITH_TTL(5, false, false, false, true),      //带TTL的持久结点PERSISTENT_SEQUENTIAL_WITH_TTL(6, false, true, false, true); //带TTL的持久有序结点
}

测试代码:

@Test
public void testCreate2() throws Exception {//默认类型:持久化client.create().withMode(CreateMode.EPHEMERAL).forPath("/app2", "curator创建的临时结点".getBytes());
}

其他模式的结点同理可创建,这里不再赘述

创建多级结点

在传统的zkClient连接中是无法直接创建多级结点的,curator中可以直接创建,但如果多级结点中的父结点不存在会报错,curator

也为我们提供了相应的方法,这里我们app3本来是没有的,添加creatingParentsIfNeeded就自动创建了

@Test
public void testCreate3() throws Exception {client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/app3/app3_1", "curator递归创建结点".getBytes());
}

设置ACL

@Test
public void testCreate4() throws Exception {client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).withACL(ZooDefs.Ids.CREATOR_ALL_ACL).forPath("/app5", "curator创建的带所有访问权限临时结点".getBytes());
}

可以看到相比原生的API,curator链式调用的方式非常的优雅,并且在idea中还有提示

1.3 查询结点

查询节点:

  1. 查询数据:get: getData().forPath()
  2. 查询子节点: ls: getChildren().forPath()
  3. 查询节点状态信息:ls -s:getData().storingStatIn(状态对象).forPath()

相应的zkCli操作:

get [-s] [-w] path //查看结点存储的值及其结点状态
stat [-w] path //查看结点状态
ls [-s] [-w] [-R] path //查看某一结点下的子结点
查询数据

get /path

@Test
public void testQuery() throws Exception {byte[] data = client.getData().forPath("/app1");System.out.println(new String(data));
}
查询子结点

ls /path

@Test
public void testQuery2() throws Exception {List<String> list = client.getChildren().forPath("/app1");list.forEach(System.out::println);
}
查看结点信息

get -s /path

@Test
public void testQuery3() throws Exception {Stat status = new Stat(); //封装一个stat用来存储信息//查询节点状态信息:ls -sclient.getData().storingStatIn(status).forPath("/");System.out.println(status);
}

1.4 修改结点

修改数据

  1. 基本修改数据:setData().forPath()

  2. 根据版本修改: setData().withVersion().forPath()

    version 是通过查询出来的。目的就是为了让其他客户端或者线程不干扰我。

普通修改
@Test
public void testUpdate() throws Exception {client.setData().forPath("/","修改数据".getBytes());
}
带乐观锁的修改
@Test
public void testUpdateForVersion() throws Exception {//先要查出版本号Stat status = new Stat();client.getData().storingStatIn(status).forPath("/");int version = status.getVersion();//版本号不一样会导致修改失败client.setData().withVersion(version).forPath("/","修改数据".getBytes());
}

1.5 删除

删除节点: delete deleteall

  1. 删除单个节点:delete().forPath(“/app1”);
  2. 删除带有子节点的节点:delete().deletingChildrenIfNeeded().forPath(“/app1”);
  3. 必须成功的删除:为了防止网络抖动。本质就是重试。 client.delete().guaranteed().forPath(“/app2”);
  4. 回调:inBackground
删除单个结点
@Test
public void testDelete() throws Exception {client.delete().forPath("/app1");
}
删除带子结点的结点
@Test
public void testDeleteAll() throws Exception {client.delete().deletingChildrenIfNeeded().forPath("/app1");
}
必须成功的删除

主要是为了防止网络抖动

@Test
public void testDeleteAll() throws Exception {client.delete().guaranteed().forPath("/app1");
}
带回调函数的删除
@Test
public void testDeleteWithCallback() throws Exception {client.delete().guaranteed().inBackground(new BackgroundCallback() {@Overridepublic void processResult(CuratorFramework client, CuratorEvent event) throws Exception {//删除结点后的业务逻辑代码System.out.println("我被删除了~");System.out.println(event);}}).forPath("/app1");
}

2. 监听器事件

我们知道在zookeeper中,监听器是实现其特性的重要保障,而传统的zkClient代码有些繁琐,通过Curator我们可以写出更流畅的代码

Curator引入了Cache来实现对ZooKeeper服务端事件的监听

Zookeeper提供了三种Watcher

  • NodeCache:只是监听某一个特定的节点
  • PathChildrenCache:监控一个ZNode的子节点.
  • TreeCache:可以监控整个树上的所有节点,类似于PathChildrenCache和NodeCache的组合

2.1 NodeCache单一结点连续监听

zkClient中我们知道,监听事件只能绑定一次(这个是zookeeper的性质),我们为了持续监听一个结点,通常做法是在监听事件完后再给结点绑定一次监听,而CuratorNodeCache为我们提供了持续监听的方法

@Test
public void testNodeCache() throws Exception {//countDownLatch为了堵塞主线程,不然主线程执行完了子线程也会结束CountDownLatch countDownLatch = new CountDownLatch(Integer.MAX_VALUE);//1. 创建NodeCache对象NodeCache nodeCache = new NodeCache(client, "/node1");//2. 注册监听nodeCache.getListenable().addListener(new NodeCacheListener() {//能够监听到结点增、删、改,且可以连续监听@Overridepublic void nodeChanged() throws Exception {System.out.println("结点修改了");//获取修改后的结点byte[] data = nodeCache.getCurrentData().getData();System.out.println(new String(data));countDownLatch.countDown();}});//3. 开启监听,如果设置为true,则开启监听时,加载缓存数据nodeCache.start(true);countDownLatch.await();
}

2.2 PathChildrenCache监听子结点

注意PathChildrenCache只会监听子结点,当前结点的变化是监听不到的

@Test
public void testPathChildrenCache() throws Exception {//countDownLatch为了堵塞主线程,不然主线程执行完了子线程也会结束CountDownLatch countDownLatch = new CountDownLatch(Integer.MAX_VALUE);//1.创建监听对象//第三个参数表示缓存每次结点更新后的数据PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/node2", true);//2. 绑定监听器pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {@Overridepublic void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {System.out.println("子结点变化了");System.out.println(event);if (PathChildrenCacheEvent.Type.CHILD_UPDATED == event.getType()) {//更新了子结点System.out.println("子结点更新了");//第一个getData里有很多数据,我们只拿data部分byte[] data = event.getData().getData();System.out.println("更新后的值为:" + new String(data));} else if (PathChildrenCacheEvent.Type.CHILD_ADDED == event.getType()) {//添加了子结点System.out.println("添加了子结点");String path = event.getData().getPath();System.out.println("子结点路径为" + path);} else if (PathChildrenCacheEvent.Type.CHILD_REMOVED == event.getType()) {//删除了子结点System.out.println("删除了子结点");String path = event.getData().getPath();System.out.println("子结点路径为" + path);}countDownLatch.countDown();}});// 3. 开启监听pathChildrenCache.start();// 堵塞主线程countDownLatch.await();
}

2.3 TreeCache监听当前结点+子结点

TreeCache相当于NodeCache(只监听当前结点)+ PathChildrenCache(只监听子结点)的结合版,即监听当前和子结点

@Test
public void testPathTreeCache() throws Exception {//countDownLatch为了堵塞主线程,不然主线程执行完了子线程也会结束CountDownLatch countDownLatch = new CountDownLatch(Integer.MAX_VALUE);//1. 创建监听器TreeCache treeCache = new TreeCache(client, "/node2");//2. 注册监听treeCache.getListenable().addListener(new TreeCacheListener() {@Overridepublic void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {System.out.println("结点变化了");System.out.println(event);if (TreeCacheEvent.Type.NODE_UPDATED == event.getType()) {//更新了子结点System.out.println("结点更新了");//第一个getData里有很多数据,我们只拿data部分byte[] data = event.getData().getData();System.out.println("更新后的值为:" + new String(data));} else if (TreeCacheEvent.Type.NODE_ADDED == event.getType()) {//添加了子结点System.out.println("添加了结点");String path = event.getData().getPath();System.out.println("子结点路径为" + path);} else if (TreeCacheEvent.Type.NODE_REMOVED == event.getType()) {//删除了子结点System.out.println("删除了结点");String path = event.getData().getPath();System.out.println("删除结点路径为" + path);}countDownLatch.countDown();}});//3. 开启监听treeCache.start();countDownLatch.await();
}

3. Curator 分布式锁

分布式锁:

  • 在我们进行单机应用开发,涉及并发同步的时候,我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题,这时多线程的运行都是在同一个JVM之下,没有任何问题
  • 但当我们的应用是分布式集群工作的情况下,属于多VM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题
  • 那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题—— 这就是分布式锁

常见的分布式锁实现有:

  • 基于缓存实现(redis、Memcache)
  • 基于zookeeper(Curator )
  • 数据库层面(悲观锁、乐观锁)

基于缓存虽然性能高,但是不可靠,因为redis集群保证的AP,基于数据库层面的性能太低了,不推荐

现在的最优解是基于zookeeper,zookeeper保证的CP,即强一致性,能够保证锁的功能

3.1 zookeeper分布式锁原理

核心思想(临时、顺序、监听):

当客户端要获取锁,则创建结点,使用完锁,则删除结点

  1. 客户端获取锁时,在lock结点下创建临时顺序结点
  2. 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁使用完锁后,将该节点删除
  3. 如果发现自己创建的结点并非lock所有子结点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个结点,同时对其注册事件监听器,监听删除事件
  4. 如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。

常见面试题:

  • 为什么是临时结点?答:防止某个结点宕机而导致锁一直不释放的问题
  • 为什么是顺序结点? 答:公平锁,为了寻找最小结点从而获取锁

3.2 Curator 封装的五种分布式锁

Curator 为我们封装了五种常用的分布式锁

  1. InterProcessSemaphoneMutex:分布式排他锁(非可重入锁)
  2. InterProcessMutex:分布式可重入排他锁
  3. InterProcessReadWriteLock:分布式读写锁
  4. InterProcessMultiLock:将多个锁作为单个实体管理的容器
  5. InterProcessSemaphoreV2:共享信号量

熟悉JUC的同学看到这些名词应该会感到很熟悉,Curator 只是将单机的锁改成了分布式锁,其作用同样是保证共享变量的安全问题

下面我们结合具体的业务场景来分析一下分布式锁的

3.3 模拟12306卖票实现可重入锁

image-20220519211027529

首先我们知道很多用户都会访问12306的同一份共享资源(票),所以我们的分布式锁肯定是加在共享资源上的

我们先封装一个资源类,用来模拟共享资源

public class Ticket12306 implements Runnable{private int tickets = 10;//数据库里的票数@Overridepublic void run() {while (tickets > 0){System.out.println(Thread.currentThread().getName() + ":" + tickets);tickets--;}}
}

当我们的客户端卖票时如果不加锁肯定是会有票超卖的问题的

public class DistributedLock {public static void main(String[] args) {Ticket12306 ticket12306 = new Ticket12306();//创建客户端Thread t1 = new Thread(ticket12306, "飞猪");Thread t2 = new Thread(ticket12306, "携程");Thread t3 = new Thread(ticket12306, "去哪儿");//开始抢票t1.start();t2.start();t3.start();}
}

运行结果

飞猪:10
飞猪:9
飞猪:8
飞猪:7
飞猪:6
飞猪:5
飞猪:4
飞猪:3
飞猪:2
飞猪:1
去哪儿:10
携程:10

使用Curator封装好的锁非常简单,我们这里先演示一个 InterProcessMutex:分布式可重入排他锁的,对应单机中的SynchronizedReentrantLock

public class Ticket12306 implements Runnable {private int tickets = 10;//数据库里的票数private InterProcessMutex lock; //分布式可重入排他锁public Ticket12306() {String url = "zookeeper的ip地址:端口号";//重试策略,每隔3秒重试一次,最多重试10次ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(3000, 10);CuratorFramework client = CuratorFrameworkFactory.builder().connectString(url).sessionTimeoutMs(60 * 1000).connectionTimeoutMs(15 * 1000).retryPolicy(retryPolicy).build();//开启连接zookeeperclient.start();lock = new InterProcessMutex(client,"/lock");}@Overridepublic void run() {//获取锁,三秒钟获取一次,如果没有获取到需要等三秒再获取try {lock.acquire(3, TimeUnit.SECONDS);while (tickets > 0) {System.out.println(Thread.currentThread().getName() + ":" + tickets);tickets--;}} catch (Exception e) {e.printStackTrace();} finally {//释放锁,注意锁无论如何都要释放掉try {lock.release();} catch (Exception e) {e.printStackTrace();}}}
}

每一个线程执行其实就对应一个新的zookeeper连接,这里其实已经模拟出了多客户端争抢锁的情况,我们从结果来看其实问题已经的到了解决

去哪儿:10
去哪儿:9
去哪儿:8
去哪儿:7
去哪儿:6
去哪儿:5
去哪儿:4
去哪儿:3
去哪儿:2
去哪儿:1

但是由于我们设置的是InterProcessMutex:分布式可重入排他锁,所以导致由同一个客户端多次抢到锁的现象发生

3.4 读写锁InterProcessReadWriteLock

和JUC里面的一样,主要核心思想是读锁共享,写锁排他

  • 读锁:大家都可以读,要想上读锁的前提:之前的锁没有写锁
  • 写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁

和JUC里一样使用即可,Curator 底层都已经封装好了

private InterProcessReadWriteLock readWriterLock;
readWriterLock.readLock().acquire();
readWriterLock.readLock().release();
readWriterLock.writeLock().acquire();
readWriterLock.writeLock().release();

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

“从根到叶:深入理解排序数据结构“

一.排序的概念及引用 1.1排序的概念 排序是指将一组数据按照一定的规则重新排列的过程。排序的目的是为了使数据具有有序性&#xff0c;便于查找、插入、删除等操作&#xff0c;提高数据的组织和管理效率。 稳定性是指如果序列中存在相等元素&#xff0c;在排序完成后&#…

300分钟吃透分布式缓存-12讲:为何MC能长期维持高性能读写?

内存管理 slab 机制 讲完淘汰策略&#xff0c;我们接下来学习内存管理 slab 机制。 Mc 内存分配采用 slab 机制&#xff0c;slab 机制可以规避内存碎片&#xff0c;是 Mc 能持续高性能进行数据读写的关键。 slabclass Mc 的 slab 机制是通过 slabclass 来进行运作的&#x…

程序媛的mac修炼手册-- 小白入门Java篇

最近因为要用CiteSpace做文献综述&#xff0c;间接接触Java了。所以&#xff0c;继Python、C之后&#xff0c;又要涉猎Java了。刺激&#xff01;&#xff01; 由于CiteSpace与Java要求版本高度匹配&#xff0c;有个匹配详情明天为大家讲解。总之&#xff0c;我的Java之旅开始于…

冲击大厂算法面试=>链表专题【链表删除】

冲击大厂算法面试>链表专题【链表删除】 本文学习目标或者巩固的知识点 学习如何删除链表中的某个节点 如何删除valk的节点如何删除倒数第n个节点 学习如何删除链表中的某些节点 涉及头节点问题如何解决 提前说明&#xff1a;算法题目来自力扣、牛客等等途径 &#x1f7e…

java课设之简易版客房管理系统(mvc三层架构)

&#xff08;一&#xff09;、系统概述&#xff1a; 客房管理系统是一个用于管理酒店客房信息的程序&#xff0c;主要功能包括客房信息录入、客房状态查询、客房订单管理&#xff0c;客房的预定功能。 &#xff08;二&#xff09;、功能说明&#xff1a; 1.登录&#xff1a;管理…

【Pytorch】从MoCo看无监督对比学习;从SupCon看有监督对比学习

目录 无监督对比学习&#xff1a;Moco文章内容理解代码解释 有监督对比学习&#xff1a;Supervised Contrastive Learning文章内容理解 无监督对比学习&#xff1a;Moco 文章内容理解 以下内容全部来自于&#xff1a;自监督学习-MoCo-论文笔记. 侵删 论文&#xff1a;Momentu…

一种基于javax.max注解的能力增强技术

目录 现有框架的不足之处 我的改进内容 改进的成果 现有框架的不足之处 Max是javax.validation包中的一个常用注解&#xff0c;用于对传入参数进行最大值校验。但是其校验区间为闭区间&#xff0c;且不支持修改&#xff0c;如&#xff1a;Max(2)&#xff0c;表示表示传入参…

【解决(几乎)任何机器学习问题】:特征选择

当你创建了成千上万个特征后&#xff0c;就该从中挑选出⼏个了。但是&#xff0c;我们绝不应该创建成百上千个⽆⽤的特征。特征过多会带来⼀个众所周知的问题&#xff0c;即 "维度诅咒"。如果你有很多特征&#xff0c;你也必须有很多训练样本来捕捉所有特征。什么是 …

DC与DCT DCG的区别

先进工艺不再wire load model进行静态时序分析&#xff0c;否则综合结果与后端物理电路差距很大&#xff0c;因此DC综合工具也进行了多次迭代&#xff0c;DC工具有两种模式&#xff0c;包括wire load mode和Topographical Mode&#xff0c;也就是对应的DC Expert和DC Ultra。 …

JavaScript从零写网站《一瞬》开发日志20240223

产品介绍 一个无需注册能随时发布图片并配一段文字介绍的app&#xff0c;有时间线&#xff0c;用户在主页面向下滑动&#xff0c;可以看到被发布的若干图片&#xff0c;并且能够在每一个发布处做基本互动——评论&#xff0c;点赞 编程语言 本产品使用htmlcssJavaScript开发…

【数据结构】每天五分钟,快速入门数据结构(二)——链表

目录 一 构建一个单向链表 二 特点 三 时间复杂度 四 相关算法 1.判断链表是否成环及成环位置 2.链表反转 五 Java中的LinkedList 类 1.使用 2.LinkedList 方法 一 构建一个单向链表 // 设计链表结构class ListNode {int val;ListNode next;ListNode(){}ListNode(int…

《UE5_C++多人TPS完整教程》学习笔记24 ——《P25 完善菜单子系统(Polishing The Menu Subsystem)》

本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P25 完善菜单子系统&#xff08;Polishing The Menu Subsystem&#xff09;》 的学习笔记&#xff0c;该系列教学视频为 Udemy 课程 《Unreal Engine 5 C Multiplayer Shooter》 的中文字幕翻译版&#xff0c;UP主&…

TongWEB(东方通),部署WEB前后端项目步骤

我的系统: 银河麒麟桌面系统V10(SP1)(兆芯) 环境需要搭建好,什么redis,数据库等 1.准备项目前端war包 (我后端项目本就是war部署,jar转war自行百度一下吧) 进入前端打包好的dist文件夹,创建一个文件夹 WEB-INF ,再在 WEB-INF 里创建一个 web.xml 文件,文件内容: <web-…

Linux——简单的Shell程序

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、Shell程序思路二、Shell代码展示 一、Shell程序思路 用下图的时间轴来表示事件的发生次序…

经典Go知识点总结

开篇推荐 来来来,老铁们,男人女人都需要的技术活 拿去不谢:远程调试,发布网站到公网演示,远程访问内网服务,游戏联机 推荐链接 1.无论sync.Mutex还是其衍生品都会提示不能复制,但是能够编译运行 加锁后复制变量&#xff0c;会将锁的状态也复制&#xff0c;所以 mu1 其实是已…

【JVM】Java中SPI机制

打破双亲委派模型中提到SPI和JDBC相关内容&#xff0c;那么是如何打破双亲委派模型呢?本文进行一个讲解&#xff0c;在开始讲解之前&#xff0c;我们需要先了解Java中的SPI机制 是什么 SPI 全称Service Provider Interface&#xff0c;是 Java 提供的一套用来被第三方实现或…

python jupyter notebook打开页面方便使用

如果没安装jupyter, 请安装&#xff1a; pip install jupyter notebook 运行jupyter notebook jupyter notebook

“政务服务+AI交互数字人”,重新定义政务服务体验

随着AIGC发展&#xff0c;各地方政务部门纷纷通过AI交互数字人技术&#xff0c;提升企业和群众的办事效率、满意度&#xff0c;以数字人有效推动政务服务数字化、智能化发展。 *图片源于网络 如高新区将数字人海蓝作为政务服务大使&#xff0c;让数字人化身AI交互数字人可以面…

k8s-heml联动harbor 18

将打包的heml包上传到harbor仓库进行管理 创建一个公开的项目来接收传送的heml包 安装helm-push插件&#xff1a; helm plugin install https://github.com/chartmuseum/helm-push &#xff08;在线安装&#xff0c;要求网速要快或者提供科学上网&#xff09; 离线安装&…

Ansible 简介及部署 基础模块学习 ansible部署rsync 及时监控远程同步

Ansible介绍&#xff1a; Ansible 是一个配置管理系统&#xff0c;当下最流行的批量自动化运维工具之一&#xff0c;它是一款开源的自动化工具&#xff0c;基于Python开发的配置管理和应用部署的工具。 Ansible 是基于模块工作的&#xff0c;它只是提供了一种运行框架&#xff…