etcd 是由Go语言编写的 key-value 存储, 主要用途是共享配置和服务发现
分布式系统之间必然要做到数据共享, 需要依赖一个可靠的共享存储服务, 而etcd能够提供这样的服务
etcd类似的项目: zookeeper和consul
etcd常见的两个版本v2和v3, 它们是两个独立的应用
文档建议使用v3版本, 下载: https://github.com/coreos/etcd/releases
解压后:
启动服务:etcd.exe (双击即可开启服务)
客户端:etcdctl.exe(在当前目录下cmd打开命令行)
在当前命令行下存储:
etcdctl version 查看版本(包括API的版本)
etcdctl put "键" "值" 放入键值对(如果已存在该key则会覆盖掉原来的值)
etcdctl get "键" 通过键获取值
etcdctl del "键" 通过键删除
golang中使用etcd:
1.首先要安装etcd: 可以手动下载, 地址栏输入 -> https://github.com/etcd-io/etcd.git
2.下载以后, 要根据导入包的格式创建目录, 如下:
package xx
import (
"go.etcd.io/etcd/clientv3"
)
在 %gopath%/src 下创建目录go.etcd.io/etcd/ --> %gopath%/src/go.etcd.io/etcd
3.将下载的项目放在创建的目录下面, 如下创建go.etcd.io目录
D:\golang\workspace\project\src\
注: 使用go get方式不一定能下载成功, 当不能成功下载时, 可以使用如上的手动方式
使用手动方式, 在创建目录的时候要注意, 它根据导包时(import "go.etcd.io/etcd/clientv3")的格式创建
意思是: 第一个目录是go.etcd.io, 第二个目录是etcd, 最后一个是包名(不需要创建)
所以这里要创建目录为go.etcd.io/etcd -->将下载的内容放在etcd目录下面
1.创建client:
要使用etcd, 首先就是创建client, 这需要根据配置
这个配置是一个结构体类型的实例, 配置中包括主机, 超时等字段
conf := clientv3.Config {
Endpoints : []string {"localhost:2380"},
DialTimeout : 3 * time.Second,
}
client := clientv3.New(conf)
client代表etcd客户端, 它是*Client类型
Client代表了整个客户端, 它是结构体类型, 包括如下核心字段:
2.获取KV:
字段KV是我们主要使用的键值操作
KV是一个interface, 无法直接使用
但是etcd提供了一个函数直接获取字段KV的实例, 如下:
kv := clientv3.NewKV(client)
现在得到字段KV的实例后, 就可以做kv操作了:
_, err := kv.Put(context.TODO(), key, value) // 存入
resp, err := kv.Get(context.TODO(), key) // 获取
3.解析resp
如果是向etcd中存入值, 则返回*GetResponse
如果是从etcd中获取值, 则返回*PutResponse
如果是从etcd中删除, 则返回*DeleteResponse
上面是get操作, 所以resp是*GetResponse类型的实例 (put/delete操作也是类似的)
使用resp获取所有的"孩子" --> res.Kvs (Kvs是GetResponse里面的一个字段, 它是一个切片)
循环res.Kvs如下
for _,val := range resp.Kvs { // val即表示单个"孩子"
val := string(val.Value) // 获取Value, 但是它是字节数组, 所以转换为string
fmt.Println(val)
}
它即使存取中文也没有问题
4.更多:
.使用像目录结构这种形式的string 作为key
虽然用法和上面的没有区别, 但是它代表目录层级的意思
putResp, err := kv.Put(context.TODO(),"/test/a", "something")
.Get/put/delete行为
如上面的get操作, 它是根据key获取value, 可以通过参数来影响get的行为
getResp, err := kv.Get(context.TODO(), "/test/", clientv3.WithPrefix())
putResp, err := kv.Put(context.TODO(), "/test/", clientv3.WithPrevKV())
delResp, err := kv.Delete(context.TODO(), "/test/", clientv3.WithPrevKV())
clientv3.WithPrefix(), 表示以"/test/"开始查找, 不再是完全匹配
clientv3.WithPrevKV(), 表示需要返回put时覆盖的值或 删除的值
5.获取Lease:
上面获取了Client的KV字段, 现在获取Client的Lease字段, 与获取KV类似
lease := clientv3.NewLease(client)
Lease也是接口, 提供如下功能:
type Lease interface {
Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
Leases(ctx context.Context) (*LeaseLeasesResponse, error)
KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)
Close() error
}
Grant 分配租约
Revoke 释放租约
TimeToLive 获取剩余存活时间(TTL)
Leases 获取所有租约
KeepAlive 续约租约
KeepAliveOnce 为租约续约一次
Close 关闭所有租约
7.创建租约并根据租约存入值
var kv = clientv3.NewKV(client) // 获取KV
lease := clientv3.NewLease(client) // 获取Lease
grantResp , err := lease.Grant(context.TODO(), 10) // 创建一个10秒钟的租约
if err!=nil {
return
}
// 根据租约put一个会过期的值(如果没有第四个参数, 则put的是永久的值)
kv.Put(context.TODO(), "/test/t", "value_test", clientv3.WithLease(grantResp.ID))
8.op操作 (在事务中得使用op操作)
使用op操作 增 删 查, 可以代替前面使用的kv.Put/Get 以及Delete
它是先包装好要使用的方式(get/put/delete), 再统一使用do来操作
直接使用kv操作和使用op操作, 它们其实是一样的
op := clientv3.OpGet("p")
opResp, err := kv.Do(context.TODO(), op)
if err != nil {
return
}
for _, val := range opResp.Get().Kvs { // 调用Get()
fmt.Println(string(val.Value))
}
-----------------------
op := clientv3.OpPut("x2", "xxx", clientv3.WithPrevKV())
// 第三个参数用于返回的值
opResp, err := kv.Do(context.TODO(), op) if err != nil {
return
}
// 它返回的是被覆盖的value值
// 如果之前里面没有该key, 则表示没有该key对应的value, 返回nil
// 所以在此情况下要考虑到空指针的情况
preKV := opResp.Put().PrevKv // 调用Put()
if preKV == nil {
fmt.Println("没有返回值")
} else {
fmt.Println(string(preKV.Value))
}
-----------------------
op := clientv3.OpDelete("x2", clientv3.WithPrevKV())
opResp, err := kv.Do(context.TODO(), op)
if err != nil {
return
}
value := opResp.Del().PrevKvs[0].Value // 调用Del()
fmt.Println(string(value))
它们的返回值是OpResponse, 根据包装的方式调用Get() / Put() / Del()
得到对应的*GetResponse / *PutResponse / *DeleteResponse, 其结构如下:
type OpResponse struct {
put *PutResponse
get *GetResponse
del *DeleteResponse
txn *TxnResponse
}
9.事务txn:
事务一般就是为了保持原子性操作
开启事务 --> txn := kv.Txn(context.TODO())
事务的使用 --> If(条件). Then(执行若干操作).Else(执行若干操作).Commit()
.If -- 事务的条件判断, 可以有多个条件
clientv3.Compare(clientv3.Value(key), "=", this_value)
.Then -- 当If为true时执行的操作
clientv3.OpGet(key)
.Else -- 当If的false时执行的操作
clientv3.OpPut(key, value)
.Commit -- 提交事务
func main() {
kv := clientv3.NewKV(client)
txn := kv.Txn(context.TODO()) // 开启事务
txnResp, err := txn.If(clientv3.Compare(clientv3.Value("abc"), "=", "abc1234")).
Then(clientv3.OpGet("a"), clientv3.OpGet("b")).
Else(clientv3.OpPut("abc", "abc123", clientv3.WithPrevKV())).
Commit()
if err != nil {
fmt.Println("err: ", err)
return
}
// 判断If操作是否成功
if txnResp.Succeeded {
// 和前面使用KV.Do()相似, 都是获取OpResponse
// 再调GetResponseRange()获取GetResponse(别名, 见下面) 或 调用GetResponsePut()获取PutResponse
fmt.Println(txnResp.Responses[0].GetResponseRange().Kvs)
// 在获取到Kvs时其实应该判断它的长度是否为0
if len(txnResp.Responses[1].GetResponseRange().Kvs) == 0 { // GetResponseRange
fmt.Println("没有放入kv")
return
}
fmt.Println(string(txnResp.Responses[1].GetResponseRange().Kvs[0].Value))
} else {
// 判断*KeyValue是否为空
if txnResp.Responses[0].GetResponsePut().PrevKv == nil { // GetResponsePut
fmt.Println("nil")
return
}
fmt.Println(string(txnResp.Responses[0].GetResponsePut().PrevKv.Value))
}
}
Txn的结构如下:
type Txn interface {
If(cs ...Cmp) Txn
Then(ops ...Op) Txn
Else(ops ...Op) Txn
Commit() (*TxnResponse, error)
}
注: 它这时里有别名的使用
即GetResponse -- RangeResponse
PutResponse -- PutResponse(这个是原样)
再看上面代码 txnResp.Responses[0].GetResponseRange().Kvs 就好理解了
type (
CompactResponse pb.CompactionResponse
PutResponse pb.PutResponse
GetResponse pb.RangeResponse
DeleteResponse pb.DeleteRangeResponse
TxnResponse pb.TxnResponse
)
方法名称真不好区分, 记住中转站: OpResponse, 通过它来获取返回类型
put -- GetResponsePut
get -- GetResponseRange
delete -- GetResponseDeleteRange