前文分享etcd框架Go语言的实践,今天分享一下Java客户端的不分。再分享之前,先简单聊一下我查阅的资料的现状,以方便各位再开始Java客户端学习之前,有个心理预期。

etcd本身是Go语言编写的,所以在语言支持上,Go语言是支持的最好的。其他的就差强人意,这种场景有点像 Web3j ,有人再维护,但是从使用便捷程度上,总是不能一帆风顺直接上手。

而且还有一个原因,etcd的Java实现库太多了,各种库之间的细微差异也能让我搜索资料的时候难以准确找到最佳实践及其原理介绍。

大多数实现库都用了大量的异步操作,语法跟 Web3j 类似,我也不确定是哪种设计模式,如果你有 Web3j 使用经验,相信会更加容易上手。

Java 客户端比较

特性jetcdetcd4jspring-cloud-kubernetesvertx-etcd-client
维护者etcd-io (CoreOS)jurmousSpring CloudEclipse Vert.x
etcd 兼容性v3 API主要支持 v2 APIv3 APIv3 API
异步支持
依赖gRPCNettySpring CloudVert.x
特点官方支持,全面的功能轻量级,简单易用与 Spring Cloud 集成与 Vert.x 生态系统集成
适用场景大型项目,需要全面功能简单使用,遗留系统Spring Cloud 项目Vert.x 项目
Watch 支持
事务支持有限通过 Spring 抽象
性能中等依赖 Spring 抽象
社区活跃度中等
文档质量详细一般详细详细
学习曲线中等高(如果不熟悉 Spring)中等

详细比较

  1. jetcd
    • 优点:
      • 官方支持,与 etcd 版本同步更新
      • 全面支持 etcd v3 API
      • 性能优秀,适合大规模生产环境
    • 缺点:
      • 依赖较重(gRPC)
      • 学习曲线可能稍陡
  2. etcd4j
    • 优点:
      • 轻量级,容易集成
      • API 简单直观
    • 缺点:
      • 主要支持 etcd v2 API,对 v3 支持有限
      • 社区更新较慢
      • 不适合需要 v3 API 特性的新项目
  3. spring-cloud-kubernetes
    • 优点:
      • 与 Spring Cloud 和 Kubernetes 生态系统深度集成
      • 提供服务发现和配置管理功能
    • 缺点:
      • 依赖 Spring 生态系统,不适合非 Spring 项目
      • 可能引入不必要的复杂性(如果只需要简单的 etcd 客户端)
  4. vertx-etcd-client
    • 优点:
      • 与 Vert.x 生态系统集成
      • 非阻塞 API,适合高并发场景
    • 缺点:
      • 与 Vert.x 绑定,不适合非 Vert.x 项目
      • 社区相对较小

Java 客户端实践

下面我选择 jetcd 作为实现库,首先我们添加依赖项目:

<dependency>  
    <groupId>io.etcd</groupId>  
    <artifactId>jetcd-core</artifactId>  
    <version>0.7.0</version>  
</dependency>  
<dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>  
    <version>31.1-jre</version>  
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

如果你再运行当中遇到了 Exception in thread "main" java.lang.NoClassDefFoundError: 此类错误,请检查服务端版本,gRPC版本,客户端版本,以及依赖项缺失。这也是劝退的原因之一。

接下来我们来看Case,除了读写以外,我增加了监听的用例。总体来讲,语法比较熟悉 (我用过 Web3j ),下面是两个简单的例子,用来演示 jetcd 的基本使用。

package com.funtest.temp  
  
import com.funtester.frame.SourceCode  
import io.etcd.jetcd.ByteSequence  
import io.etcd.jetcd.Client  
import io.etcd.jetcd.Watch  
import io.etcd.jetcd.kv.GetResponse  
import io.etcd.jetcd.watch.WatchEvent  
  
import java.nio.charset.Charset  
import java.nio.charset.StandardCharsets  
import java.util.concurrent.CompletableFuture  
  
class TtcdTest extends SourceCode {  
  
    static Charset defaultCharset = StandardCharsets.UTF_8  
  
    // 创建客户端, 连接etcd  
    static def client = Client.builder().endpoints("http://localhost:2379").build()  
  
    // 创建KV客户端, 用于读写数据  
    static def kVClient = client.getKVClient()  
  
    static def watchClient = client.getWatchClient()  
  
    /**  
     * 监听etcd中的key变化, 有变化时打印出来  
     */  
    static watch() {  
        def key = toByteSequence("key")// 监听的key  
        Watch.Listener listener = Watch.listener(watchResponse -> {// 监听器  
            for (WatchEvent event : watchResponse.getEvents()) {// 事件  
                println("watch change ------------------")// 打印  
                println("修改的类型Event type: " + event.getEventType());// 事件类型, PUT, DELETE  
                println("修改的Key: " + event.getKeyValue().getKey().toString(StandardCharsets.UTF_8));// 修改的Key, ByteSequence转字符串  
                println("修改后Value: " + event.getKeyValue().getValue().toString(StandardCharsets.UTF_8));// 修改后的Value, ByteSequence转字符串  
            }  
        });  
      watchClient.watch(key, listener)// 监听key, 有变化时触发监听器  
    }  
  
    /**  
     * 写入数据, 读取数据  
     * @return  
     */  
    static writeRead() {  
        kVClient.put(toByteSequence("key"), toByteSequence("FunTester")).get()// 写入key-value  
        CompletableFuture<GetResponse> getFuture = kVClient.get(toByteSequence("key"))// 读取key-value  
        GetResponse response = getFuture.get()// 获取结果, 阻塞等待, 直到获取到结果  
        println("Value: " + response.getKvs().get(0).getValue().toString())// 打印结果  
    }  
  
    /**  
     * 字符串转ByteSequence  
     * @param str  
     * @param Charset  
     * @return  
     */  
    static ByteSequence toByteSequence(String str, Charset = defaultCharset) {  
        return ByteSequence.from(str, defaultCharset);  
    }  
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.

下面我们来依次执行两个方法:

    public static void main(String[] args) {
        watch()
        writeRead()
    }
  • 1.
  • 2.
  • 3.
  • 4.

下面是控制台打印:

watch change ------------------
修改的类型Event type: PUT
修改的Key: key
修改后Value: FunTester
Value: FunTester
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

可以看到是满足预期的。但是问题来了,JVM进程就是不退出,比较尴尬,即使我们加上关闭客户端的方法 client.close() 也不行,打开线程转储之后发现好几个 RUNNABLE 的线程,还有一个 forkjoin 线程池,现象跟 Web3j 很像,但是这次跟 Netty 相关,我也懒得深究原因了。