Zookeeper4:Java客户端、应用场景以及实现、第三方客户端curator工具包

文章目录

    • Java连接Zookeeper服务端
      • 依赖
      • 代码使用
    • 应用场景
      • 统一命名服务
      • 统一配置管理
      • 统一集群管理
      • 服务器节点动态上下线
        • 理解
        • 实现
          • 模拟服务提供者【客户端代码】-注册服务
          • 模拟服务消费者【客户端代码】-获取服务信息进行请求消费
      • 软负载均衡
      • 分布式锁
        • 理解
        • 实现
      • 生产集群安装N台机器合适
    • 第三方基于zookeeper的包
      • curator
        • 依赖
        • 代码

Java连接Zookeeper服务端

文档: https://zookeeper.apache.org/doc/r3.9.1/javaExample.html

依赖

依赖

  <dependencies><!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper --><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.9.1</version></dependency><!-- https://mvnrepository.com/artifact/org.dromara.hutool/hutool-all --><dependency><groupId>org.dromara.hutool</groupId><artifactId>hutool-all</artifactId><version>6.0.0-M11</version></dependency><!-- https://mvnrepository.com/artifact/junit/junit --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.10.2</version></dependency><!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><scope>provided</scope></dependency></dependencies>

代码使用

Java代码

public class ZkClient {@Test@SneakyThrowspublic void test1() {String connectString = "192.168.19.107:2181"; // zookeeper服务端信息int sessionTimeout = 2000; // 连接最大时长(毫秒)ZooKeeper zooKeeperClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {public void process(WatchedEvent event) {Console.log("服务端推送给客户端的监听事件信息 == {}", event);}});// 监听节点数据的变化 === 等价于get -w 命令try {String s = zooKeeperClient.create("/test", "testData".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);Console.log("创建节点成功:{}", s);} catch (Exception e) {Console.log("创建节点失败:{}", e.getMessage());}// 启动监听增删子节点的变化,然后在前面【Watcher】能收到监听事件 === 等价于ls -w 命令List<String> children = zooKeeperClient.getChildren("/test", true);Console.log("Zookeeper服务端当前/test所有的子节点名字:{}", children);//启动节点的状态信息变化 === 等价于stat -w 命令Stat statInfo = zooKeeperClient.exists("/test", true);Console.log("Zookeeper服务端当前/test节点的状态信息:{}" , statInfo);//程序永远不结束while (true) {try {Thread.sleep(1000); // 暂停1秒钟} catch (InterruptedException e) {e.printStackTrace();}}}}

1. 前提背景
在这里插入图片描述

2. 开始执行代码
在这里插入图片描述

3. 命令增加节点
在这里插入图片描述

4. Java客户端监听到的消息
在这里插入图片描述

应用场景

统一命名服务

对应用、服务同意命名便于识别,比如一个对外服务接口的多集群,则需要统一的管理同一服务的所有IP

在这里插入图片描述

统一配置管理

  • 场景:
  1. 一般要求一个集群中,所有节点的配置信息是一致的,比如Kafka集群。
  2. 对配置文件修改后,希望能够快速同步到各个节点上
  3. 实现:配置信息写入到Zookeeper一个节点中,客户端监听这个节点即可

在这里插入图片描述

统一集群管理

  • 场景:
  1. 分布式环境,实时掌握每个节点状态是必要的
  2. 实现: 节点信息写入ZooKeeper_上的一个ZNode。客户端监听这个ZNode可获取它的实时状态变化

在这里插入图片描述

服务器节点动态上下线

理解

特点: 客户端能实时洞察到服务器上下线的变化

在这里插入图片描述

实现

前提: 运行代码前自行在Zookeeper客户端创建/service节点【create /service “service”】,因为zookeeper创建子节点前必须有父节点,否则创建子节点失败

在这里插入图片描述

模拟服务提供者【客户端代码】-注册服务
public class ServiceProviderZkClient {private static String connecting = StrUtil.join(StrUtil.COMMA,"192.168.19.107:2181","192.168.19.108:2181","192.168.19.109:2181");private static Integer timeout = 2000;@SneakyThrowspublic static void main(String[] args) {Arrays.asList("application1","application2","application3").stream().parallel().forEach(applicationName -> {serviceRegister(applicationName);});}@SneakyThrowspublic static void serviceRegister(String applicationName) {ZooKeeper zooKeeper = new ZooKeeper(connecting, timeout, new Watcher() {public void process(WatchedEvent event) {Console.log("服务端推送的监听信息:{}", event);}});String zookeeperPath = StrUtil.format("/service/{}", applicationName);byte[] zookeeperPathData = Convert.toPrimitiveByteArray(StrUtil.format("{}应用的IP地址等信息", applicationName));String newNodePath = zooKeeper.create(zookeeperPath, zookeeperPathData, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);Console.log("【{}】在线" , applicationName);//程序永远不结束while (true) {try {Thread.sleep(1000); // 暂停1秒钟} catch (InterruptedException e) {e.printStackTrace();}}}}
模拟服务消费者【客户端代码】-获取服务信息进行请求消费
public class ServiceProviderConsumerZkClient {private static String connecting = StrUtil.join(StrUtil.COMMA,"192.168.19.107:2181","192.168.19.108:2181","192.168.19.109:2181");private static Integer timeout = 2000;private static AtomicReference children = new AtomicReference(ListUtil.of());private static ZooKeeper zooKeeper = null;@SneakyThrowspublic static void main(String[] args) {zooKeeper = new ZooKeeper(connecting, timeout, new Watcher() {public void process(WatchedEvent event) {Console.log("服务端推送的监听信息:{}", event);//每次收到监听通知消息,同步服务在线状态getServiceNode();}});//获取在线中的提供提供者getServiceNode();while (true) {String targetServiceName = "application2";//在线的服务真实路径String targetServiceNodeName = CollUtil.emptyIfNull((List<String>)children.get()).stream().filter(childrenPath -> StrUtil.contains(childrenPath, targetServiceName)).findFirst().orElse(null);String targetServiceNamePath = StrUtil.format("/service/{}" , targetServiceNodeName);boolean targetServiceNameExistFlag = StrUtil.isNotBlank(targetServiceNodeName);if(targetServiceNameExistFlag) {//获取服务的配置信息进行服务调用 == 节点里面一般包含当前服务提供者http,端口等等信息String nodeData = Convert.toStr(zooKeeper.getData(targetServiceNamePath, false, null));Console.log("【{}】第三方服务上线,调用接口成功", targetServiceName);}else {Console.log("【{}】第三方服务未上线,调用接口失败" , targetServiceName);}ThreadUtil.sleep(5000);}}@SneakyThrowspublic static void getServiceNode() {children.set(zooKeeper.getChildren("/service", true));Console.log("系统中的服务提供者节点:{}" , children.get());}}

服务提供者进行注册服务时
在这里插入图片描述

服务消费者进行消费时
在这里插入图片描述

软负载均衡

特点: 在Zookeepert中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求

在这里插入图片描述

分布式锁

理解

概念: 分布式系统中能保证多个进程有序地进行访问临界资源的锁,拿到锁的进程才可以访问资源,否则一直排队等待锁

在这里插入图片描述

实现
public class DistributedLock {private static String connecting = StrUtil.join(StrUtil.COMMA, "192.168.19.107:2181", "192.168.19.108:2181", "192.168.19.109:2181");private static Integer timeout = 2000;private static ZooKeeper zooKeeper;private static String parentPath = "/DistributedLock";private static Map<String, CountDownLatch> threadIdToCountDownLatchMap = MapUtil.newSafeConcurrentHashMap();static {init();}@SneakyThrowsprivate static void init() {zooKeeper = connectZooKeeper();// 创建锁父节点String realParentPath = zooKeeper.create(parentPath, Convert.toPrimitiveByteArray(parentPath), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);// 监听父节点下子节点增删的变化// List<String> sonNodeNames = getParentSortedNodes(true);}@SneakyThrowsprivate static ZooKeeper connectZooKeeper() {ZooKeeper connectedZooKeeper = new ZooKeeper(connecting, timeout, new Watcher() {@Overridepublic void process(WatchedEvent event) {Console.log("zookeeper收到服务端监听通知消息:{}", event);String dealNodePath = event.getPath();Event.EventType eventType = event.getType();if (eventType == Event.EventType.NodeDeleted) {String nodeBelongThreadId = StrUtil.subBefore(StrUtil.subAfter(dealNodePath, "/", true), "_", false);Console.log("收到删除节点通知,释放线程等待 == {}", nodeBelongThreadId);// 释放锁CountDownLatch countDownLatch = threadIdToCountDownLatchMap.get(nodeBelongThreadId);if (countDownLatch != null) {countDownLatch.countDown();threadIdToCountDownLatchMap.remove(nodeBelongThreadId);}}}});return connectedZooKeeper;}@SneakyThrowspublic static List<String> getParentSortedNodes(Boolean watchFlag) {List<String> sonNodeNames = CollUtil.emptyIfNull(zooKeeper.getChildren(parentPath, true)).stream().sorted(CompareUtil::compare).collect(Collectors.toList());return sonNodeNames;}/*** 获取锁*/@SneakyThrowspublic static void acquireLock() {// 当前锁的节点前缀String nodeNamePrefix = Thread.currentThread().getId() + "_";// 当前锁的节点完整领前缀String absolutenodeNamePathPrefix = StrUtil.format("{}/{}", parentPath, nodeNamePrefix);// 完整的前缀String realAbsolutenodeNamePath = zooKeeper.create(absolutenodeNamePathPrefix, Convert.toPrimitiveByteArray("absolutenodeNamePathPrefix"), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);// 获取当前父路径下的所有节点List<String> sonNodeNames = getParentSortedNodes(false);if (CollUtil.size(sonNodeNames) == 1) { // 获取锁Console.log("【子元素为1】获取锁【{}】",realAbsolutenodeNamePath);return;} else {String firstNodeName = CollUtil.getFirst(sonNodeNames);if (StrUtil.startWith(firstNodeName, nodeNamePrefix)) {Console.log("【当前子节点在排序后序列为第一个】获取锁【{}】",realAbsolutenodeNamePath);return;} else {// 一直等待直到有机会获取锁CountDownLatch countDownLatch = new CountDownLatch(1);// 监听该节点的前一个节点的增删变化int currentNodeIndex = CollUtil.indexOf(sonNodeNames, nodeName -> StrUtil.endWith(realAbsolutenodeNamePath, nodeName));String previousNodeName = CollUtil.get(sonNodeNames, currentNodeIndex - 1);String previousNodePath = StrUtil.format("{}/{}", parentPath, previousNodeName);zooKeeper.getData(previousNodePath, true, null);threadIdToCountDownLatchMap.put(StrUtil.subBefore(previousNodeName,"_", false), countDownLatch);countDownLatch.await();Console.log("获取锁【{}】", realAbsolutenodeNamePath);}}}/*** 释放锁*/@SneakyThrowspublic static void releaseLock() {String currentThreadId = Convert.toStr(Thread.currentThread().getId());// CountDownLatch countDownLatch = threadIdToCountDownLatchMap.get(currentThreadId);// if (ObjUtil.isNull(countDownLatch)) {//     Console.log("当前线程并没有等待锁的操作");//     return;// }// 当前锁的节点前缀String nodeNamePrefix = currentThreadId + "_";String realNodeName = getParentSortedNodes(false).stream().filter(nodeName -> StrUtil.startWith(nodeName, nodeNamePrefix)).findFirst().orElse(null);if (StrUtil.isBlank(realNodeName)) {Console.log("当前线程并未有获取锁的操作");return;}String completeNodePath = StrUtil.format("{}/{}", parentPath, realNodeName);zooKeeper.delete(completeNodePath, -1);Console.log("释放锁【{}】", completeNodePath);}public static void main(String[] args) {String s = StrUtil.subAfter("fsd/fdsfsdfds", "/", true);Console.log(s);}
}


public class App2Test {@Test@SneakyThrowspublic void test3() {Bean publicBean = new Bean();List<Thread> threadGroup = ListUtil.of();for (int i = 0; i < 10; i++) {Thread newThread = new Thread(() -> {DistributedLock.acquireLock();// 随机等待try {Thread.sleep(RandomUtil.randomInt(1000, 3000));} catch (InterruptedException e) {throw new RuntimeException(e);}publicBean.num = ++publicBean.num;Console.log("【{}】:数+1处理 == {}", Thread.currentThread().getId(), publicBean.num);DistributedLock.releaseLock();});newThread.start();threadGroup.add(newThread);}for (Thread runThread : threadGroup) {// 等待线程运行完runThread.join();}Console.log("公共数据最终的结果:{}", publicBean.num);Assert.equals(publicBean.num, 10);}
}

在这里插入图片描述

生产集群安装N台机器合适

特点: 好处提高可靠性、坏处数据同步有延迟

案例(生产经验)
10台服务器:3个ZK
20台服务器:5个ZK
100台服务器:11个zk
200台服务器:11台zk

第三方基于zookeeper的包

curator

官网: https://curator.apache.org/docs/about

入门教程: https://curator.apache.org/docs/getting-started/

依赖
    <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework --><dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>5.6.0</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes --><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>5.6.0</version></dependency><!-- https://mvnrepository.com/artifact/org.apache.curator/curator-client --><dependency><groupId>org.apache.curator</groupId><artifactId>curator-client</artifactId><version>5.6.0</version></dependency>
代码

发现: 运行代码可见curator的分布式锁的原理跟前面自己实现的逻辑差不多,都是通过增、删子节点,然后监控前一个节点被删释放锁的逻辑原理去做的

public class OtherTest {@Test@SneakyThrowspublic void test2() {String connectString = "192.168.19.107:2181,192.168.19.108:2181,192.168.19.109:2181";RetryOneTime retryOneTime = new RetryOneTime(2000);CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(connectString, 60 * 1000 * 10, 15 * 1000 * 10, retryOneTime);curatorFramework.start();//获取某父目录旗下的亲儿子节点名字信息List<String> sonNodeNames = curatorFramework.getChildren().forPath("/");Console.log(sonNodeNames);// 分布式锁InterProcessMutex interProcessMutexLock = new InterProcessMutex(curatorFramework, "/CuratorLock");App2Test.Bean publicBean = new App2Test.Bean();List<Thread> threadGroup = ListUtil.of();for (int i = 0; i < 5; i++) {Thread newThread = new Thread(() -> {try {interProcessMutexLock.acquire();// 随机等待try {Thread.sleep(RandomUtil.randomInt(1000, 3000));} catch (InterruptedException e) {throw new RuntimeException(e);}publicBean.num = ++publicBean.num;Console.log("【{}】:数+1处理 == {}", Thread.currentThread().getId(), publicBean.num);}catch (Exception e) {}finally {try {interProcessMutexLock.release();}catch (Exception e) {}}});newThread.start();threadGroup.add(newThread);}for (Thread runThread : threadGroup) {// 等待线程运行完runThread.join();}Console.log("公共数据最终的结果:{}", publicBean.num);Assert.equals(publicBean.num, 5);}}

在这里插入图片描述

在这里插入图片描述


刚兴趣的同行可以进群沟通交流,内置机器人供大家愉快

在这里插入图片描述

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

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

相关文章

Java中的Collection

Collection Collection 集合概述和使用 Collection集合概述 是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素 JDK 不提供此接口的任何直接实现.它提供更具体的子接口(如Set和List)实现 创建Collection集合的对象 多态的方式 具体的实现类ArrayList C…

MATLAB环境下基于熵的声纳图像分割算法

声纳图像作为准确获取水下信息的重要途径之一&#xff0c;在国防、军事、工程等方面发挥着巨大作用。然而&#xff0c;由于水声信道的复杂多变和声波本身的传播损失&#xff0c;声纳图像往往呈现出分辨率和对比度不高、噪声干扰严重、目标轮廓模糊等特点。 声纳图像的分割指的…

FCIS 2023网络安全创新大会:洞察前沿技术,探索安全新境界(附大会核心PPT下载)

随着信息技术的飞速发展&#xff0c;网络安全问题日益凸显&#xff0c;成为全球关注的焦点。作为网络安全领域的重要盛会&#xff0c;FCIS 2023网络安全创新大会如期而至&#xff0c;汇聚了全球网络安全领域的顶尖专家、学者、企业家和政策制定者&#xff0c;共同探讨网络安全的…

ABAP - SALV教程10 添加可编辑checkbox列

几乎所有的功能报表都会有那么一个选择列&#xff0c;问了业务顾问&#xff0c;业务顾问说是用户不习惯使用报表原生的选择模式。效果图SALV的选择列是通过将列设置成checkbox_hotspot样式&#xff0c;注册单击事件完成勾选功能的。完成步骤 将SEL列设置成checkbox_hotspot样式…

【笔记】OpenHarmony和HarmonyOS区别及应用开发简介

一、概念 OpenHarmony(OH) &#xff1a; OpenAtom OpenHarmonyHarmonyOS(HO)&#xff1a;开发 | 华为开发者联盟 (huawei.com) HO当前最高是3.1&#xff0c;在华为mate 60上面也是。关于4.0、5.0和next这类版本说法都是面向用户的&#xff0c;不是开发人员。对于程序员&#…

Springboot 项目读取yaml的配置文件信息给静态方法使用,以及通过配置 ResourceBundle 类读取config.properties

读取yaml 的配置文件 配置文件信息 iot_saas_tenement:user_id: 7........8d9bprivate_key: MII.......qQbj_url: http://4.....5:8088project_name: iot_s.......rojectdevice_name: te.....ice 创建一个类 ProxyProperties 读取配置文件信息&#xff0c;并对外提供get方法 …

内存的检测与排查

内存&#x1f40e;的检测与排查 文章目录 内存&#x1f40e;的检测与排查查杀Java Web filter型内存马0x01 内存马简历史0x02 查杀思路0x03 内存马的识别0x04 内存马的查杀 查杀Java Web filter型内存马 0x01 内存马简历史 其实内存马由来已久&#xff0c;早在17年n1nty师傅的…

QT6 libModbus 用于ModbusTcp客户端读写服务端

虽然在以前的文章中多次描述过,那么本文使用开源库libModbus,可得到更好的性能&#xff0c;也可移植到各种平台。 性能&#xff1a;读1次和写1次约各用时2ms。 分别创建了读和写各1个连接指针&#xff0c;用于读100个寄存器和写100个寄存器&#xff0c;读写分离。 客户端&am…

物联网与智慧城市:科技驱动下的城市智能化升级之路

一、引言 随着科技的不断进步和城市化进程的加速&#xff0c;物联网与智慧城市的结合已经成为推动城市智能化升级的关键力量。物联网技术以其强大的连接和数据处理能力&#xff0c;为智慧城市的建设提供了无限可能。本文旨在探讨物联网如何助力智慧城市的构建&#xff0c;以及…

SLAM ORB-SLAM2(21)基础矩阵的计算和评分

SLAM ORB-SLAM2&#xff08;21&#xff09;基础矩阵的计算和评分 1. 前言2. 基础矩阵2.1. 对级约束2.2. 推导2.3. 计算原理 3. ComputeF214. CheckFundamental 1. 前言 在 《SLAM ORB-SLAM2&#xff08;20&#xff09;查找基础矩阵》 中了解到 查找基础矩阵主要过程&#xff1…

web基础03-JavaScript

目录 一、JavaScript基础 1.变量 2.输出 3.变量提升 4.区块 5.JavaScript数据类型 6.查看数值类型 7.undefined和null 8.布尔值 9.和的区别 10.算数/三元/比较/逻辑/赋值运算符 11.特殊字符 12.字符串 &#xff08;1&#xff09;获取字符串长度 &#xff08;2&am…

备战蓝桥杯Day21 - 堆排序的内置模块+topk问题

一、内置模块 在python中&#xff0c;堆排序已经设置好了内置模块&#xff0c;不想自己写的话可以使用内置模块&#xff0c;真的很方便&#xff0c;但是堆排序算法的底层逻辑最好还是要了解并掌握一下的。 使用heapq模块的heapify()函数将列表转换为堆&#xff0c;然后使用he…

41、网络编程/TCP.UDP通信模型练习20240301

一、编写基于TCP的客户端实现以下功能&#xff1a; 通过键盘按键控制机械臂&#xff1a;w(红色臂角度增大)s&#xff08;红色臂角度减小&#xff09;d&#xff08;蓝色臂角度增大&#xff09;a&#xff08;蓝色臂角度减小&#xff09;按键控制机械臂 1.基于TCP服务器的机械臂…

Python3零基础教程之数学运算专题进阶

大家好,我是千与编程,今天已经进入我们Python3的零基础教程的第十节之数学运算专题进阶。上一次的数学运算中我们介绍了简单的基础四则运算,加减乘除运算。当涉及到数学运算的 Python 3 刷题使用时,进阶课程包含了许多重要的概念和技巧。下面是一个简单的教程,涵盖了一些常…

勒索软件类型

勒索软件类型 加密勒索软件 它使个人文件和文件夹&#xff08;文档、电子表格、图片和视频&#xff09;被加密。受感染的文件被加密后会被删除&#xff0c;用户通常会在当下无法使用的文件的文件夹中看到一个包含付款说明的文本文件。当您尝试打开其中一个加密文件时,您才可能…

Tomcat负载均衡、动静分离

目录 引言 实验图解 1.实验环境搭建 2.部署Nginx服务器及配置静态页面Web服务 3.部署Tomcat服务及配置动态页面Web服务 4.实验验收 动态页面 静态页面 引言 tomcat服务既可以处理动态页面&#xff0c;也可以处理静态页面&#xff1b;但其处理静态页面的速度远远不如…

js优雅的统计字符串字符出现次数

题目如下 统计一串字符串中每个字符出现的频率 示例字符串 let str asdfasqwerqwrdfafafasdfopasdfopckpasdfassfd小白写法 let str asdfasqwerqwrdfafafasdfopasdfopckpasdfassfdlet result {}; for (let i 0; i < str.length; i) {if (result[str[i]]) {result[str[…

链表基础知识详解(非常详细简单易懂)

概述&#xff1a; 链表作为 C 语言中一种基础的数据结构&#xff0c;在平时写程序的时候用的并不多&#xff0c;但在操作系统里面使用的非常多。不管是RTOS还是Linux等使用非常广泛&#xff0c;所以必须要搞懂链表&#xff0c;链表分为单向链表和双向链表&#xff0c;单向链表很…

【Vue3】解锁Vue3黑科技:探索接口、泛型和自定义类型的前端奇迹

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

Android Compose - PlainTooltipBox(已废弃)的替代方案

Android Compose - PlainTooltipBox 的替代方案 TooltipBox(positionProvider TooltipDefaults.rememberPlainTooltipPositionProvider(),tooltip {PlainTooltip {Text(/* tooltip content */)}},state rememberTooltipState(), ) {// tooltip anchorIconButton(onClick {…