ETCD(9):通过Golang进行简单调用

426 阅读5分钟

1. 客户端创建

    cli,err := clientv3.New(clientv3.Config{
        Endpoints:[]string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })

如上的代码实例化了一个 client,这里需要传入的两个参数:

  • Endpoints:etcd 的多个节点服务地址,这里是单点本机测试,所以只传 1 个。
  • DialTimeout:创建 client 的首次连接超时,这里设为 5 秒,如果 5 秒都没有连接成功就会返回 err;值得注意的是,一旦 client 创建成功,我们就不用再关心后续底层连接的状态了,client 内部会重连。

初始化 etcd 客户端。客户端初始化代码如下所示:

import (
	"context"
	"fmt"
	"go.etcd.io/etcd/clientv3"
	"testing"
	"time"
)
// 测试客户端连接
func TestEtcdClientInit(t *testing.T) {
	// 客户端配置
	config := clientv3.Config{
		// 节点配置
		Endpoints:   []string{"localhost:2379"},
		DialTimeout: 5 * time.Second,
	}
	// 建立连接
	if client, err := clientv3.New(config); err != nil {
		fmt.Println(err)
	} else {
		// 输出集群信息
		fmt.Println(client.Cluster.MemberList(context.TODO()))
                client.Close()
	}
	
}

如上的代码,可能会报错 image.png

这是由于GRPC版本过高不支持clientv3导致的,我们在go mod末尾添加一行

replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

再go mod tidy一下。如果还有报错 image.png

让你

go get go.etcd.io/etcd/clientv3@v3.3.27+incompatible

预期的执行结果如下:

=== RUN   TestEtcdClientInit
&{cluster_id:14841639068965178418 member_id:10276657743932975437 raft_term:2  [ID:10276657743932975437 name:"default" peerURLs:"http://localhost:2380" clientURLs:"http://0.0.0.0:2379" ] {} [] 0} <nil>

可以看到 clientv3 与 etcd Server 的节点 localhost:2379 成功建立了连接,并且输出了集群的信息,下面我们就可以对 etcd 进行操作了。

image.png

2. KV 存储

kv 对象的实例获取通过如下的方式:

kv  := clientev3.NewKV(client)

我们来看一下 kv 接口的具体定义:

type KV interface {

    Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)

    Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)

    Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)

    // 压缩给定版本之前的 KV 历史
    Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)

    // 指定某种没有事务的操作
    Do(ctx context.Context, op Op) (OpResponse, error)

    // Txn 创建一个事务
    Txn(ctx context.Context) Txn
}

从 KV 对象的定义我们可知,它就是一个接口对象,包含几个主要的 kv 操作方法:

2.1 kv 存储 put

put 定义如下:

    Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)

参数:

  • ctx: 用来跟踪上下文的,比如超时控制
  • key: 存储对象的 key
  • val: 存储对象的 value
  • opts:  可变参数,额外选项

Put 将一个键值对放入 etcd 中。请注意,键值可以是纯字节数组,字符串是该字节数组的不可变表示形式。要获取字节字符串,请执行 string([] byte {0x10,0x20})

put 的使用方法如下所示:

putResp, err := kv.Put(context.TODO(),"aa", "hello-world!")

2.2 kv 查询 get

现在可以对存储的数据进行取值了。默认情况下,Get 将返回 “ key” 对应的值。

Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)

OpOption 为可选的函数传参,传参为 WithRange(end) 时,Get 将返回 [key,end)范围内的键;传参为 WithFromKey() 时,Get 返回大于或等于 key 的键;当通过 rev> 0 传递 WithRev(rev) 时,Get 查询给定修订版本的键;如果压缩了所查找的修订版本,则返回请求失败,并显示 ErrCompacted。 传递 WithLimit(limit) 时,返回的 key 数量受 limit 限制;传参为 WithSort 时,将对键进行排序。对应的使用方法如下:

getResp, err := kv.Get(context.TODO(), "aa")

从以上数据的存储和取值,我们知道 put 返回 PutResponse、get 返回 GetResponse,注意:不同的 KV 操作对应不同的 response 结构,定义如下:

type (
    CompactResponse pb.CompactionResponse
    PutResponse     pb.PutResponse
    GetResponse     pb.RangeResponse
    DeleteResponse  pb.DeleteRangeResponse
    TxnResponse     pb.TxnResponse
)

我们分别来看一看 PutResponse 和 GetResponse 映射的 RangeResponse 结构的定义:

type PutResponse struct {
    Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
    // if prev_kv is set in the request, the previous key-value pair will be returned.
    PrevKv *mvccpb.KeyValue `protobuf:"bytes,2,opt,name=prev_kv,json=prevKv" json:"prev_kv,omitempty"`
}
//Header 里保存的主要是本次更新的 revision 信息

type RangeResponse struct {
    Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
    // kvs is the list of key-value pairs matched by the range request.
    // kvs is empty when count is requested.
    Kvs []*mvccpb.KeyValue `protobuf:"bytes,2,rep,name=kvs" json:"kvs,omitempty"`
    // more indicates if there are more keys to return in the requested range.
    More bool `protobuf:"varint,3,opt,name=more,proto3" json:"more,omitempty"`
    // count is set to the number of keys within the range when requested.
    Count int64 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"`
}

Kvs 字段,保存了本次 Get 查询到的所有 kv 对,我们继续看一下 mvccpb.KeyValue 对象的定义:

type KeyValue struct {

    Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
    // create_revision 是当前 key 的最后创建版本
    CreateRevision int64 `protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3" json:"create_revision,omitempty"`
    // mod_revision 是指当前 key 的最新修订版本
    ModRevision int64 `protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3" json:"mod_revision,omitempty"`
    // key 的版本,每次更新都会增加版本号
    Version int64 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"`

    Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`

    // 绑定了 key 的租期 Id,当 lease 为 0 ,则表明没有绑定 key;租期过期,则会删除 key
    Lease int64 `protobuf:"varint,6,opt,name=lease,proto3" json:"lease,omitempty"`
}

至于 RangeResponse.More 和 Count,当我们使用 withLimit() 选项进行 Get 时会发挥作用,相当于分页查询。

接下来,我们通过一个特别的 Get 选项,获取 aa 目录下的所有子目录:

rangeResp, err := kv.Get(context.TODO(), "/aa", clientv3.WithPrefix())

WithPrefix() 用于查找以 /aa 为前缀的所有 key,因此可以模拟出查找子目录的效果。

我们知道 etcd 是一个有序的 kv 存储,因此 /aa 为前缀的 key 总是顺序排列在一起。

withPrefix 实际上会转化为范围查询,它根据前缀 /aa 生成了一个 key range,[“/aa/”, “/aa0”),这是因为比 / 大的字符是 0,所以以 /aa0 作为范围的末尾,就可以扫描到所有的 /aa/ 打头的 key 了。

2.3 KV 操作实践

package main

import (
	"context"
	"fmt"
	"time"

	"go.etcd.io/etcd/clientv3"
)

func main() {
	config := clientv3.Config{Endpoints: []string{"localhost:2379"}, DialTimeout: 5 * time.Second}
	client, err := clientv3.New(config)
	if err != nil{
		fmt.Println(err)
	}else{
		// 输出集群信息
		fmt.Println(client.Cluster.MemberList(context.Background()))
	}
	kv := clientv3.NewKV(client)
	// 存放
	kv.Put(context.Background(), "aaa", "bbb")
	kv.Put(context.Background(), "aab", "ccc")
	// 获取
	getResp, _ := kv.Get(context.Background(), "aa", clientv3.WithPrefix())
	kvs := getResp.Kvs
	for _, kv := range kvs{
		fmt.Print(kv.Key ," ")
		fmt.Println(kv.Value)
	}
	delResp, _ := kv.Delete(context.Background(), "aaa")
	fmt.Printf("delete aaa for %t\n", delResp.Deleted > 0)
 

}

如上的测试用例,主要是针对 kv 的操作,依次获取 key,即 Get(),对应 etcd 底层实现的 range 接口;其次是写入键值对,即 put 操作;最后删除刚刚写入的键值对。预期的执行结果如下所示:

image.png