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()
}
}
如上的代码,可能会报错
这是由于GRPC版本过高不支持clientv3导致的,我们在go mod末尾添加一行
replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
再go mod tidy一下。如果还有报错
让你
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 进行操作了。
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 操作;最后删除刚刚写入的键值对。预期的执行结果如下所示: