首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >jetcd实战之二:基本操作

jetcd实战之二:基本操作

原创
作者头像
程序员欣宸
修改2021-09-26 09:24:39
1.3K0
修改2021-09-26 09:24:39
举报
文章被收录于专栏:实战docker实战docker实战docker

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

系列文章链接

  1. jetcd实战之一:极速体验
  2. jetcd实战之二:基本操作
  3. jetcd实战之三:进阶操作(事务、监听、租约)

本篇概览

本文是《jetcd实战系列》的第二篇,经过前面的准备,我们有了可用的etcd集群环境和gradle父工程,并且写了个helloworld程序连接etcd简单体验了一番,今天的实战咱们聚焦那些常用的etcd操作,例如写、读、删除等,这些操作可以覆盖到日常大部分场景,本文主要有以下几部分组成:

  1. 编写接口类EtcdService.java,定义常用的etcd操作;
  2. 编写接口类的实现EtcdServiceImpl.java,这里面主要是调用jetcd提供的API来完成具体的etcd操作;
  3. 编写单元测试类EtcdServiceImplTest.java,这里面有很多测试方法,来演示如何使用EtcdService的接口来实现各种复杂的操作;

源码下载

名称

链接

备注

项目主页

该项目在GitHub上的主页

git仓库地址(https)

该项目源码的仓库地址,https协议

git仓库地址(ssh)

git@github.com:zq2599/blog_demos.git

该项目源码的仓库地址,ssh协议

  • 这个git项目中有多个文件夹,kubebuilder相关的应用在jetcd-tutorials文件夹下,如下图红框所示:
    在这里插入图片描述
    在这里插入图片描述
  • jetcd-tutorials文件夹下有多个子项目,本篇的是base-operate
    在这里插入图片描述
    在这里插入图片描述

新建子模块base-operate

  • 在父工程jetcd-tutorials下新增名为base-operate的Gradle子模块,其build.gradle文件内容如下:
plugins {
    id 'java-library'
}

// 子模块自己的依赖
dependencies {
    api 'io.etcd:jetcd-core'
    api 'org.projectlombok:lombok'
    // annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
    annotationProcessor 'org.projectlombok:lombok'
    // slf4j的包自己用就行了,不要继承到其他工程中去,否则容易和其他日志包起冲突
    implementation 'org.slf4j:slf4j-log4j12'
    testImplementation('org.junit.jupiter:junit-jupiter')
}

test {
    useJUnitPlatform()
}
  • 新增接口EtcdService.java,这里面定义了常用的etcd操作:
package com.bolingcavalry.dao;

import io.etcd.jetcd.Response.Header;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;

/**
 * @Description: Etcd操作服务的接口
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2021/3/30 7:55
 */
public interface EtcdService {

    /**
     * 写入
     * @param key
     * @param value
     */
    Header put(String key, String value) throws Exception;

    /**
     * 读取
     * @param key
     * @return
     */
    String getSingle(String key) throws Exception;


    /**
     * 带额外条件的查询操作,例如前缀、结果排序等
     * @param key
     * @param getOption
     * @return
     */
    GetResponse getRange(String key, GetOption getOption) throws Exception;

    /**
     * 单个删除
     * @param key
     * @return
     */
    long deleteSingle(String key) throws Exception;

    /**
     * 范围删除
     * @param key
     * @param deleteOption
     * @return
     */
    long deleteRange(String key, DeleteOption deleteOption) throws Exception;

    /**
     * 关闭,释放资源
     */
    void close();
}
  • 新增上述接口对应的实现类,可见大多数是直接调用jetcd提供的API:package com.bolingcavalry.dao.impl; import com.bolingcavalry.dao.EtcdService; import io.etcd.jetcd.ByteSequence; import io.etcd.jetcd.Client; import io.etcd.jetcd.KV; import io.etcd.jetcd.Response; import io.etcd.jetcd.kv.GetResponse; import io.etcd.jetcd.options.DeleteOption; import io.etcd.jetcd.options.GetOption; import static com.google.common.base.Charsets.UTF_8; /** * @Description: etcd服务的实现类 * @author: willzhao E-mail: zq2599@gmail.com * @date: 2021/3/30 8:28 */ public class EtcdServiceImpl implements EtcdService { private Client client; private String endpoints; private Object lock = new Object(); public EtcdServiceImpl(String endpoints) { super(); this.endpoints = endpoints; } /** * 将字符串转为客户端所需的ByteSequence实例 * @param val * @return */ public static ByteSequence bytesOf(String val) { return ByteSequence.from(val, UTF_8); } /** * 新建key-value客户端实例 * @return */ private KV getKVClient(){ if (null==client) { synchronized (lock) { if (null==client) { client = Client.builder().endpoints(endpoints.split(",")).build(); } } } return client.getKVClient(); } @Override public void close() { client.close(); client = null; } @Override public Response.Header put(String key, String value) throws Exception { return getKVClient().put(bytesOf(key), bytesOf(value)).get().getHeader(); } @Override public String getSingle(String key) throws Exception { GetResponse getResponse = getKVClient().get(bytesOf(key)).get(); return getResponse.getCount()>0 ? getResponse.getKvs().get(0).getValue().toString(UTF_8) : null; } @Override public GetResponse getRange(String key, GetOption getOption) throws Exception { return getKVClient().get(bytesOf(key), getOption).get(); } @Override public long deleteSingle(String key) throws Exception { return getKVClient().delete(bytesOf(key)).get().getDeleted(); } @Override public long deleteRange(String key, DeleteOption deleteOption) throws Exception { return getKVClient().delete(bytesOf(key), deleteOption).get().getDeleted(); } }
  • 看到这里,您一定觉得太easy了,确实,调用上述方法就能轻松完成常用的读写操作,但很多时候咱们的操作并非对指定的key做读写那么简单,例如按前缀查询、只返回数量不返回数据、批量删除直到指定的key出现为止,其实只要用好EtcdService提供的那几个接口,上述复杂操作都能轻松完成;
  • 接下来咱们通过单元测试来逐一体验EtcdService提供的那几个接口,并尝试完成各种复杂操作;编写单元测试用例
  • 新增单元测试类EtcdServiceImplTest,如下图所示,为了让其内部的方法按我们指定的顺序执行,记得给类添加注解@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
在这里插入图片描述
在这里插入图片描述
  • 如下图红框,默认使用Gradle作为测试工具,这里请改成红框中的IntelliJ IDEA,这样单元测试代码中的Order、DisplayName等注解才能生效:
在这里插入图片描述
在这里插入图片描述
  • 接下来开始在EtcdServiceImplTest中写代码,先写个key方法,这里面用当前时间和输入的字符串拼接成一个独一无二的字符串,可以作为后面的测试是的key(或者key前缀):
	private static String key(String name) {
        return "/EtcdServiceImplTest/" + name + "-" + System.currentTimeMillis();
    }
  • 定义EtcdServiceImp实例作为静态变量,后面的测试中都会用到,另外还要在测试结束时关闭客户端连接:
	private static EtcdService etcdService = new EtcdServiceImpl();

    @AfterAll
    static void close() {
        etcdService.close();
    }
  • 接下来开始体验etcd的基本操作;

基本写操作

  • 写操作非常简单,就是调用put方法传入key和value,至于验证,在开始读操作之前先简单点,确认header非空即可:
    @Test
    @Order(1)
    @DisplayName("基本写操作")
    void put() throws Exception {
        Response.Header header = etcdService.put(key("put"), "123");
        assertNotNull(header);
    }

读操作

  • 先测试最基本的读操作,用getSingle方法可以返回单个结果:
    @Test
    @Order(2)
    @DisplayName("基本读操作")
    void getSingle() throws Exception {
        String key = key("getSingle");
        String value = String.valueOf(System.currentTimeMillis());

        // 先写入
        etcdService.put(key, value);

        // 再读取
        String queryRlt = etcdService.getSingle(key);

        assertEquals(value, queryRlt);
    }
  • 接下来借助GetOption对象,我们可以进行跟多复杂的读操作,先看如何通过前缀查询多个键值对:
    @Test
    @Order(3)
    @DisplayName("读操作(指定前缀)")
    void getWithPrefix() throws Exception {
        String prefix = key("getWithPrefix");

        // 先写入十条
        int num = 10;

        for (int i=0;i<num;i++) {
            // 写入,每个key都不同
            etcdService.put(prefix + i, String.valueOf(i));
        }

        // 带前缀的方式查询,注意要入参key和prefix是同一个值
        GetOption getOption = GetOption.newBuilder().withPrefix(EtcdServiceImpl.bytesOf(prefix)).build();
        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // 总数应该是十个
        assertEquals(num, getResponse.getCount());
    }
  • 假设总共有十条结果,还可以控制只返回五条记录(不过总数字段还是十):
    @Test
    @Order(4)
    @DisplayName("读操作(指定KeyValue结果数量)")
    void getWithLimit() throws Exception {
        String prefix = key("getWithLimit");

        // 先写入十条
        int num = 10;
        int limit = num/2;

        for (int i=0;i<num;i++) {
            // 写入,每个key都不同
            etcdService.put(prefix + i, String.valueOf(i));
        }

        // 带前缀的方式查询,查出来应该是十个,再加上数量限制为五个
        GetOption getOption = GetOption.newBuilder()
                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                .withLimit(limit)
                .build();

        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // 总数还是十个
        assertEquals(num, getResponse.getCount());
        // 结果的数量和limit有关,是5个
        assertEquals(limit, getResponse.getKvs().size());
    }
  • revision字段是etcd的全局版本号,每次写入都会对应一个revision值,可以用该revision值作为查询条件,查到指定key过往的某个版本的值:
    @Test
    @Order(5)
    @DisplayName("读操作(指定revision)")
    void getWithRevision() throws Exception {
        String key = key("getWithRevision");

        // 先写入十条
        int num = 10;
        int limit = num/2;

        // 第一次写入时的revision
        long firstRevision = 0L;

        // 第一次写入的value
        String firstValue = null;

        // 最后一次写入的value
        String lastValue = null;

        for (int i=0;i<num;i++) {
            // 用同一个key写十次,每次的value都不同
            String value = String.valueOf(i);
            // 注意,key一直没有变化
            Response.Header header = etcdService.put(key, value);

            // 第一次写入的revision和value都保存下来,后面用revision取出值,和value对比应该相等
            if (0==i) {
                firstRevision = header.getRevision();
                firstValue = value;
            } else if ((num-1)==i) {
                // 将最后一次写入的value记录下来
                lastValue = value;
            }
        }


        // 记录下来的第一次写入的值和最后一次写入的值应该不等
        assertNotEquals(firstValue, lastValue);

        // 如果不带其他条件只用key查找,查出的值应该等于最后一次写入的
        assertEquals(lastValue, etcdService.getSingle(key));

        // 查询条件中指定第一次写入的revision
        GetOption getOption = GetOption.newBuilder()
                              .withRevision(firstRevision)
                              .build();

        GetResponse getResponse = etcdService.getRange(key, getOption);

        // 总数是一个
        assertEquals(1, getResponse.getCount());

        // 结果的value应该和前面记录的第一次写入的值相等
        assertEquals(firstValue, getResponse.getKvs().get(0).getValue().toString(UTF_8));
    }
  • 当前查询结果有多个时,还可以对结果进行排序,key或者value都能用作排序字段,并且可以选择升序还是降序:
    @Test
    @Order(6)
    @DisplayName("读操作(结果排序)")
    void getWithOrder() throws Exception {
        String prefix = key("getWithOrder");

        // 先写入十条,每一条的key都不同,value也不同
        int num = 10;

        // 第一次写的key
        String firstKey = null;
        // 第一次写的value
        String firstValue = null;
        // 最后一次写的key
        String lastKey = null;
        // 最后一次写的value
        String lastValue = null;

        for (int i=0;i<num;i++) {
            String key = prefix + i;
            String value = String.valueOf(i);
            // 写入,每个key都不同
            etcdService.put(key, value);

            // 把第一次写的key、value,最后一次写的key、value保存到对应的变量中
            if(0==i) {
                firstKey = key;
                firstValue = value;
            } else if((num-1)==i) {
                lastKey = key;
                lastValue = value;
            }
        }


        // 第一次查询,结果用key排序,从大到小
        GetOption getOption = GetOption.newBuilder()
                                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                                .withSortField(GetOption.SortTarget.KEY)
                                .withSortOrder(GetOption.SortOrder.DESCEND)
                                .build();

        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // 总数还是十个
        assertEquals(num, getResponse.getCount());

        // 取查询结果的第一条
        KeyValue firstResult = getResponse.getKvs().get(0);

        // 因为是从大到小,查询结果的第一条应该是最后一次写入的(key是lastKey,value是lastValue)
        assertEquals(lastKey, firstResult.getKey().toString(UTF_8));
        assertEquals(lastValue, firstResult.getValue().toString(UTF_8));


        // 第二次查询,结果用key排序,从小到大
        getOption = GetOption.newBuilder()
                    .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                    .withSortField(GetOption.SortTarget.KEY)
                    .withSortOrder(GetOption.SortOrder.ASCEND)
                    .build();

        getResponse = etcdService.getRange(prefix, getOption);

        // 总数还是十个
        assertEquals(num, getResponse.getCount());

        // 取查询结果的第一条
        firstResult = getResponse.getKvs().get(0);

        // 因为是从小到大,查询结果的第一条应该是第一次写入的(key是firstKey,value是firstValue)
        assertEquals(firstKey, firstResult.getKey().toString(UTF_8));
        assertEquals(firstValue, firstResult.getValue().toString(UTF_8));
    }
  • 指定返回结果中只有key没有value:
    @Test
    @Order(7)
    @DisplayName("读操作(只返回key)")
    void getOnlyKey() throws Exception {
        String key = key("getOnlyKey");
        // 写入一条记录
        etcdService.put(key, String.valueOf(System.currentTimeMillis()));

        // 查询条件中指定只返回key
        GetOption getOption = GetOption.newBuilder()
                            .withKeysOnly(true)
                            .build();

        GetResponse getResponse = etcdService.getRange(key, getOption);

        assertEquals(1, getResponse.getCount());

        KeyValue keyValue = getResponse.getKvs().get(0);

        assertNotNull(keyValue);

        assertEquals(key, keyValue.getKey().toString(UTF_8));

        // value应该是空的
        assertTrue(keyValue.getValue().isEmpty());
    }
  • 返回的结果中只有数量,不包含key和value:
    @Test
    @Order(8)
    @DisplayName("读操作(只返回数量)")
    void getOnlyCount() throws Exception {
        String key = key("getOnlyCount");
        // 写入一条记录
        etcdService.put(key, String.valueOf(System.currentTimeMillis()));

        // 查询条件中指定只返回key
        GetOption getOption = GetOption.newBuilder()
                            .withCountOnly(true)
                            .build();

        GetResponse getResponse = etcdService.getRange(key, getOption);

        // 数量应该是1
        assertEquals(1, getResponse.getCount());

        // KeyValue应该是空的
        assertTrue(getResponse.getKvs().isEmpty());
    }
  • 假设etcd有三个key:a1、a2、a3,那么通过前缀a可以将这三个key都查出来,与此同时还可以再加个endKey查询条件,假设endKey等于a2,那么查找工作在查到a2时就会停止并返回,而返回值中只有a1,不包含a2,换言之endKey之前的值才会被返回
    @Test
    @Order(9)
    @DisplayName("读操作(查到指定key就结束)")
    void getWithEndKey() throws Exception {
        String prefix = key("getWithEndKey");
        String endKey = null;

        int num = 10;

        for (int i=0;i<num;i++) {
            String key = prefix + i;
            // 写入,每个key都不同
            etcdService.put(key, String.valueOf(i));

            // 总共写入十条记录,把第九条的key作为endKey保存
            if ((num-2)==i) {
                endKey = key;
            }
        }

        // 查询条件中指定了endKey是上面写入的第九条记录的key
        // 注意,查询结果中不包含endKey那条记录,也就是说只返回前八条
        GetOption getOption = GetOption.newBuilder()
                .withRange(EtcdServiceImpl.bytesOf(endKey))
                .build();

        GetResponse getResponse = etcdService.getRange(prefix, getOption);

        // 注意,查询结果中不包含endKey那条记录,也就是说只返回前八条
        assertEquals(num-2, getResponse.getCount());
    }
  • 以上就是读操作的常见用法,接下来看删除;

删除操作

  • 最基本的删除就是调用deleteSingle方法: @Test @Order(10) @DisplayName("单个删除") void deleteSingle() throws Exception { String key = key("deleteSingle"); // 写入一条记录 etcdService.put(key, String.valueOf(System.currentTimeMillis())); // 此时应该能查到 assertNotNull(etcdService.getSingle(key)); // 删除 etcdService.deleteSingle(key); // 此时应该查不到了 assertNull(etcdService.getSingle(key)); } @Test @Order(11) @DisplayName("删除(指定前缀)") void deleteWithPrefix() throws Exception { String prefix = key("deleteWithPrefix"); int num = 10; // 写入,每个key都不同,但是有相同的前缀 for (int i=0;i<num;i++) { etcdService.put(prefix + i, String.valueOf(i)); } GetOption getOption = GetOption.newBuilder() .withPrefix(EtcdServiceImpl.bytesOf(prefix)) .build(); // 此时总数应该是十 assertEquals(num, etcdService.getRange(prefix, getOption).getCount()); // 删除条件是指定前缀 DeleteOption deleteOption = DeleteOption.newBuilder() .withPrefix(EtcdServiceImpl.bytesOf(prefix)) .build(); // 删除 etcdService.deleteRange(prefix, deleteOption); // 删除后再查,总数应该是0 assertEquals(0, etcdService.getRange(prefix, getOption).getCount()); }
  • 借助DeleteOption对象,可以实现更多类型的删除,下面是删除指定前缀的所有记录:
  • 与读操作的endKey类似,删除操作也有endKey参数,假设etcd有三个key:a1、a2、a3,那么通过前缀a可以将这三个key都删除,与此同时还可以再加个endKey删除条件,假设endKey等于a2,那么删除工作在查到a2时就会停止并返回,被删除的记录只有a1,不包含a2,换言之endKey之前的记录才会被删除
    @Test
    @Order(11)
    @DisplayName("删除(删到指定key就结束)")
    void deleteWithEndKey() throws Exception {
        String prefix = key("deleteWithEndKey");

        int num = 10;
        String endKey = null;

        // 写入,每个key都不同,但是有相同的前缀
        for (int i=0;i<num;i++) {
            String key = prefix + i;

            etcdService.put(key, String.valueOf(i));

            // 把第九条记录的key保存在endKey变量中
            if((num-2)==i) {
                endKey = key;
            }
        }

        GetOption getOption = GetOption.newBuilder()
                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                .build();

        // 此时总数应该是十
        assertEquals(num, etcdService.getRange(prefix, getOption).getCount());

        // 删除条件是指定前缀,并且遇到第九条记录的key就停止删除操作,此时第九条和第十条都不会被删除
        DeleteOption deleteOption = DeleteOption.newBuilder()
                .withPrefix(EtcdServiceImpl.bytesOf(prefix))
                .withRange(EtcdServiceImpl.bytesOf(endKey))
                .build();

        // 删除
        etcdService.deleteRange(prefix, deleteOption);

        // 删除后再查,总数应该是二

        assertEquals(2, etcdService.getRange(prefix, getOption).getCount());
    }
  • 至此,编码结束,执行单元测试试;

执行单元测试

  • 点击下图红框中的按钮,在弹出的菜单中点击Run EtcdServiceImplTest,即可开始单元测试:
在这里插入图片描述
在这里插入图片描述
  • 如下图,单元测试通过:
    在这里插入图片描述
    在这里插入图片描述
  • 至此,使用jetcd对etcd进行基本操作的实战已经完成,希望能给您的开发带来一些参考,接下来的章节,咱们去操作一些etcd的特性,包括事务、监听、租约;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界...

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 欢迎访问我的GitHub
  • 系列文章链接
  • 本篇概览
  • 源码下载
  • 新建子模块base-operate
  • 基本写操作
  • 读操作
  • 删除操作
  • 执行单元测试
  • 你不孤单,欣宸原创一路相伴
  • 欢迎关注公众号:程序员欣宸
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档