SpringBoot基于Zookeeper和Curator生成唯一ID
Posted 嘉禾嘉宁papa
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot基于Zookeeper和Curator生成唯一ID相关的知识,希望对你有一定的参考价值。
目录
一、简介
zookeeper是一个分布式的,开放源码的分布式应用程序协调服务,它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。选择zookeeper的原因,因为zookeeper具有以下优点:
- 可靠性:如果消息被到一台服务器接受,那么它将被所有的服务器接受。
- 实时性:在一定事件范围内,client能读到最新数据,如果需要最新数据,应该在读数据之前调用sync()接口
- 独立性:各个Client之间互不干预
- 顺序性:更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行
- 原子性:一次数据的更新只能成功或者失败,没有其他中间状态
- 最终一致性:全局唯一数据视图,client无论连接到哪个server,数据视图都是一致的
二、maven依赖
我这里把里面的日志框架都过滤了,加入了lombok,你们可以根据自己的需要进行相应的处理。
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath/>
</parent>
<groupId>com.alian</groupId>
<artifactId>zookeeper-id</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zookeeper-id</name>
<description>Spring Boot之zookeeper唯一id</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 本例中你不加入容器也可以运行-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.2.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
三、配置
3.1、属性配置文件
我们在resource目录下建一个config文件夹,然后在resource/config目录下创建zookeeper的自定义配置文件zookeeper.properties,主要是zookeeper连接和目录相关的配置,因为本机的配置原因,我这里就没有采用zookeeper的集群模式了,Zookeeper建议集群节点个数为奇数(大于等于3),只要超过一半的机器能够正常提供服务,那么整个集群都是可用的状态。
zookeeper.properties
# zookeeper服务器地址(ip+port)
zookeeper.server=10.130.3.16:2181
# 节点创建的路径约定用"/"结尾
zookeeper.rootPath=/root/alian/
# 休眠时间
zookeeper.sleep-time=1000
# 最大重试次数
zookeeper.max-retries=3
# 会话超时时间
zookeeper.session-timeout=5000
# 连接超时时间
zookeeper.connection-timeout=5000
3.2、属性配置类
此配置类不懂的可以参考我另一篇文章:Spring Boot读取配置文件常用方式
ZookeeperProperties.java
package com.alian.zookeeperid.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "zookeeper")
//读取指定路径配置文件,暂不支持*.yaml文件
@PropertySource(value = "classpath:config/zookeeper.properties", encoding = "UTF-8", ignoreResourceNotFound = true)
public class ZookeeperProperties {
/**
* zookeeper服务地址
*/
private String server;
/**
* 根路径
*/
private String rootPath;
/**
* 重试等待时间
*/
private int sleepTime;
/**
* 最大重试次数
*/
private int maxRetries;
/**
* session超时时间
*/
private int sessionTimeout;
/**
* 连接超时时间
*/
private int connectionTimeout;
}
3.3、zookeeper配置类(核心)
为什么要使用Curator,因为Curator提供了简化使用zookeeper更高级的API接口,CuratorFramework实例都是线程安全的。它包涵很多优秀的特性:
- 自动连接管理:自动处理zookeeper的连接和重试存在一些潜在的问题,可以监控节点数据改变和获取更新服务的列表,而监控又可以自动被Cruator recipes删除
- 更简洁的API:提供现代流式API接口,简化了原生zookeeper方法,事件等
- Recipe实现:可以应用到leader选举,分布式锁,path缓存,和watcher,分布式队列等
ZookeeperConfig.java
package com.alian.zookeeperid.config;
import com.alian.zookeeperid.common.ZookeeperClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class ZookeeperConfig {
@Autowired
private ZookeeperProperties zookeeperProperties;
@Bean
public CuratorFramework curatorFrameworkClient() {
//重试策略,ExponentialBackoffRetry(1000,3)这里表示等待1s重试,最大重试次数为3次
RetryPolicy policy = new ExponentialBackoffRetry(zookeeperProperties.getSleepTime(), zookeeperProperties.getMaxRetries());
//构建CuratorFramework实例
CuratorFramework curatorFrameworkClient = CuratorFrameworkFactory
.builder()
.connectString(zookeeperProperties.getServer())
.sessionTimeoutMs(zookeeperProperties.getSessionTimeout())
.connectionTimeoutMs(zookeeperProperties.getConnectionTimeout())
.retryPolicy(policy)
.build();
//启动实例
curatorFrameworkClient.start();
return curatorFrameworkClient;
}
//采用这种方式可以比较优雅的关闭连接
@Bean(initMethod = "init", destroyMethod = "destroy")
public ZookeeperClient zookeeperClient(ZookeeperProperties zookeeperProperties, CuratorFramework curatorFrameworkClient) {
return new ZookeeperClient(zookeeperProperties, curatorFrameworkClient);
}
}
四、具体使用
4.1、zookeeperClient(核心)
此组件主要是完成,顺序节点的创建,我这里创建的是临时顺利节点,如果担心节点过多可以采用异步线程删除节点,我这里也只是一个示例。我想我代码的注释比这个惨白的文字要更有说服力。很多小伙伴可能会采用@Component注解,我使用@Bean(initMethod = “init”, destroyMethod = “destroy”)来创建,可以比较优雅的关闭连接,当然在使用上是一样的,只要不是同时对一个bean这么操作。
zookeeperClient.java
package com.alian.zookeeperid.common;
import com.alian.zookeeperid.config.ZookeeperProperties;
import com.sun.istack.internal.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class ZookeeperClient {
private ZookeeperProperties zookeeperProperties;
private static ExecutorService executorService;
private CuratorFramework curatorFramework;
public CuratorFramework getCuratorFramework() {
return curatorFramework;
}
public ZookeeperClient(ZookeeperProperties zookeeperProperties, CuratorFramework curatorFramework) {
this.zookeeperProperties = zookeeperProperties;
this.curatorFramework = curatorFramework;
}
public void init() {
log.info("ZookeeperClient初始化方法,新建线程池,用于删除临时节点");
try {
//线程池用于删除异步删除节点
executorService = Executors.newFixedThreadPool(10);
} catch (Exception e) {
log.error("ZookeeperClient init error {}", e.getMessage());
}
}
public void destroy() {
try {
log.info("ZookeeperClient销毁方法,如果zookeeper连接不为空,则关闭连接");
if (getCuratorFramework() != null) {
//这种方式比较优雅的关闭连接
getCuratorFramework().close();
}
} catch (Exception e) {
log.error("stop zookeeper client error {}", e.getMessage());
}
}
/**
* 分布式ID生成
*/
public String generateId(String nodeName) {
//创建临时自动编号的顺序节点,返回的是一个路径
String nodeFullPath = createSeqNode(nodeName);
if (null == nodeFullPath) {
return "";
}
String generateId = splitSeqNode(nodeFullPath);
log.info("节点编号:" + generateId);
return generateId;
}
/**
* 创建顺序节点,约定nodePrefix不能为空,也不能用"/"开头或结尾
*/
private String createSeqNode(@NotNull String nodePrefix) {
if ("".equals(nodePrefix.trim()) || nodePrefix.startsWith("/") || nodePrefix.endsWith("/")) {
log.error("节点前缀错误");
return "";
}
String nodeFullPath = "";
//根路径+要创建节点的名称(可以是路径,此处传入的nodeName不要用"/"开头或结尾)
String fullPath = zookeeperProperties.getRootPath().concat(nodePrefix);
try {
//关键点:创建临时顺序节点
//creatingParentsIfNeeded():如果传入的是路径,并且节点父路径不存在则创建父节点
nodeFullPath = curatorFramework
.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(fullPath);
//为防止生成的节点浪费系统资源,故生成后异步删除此节点
cleanNode(nodeFullPath);
} catch (Exception e) {
log.error("创建临时顺序节点异常{}", e.getMessage());
e.printStackTrace();
}
return nodeFullPath;
}
private void cleanNode(String finalBackNodePath) {
executorService.execute(() -> {
try {
//Thread.sleep(10);
Stat stat = curatorFramework.checkExists().forPath(finalBackNodePath);
if (stat != null) {
// log.info("------------" + new String(curatorFramework.getData().forPath("/zookeeper/alian")));
curatorFramework.delete().forPath(finalBackNodePath);
// log.info("删除的临时节点:{}", finalBackNodePath);
}
} catch (Exception e) {
log.error("删除节点异常{}", e.getMessage());
e.printStackTrace();
}
});
}
private String splitSeqNode(String path) {
//获取最后一个"/"的索引,用于字符串截取
int index = path.lastIndexOf("/");
if (index >= 0) {
//如果"/"位置比路径长度小,则截取"/"后面的作为节点返回
//如果"/"位置等于路径长度,说明它后面没有元素,返回空字符串
return index <= path.length() ? path.substring(index + 1) : "";
}
//不含"/",说明是节点数据,直接返回(理论上不会,因为都是"/"开头的路径,至少有一个"/")
return path;
}
}
关于zookeeper里节点的模式,新版又增加了两个带TTL的持久化节点,节点模式:
- PERSISTENT:持久化节点
- PERSISTENT_SEQUENTIAL:持久化的顺序自动编号节点
- EPHEMERAL:临时节点
- EPHEMERAL_SEQUENTIAL:临时顺序自动编号节点
- CONTAINER:容器节点
- PERSISTENT_WITH_TTL:带TTL(time-to-live,存活时间)的持久化节点,节点在TTL时间之内没有得到更新并且没有子节点,就会被自动删除
- PERSISTENT_SEQUENTIAL_WITH_TTL:带TTL(time-to-live,存活时间)和单调递增序号的持久节点,节点在TTL时间之内没有得到更新并且没有子节点,就会被自动删除
可能很多小伙伴不知道怎么创建待TTL的节点,我也给大家准备了,主要是使用withTtl(5000L)设置过期
String fullPath="/root/alian/";
String nodePath = curatorFramework
.create()
.withTtl(5000L)
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT_SEQUENTIAL_WITH_TTL)
.forPath(fullPath);
4.2、controller层
简单的一个获取id的接口,仅仅是为了演示,实际中肯定更加严格和规范。生成时间字符串yyyyMMddHHmmssSSS+zookeeper生产的id,组成我们需要的id,当然你也可以使用其他的前缀。
DistributedIdController.java
SpringBoot基于Zookeeper和Curator实现分布式锁并分析其原理
SpringBoot电商项目实战 — Zookeeper的分布式锁实现
SpringBoot2 整合 Zookeeper组件,管理架构中服务协调