一. etcd简介
etcd是基于go语言实现,主要是用于配置分享和服务发现的一个组件,最初用于解决集群管理系统中os升级时的分布式并发控制、配置文件的存储与分法等问题。因此,etcd是在分布式系统中提供强一致性、高可用性的组件,用来存储少量重要的数据。
二. 实践
项目使用的基本版本为:
Spring Cloud Alibaba版本2021.0.1.0
Spring Boot版本2.6.13
Spring Cloud版本2021.0.1
1.POM依赖
<dependency>
<groupId>io.etcd</groupId>
<artifactId>jetcd-core</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>2.0.54.Final</version>
</dependency>
2.etcd工具类
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ResourceUtils;
import javax.net.ssl.SSLException;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* etcd配置
*
* @author jiangxiaoyi
* @create 2023/03/09 10:56
*/
@Configuration
public class EtcdUtil {
private static Client client;
private static long TIME_OUT = 10000L;
private static TimeUnit TIME_UNIT = TimeUnit.MILLISECONDS;
public static void setClient(Client client) {
EtcdUtil.client = client;
}
/**
* 毫秒
*
* @param timeout 过期时间
*/
public static void setTimeOut(long timeout) {
EtcdUtil.TIME_OUT = timeout;
}
/**
* String转为ByteSequence类型对象
*
* @param val 欲转换的值 : 可以为Key或者Value
* @return ByteSequence对象
*/
public static ByteSequence bytesOf(String val) {
return ByteSequence.from(val.getBytes(StandardCharsets.UTF_8));
}
/**
* ByteSequence类型对象转为String
*
* @param byteSequence ByteSequence对象
* @return String类型
*/
public static String toString(ByteSequence byteSequence) {
return byteSequence.toString(StandardCharsets.UTF_8);
}
/**
* 判断当前Key是否存在
*
* @param key key
* @return true:是 false:否
*/
@SneakyThrows
public static Boolean hvKey(String key) {
if (null == key || "".equals(key)) {
return false;
}
ByteSequence byteKey = bytesOf(key);
GetResponse response = client.getKVClient().get(byteKey).get(TIME_OUT, TIME_UNIT);
return response.getCount() > 0;
}
/**
* 设置指定K-V
*
* @param key key
* @param value value
* @return true:成功 false:失败
*/
@SneakyThrows
public static Boolean put(String key, String value) {
if (null == key || "".equals(key)) {
throw new NullPointerException();
}
CompletableFuture<PutResponse> future = client.getKVClient().put(bytesOf(key), bytesOf(value));
PutResponse response = future.get(TIME_OUT, TIME_UNIT);
return null != response;
}
/**
* 获取指定Key的值
*
* @param key key
* @return 获取value值
*/
@SneakyThrows
public static String getSingle(String key) {
if (null == key || "".equals(key)) {
throw new NullPointerException();
}
ByteSequence byteKey = bytesOf(key);
GetResponse response = client.getKVClient().get(byteKey).get(TIME_OUT, TIME_UNIT);
if (null != response && response.getCount() > 0) {
return response.getKvs().get(0).getValue().toString(StandardCharsets.UTF_8);
} else {
return null;
}
}
/**
* 获取指定Key前缀的KV映射表
*
* @param prefix
* @return
*/
@SneakyThrows
public static Map<String, String> getWithPrefix(String prefix) {
if (null == prefix || "".equals(prefix)) {
throw new NullPointerException();
}
ByteSequence prefixByte = bytesOf(prefix);
GetOption getOption = GetOption.newBuilder().withPrefix(prefixByte).build();
GetResponse response = client.getKVClient().get(prefixByte, getOption).get(TIME_OUT, TIME_UNIT);
Map<String, String> kvMap = new HashMap<>();
if (null != response && response.getCount() > 0) {
response.getKvs().forEach(item -> kvMap.put(toString(item.getKey()), toString(item.getValue())));
}
return kvMap;
}
/**
* 删除指定Key
*/
@SneakyThrows
public static Boolean delSingle(String key) {
if (null == key || "".equals(key)) {
throw new NullPointerException();
}
long deleted = client.getKVClient().delete(bytesOf(key)).get(TIME_OUT, TIME_UNIT).getDeleted();
return deleted > 0;
}
/**
* 删除指定前缀的Key,返回删除的数量
*/
@SneakyThrows
public static long delWithPrefix(String prefix) {
if (null == prefix || "".equals(prefix)) {
throw new NullPointerException();
}
ByteSequence prefixByte = bytesOf(prefix);
DeleteOption deleteOption = DeleteOption.newBuilder().withPrefix(prefixByte).build();
long deleted = client.getKVClient().delete(prefixByte, deleteOption).get(TIME_OUT, TIME_UNIT).getDeleted();
return deleted;
}
}
3.连接实例(用户名 + 密码)
public void pushDataToEtcd(Object pushData, String requestId) {
var endpoints = applicationConfig.getEtcdUrl().split(",");
String user = "user";
String passwd = "password";
String key = "/etcd/key";
String value = JSONUtil.toJsonStr(pushData);
log.info("推送etcd数据key: " + key + " value: " + value);
Client etcdClient =
Client.builder().endpoints(endpoints).user(EtcdUtil.bytesOf(user)).password(EtcdUtil.bytesOf(passwd)).build();
EtcdUtil.setClient(etcdClient);
if (!EtcdUtil.put(key, value)) {
// 推送失败处理
}
}
4.连接实例(用户名 + 密码 + SSL认证)
设置SSL(准备好CA证书、客户端证书、私钥),所需的私钥文件必须是pkcs#8格式的.key文件才能够被netty读取到(默认生成的是**-key.pem的私钥信息,其文件格式是pkcs#1的格式)
RSA直接生成没有进行转换的秘钥格式为:-----BEGIN RSA PRIVATE KEY-----
pkcs#8格式的.key文件为的秘钥格式为----BEGIN PRIVATE KEY-----,java中的私钥一般是这种格式,但是公钥不需要转换。
转换命令:openssl pkcs8 -topk8 -nocrypt -in client-key.pem -out client.key
public void pushDataToEtcdBySSL(Object pushData) throws FileNotFoundException, SSLException {
var endpoints = applicationConfig.getEtcdUrl().split(",");
String user = "user";
String passwd = "password";
String key = "/etcd/key";
String value = JSONUtil.toJsonStr(pushData);
log.info("推送etcd数据key: "+ key + " value: " + value);
SslContext sslContext = EtcdUtil.openSslContext();
Client etcdClient =
Client.builder().endpoints(endpoints).user(EtcdUtil.bytesOf(user)).password(EtcdUtil.bytesOf(passwd))
.sslContext(sslContext).authority(applicationConfig.getEtcdIp()).build();
EtcdUtil.setClient(etcdClient);
if (!EtcdUtil.put(key, value)) {
// 推送失败处理
}
}
public static SslContext openSslContext() throws SSLException, FileNotFoundException {
// 证书、客户端证书、客户端私钥
File trustManagerFile = ResourceUtils.getFile("classpath:ca.crt");
File keyCertChainFile = ResourceUtils.getFile("classpath:client.crt");
File KeyFile = ResourceUtils.getFile("classpath:client.key");
// 这里必须要设置alpn,否则会提示ALPN must be enabled and list HTTP/2 as a supported protocol.错误; 这里主要设置了传输协议以及传输过程中的错误解决方式
ApplicationProtocolConfig alpn = new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2);
SslContext context = SslContextBuilder.forClient()
// 设置alpn
.applicationProtocolConfig(alpn)
// 设置使用的那种ssl实现方式
.sslProvider(SslProvider.OPENSSL)
// 设置ca证书
.trustManager(trustManagerFile)
// 设置客户端证书
.keyManager(keyCertChainFile, KeyFile).
build();
return context;
}
注意:如果报错:未找到匹配名称: No name matching "etcd" found.是无法找到对应的IP地址,可以通过.authority("定义的服务器端CA地址DNS/IP")解决,同样也可以通过在服务器端设置host解决。这是因为Jetcd默认设置的DNS名称为etcd。