etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。
etcd具有以下特点:
所有服务将元信息存储到以某个 prefix 开头的 key 中,然后消费者从这些 key 中获取服务信息并调用。消费者也可以 watch 这些 key 的变更,以便在服务增加和减少时及时获得通知。
应用将配置信息存放到 etcd,当配置信息被更改时可以通过 watch 机制从 etcd 及时获得通知。
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。由于 etcd 中的数据是一致的,当多个应用同时去创建一个 key 时只有一个会成功,创建成功的应用即获取了锁。etcd 还有更多的应用场景,例如集群监控,Leader 竞选等。需要注意的是,应该使用 etcd 来存储一个关键的控制数据,对于应用数据应该只在数据量较小时存储。
安装包下载地址:https://github.com/etcd-io/etcd/releases,选择对应系统下载
下载完成后解压,解压文件中有etcd,etcdurl两个二进制文件,分别是etcd服务端和客户端,执行./etcd启动服务,etcd目前默认使用2379端口提供HTTP API服务
本文主要介绍如何使用Go语言操作etcd,命令行使用不做介绍
导入包:
go get go.etcd.io/etcd/clientv3
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
}
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
// 创建客户端,连接etcd
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// put操作
_, err = cli.Put(context.TODO(), "testetcd", "xxxxx")
if err != nil {
fmt.Printf("put to etcd failed, err:%v\n", err)
return
}
// get操作
resp, err := cli.Get(context.TODO(),"testetcd") // 获取指定K的值
if err != nil {
fmt.Printf("get to etcd failed")
return
}
for _, ev := range resp.Kvs {
fmt.Printf("key:%s,value:%s\n", ev.Key, ev.Value)
}
// delete 删除key
if _,err = cli.Delete(context.TODO(), "testetcd");err != nil{
fmt.Println("delete key failed")
}else{
fmt.Println("delete key success")
}
}
输出结果:
connect to etcd success
key:testetcd,value:xxxxx
delete key success
watch用来获取更改的通知,即监控key的变化
func main() {
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Println(err)
return
}
// 创建一个watch
// watch 返回一个channel
watchChan := cli.Watch(context.Background(), "watch")
// 循环channel获取更改返回值
for wresp := range watchChan {
for _, ev := range wresp.Events {
fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key)
}
}
}
我们用命令行操作一个key:
wrz@192 etcd-v3.5.0-darwin-amd64 % ./etcdctl put "watch" "watch测试"
OK
wrz@192 etcd-v3.5.0-darwin-amd64 % ./etcdctl del "watch"
1
输出结果:
Type: PUT Key:watch Value:watch测试
Type: DELETE Key:watch
Lease 提供了以下功能:
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Println(err)
return
}
// 创建一个10秒的租约,
leaseGrandResp,err := cli.Grant(context.TODO(), 10)
if err != nil {
fmt.Println(err)
return
}
// 获取租约ID
leaseID := leaseGrandResp.ID
// 10秒钟之后, lease 这个key就会被移除
// put创建key的时候使用clientv3.WithLease(leaseID)绑定租约ID
cli.Put(context.TODO(), "lease", "这是一个租约", clientv3.WithLease(leaseID))
for{
getResp,_ := cli.Get(context.TODO(), "TestLease")
if getResp.Count > 0 {
fmt.Println("租约ID:", leaseID,"key:", string(getResp.Kvs[0].Key),"value:",string(getResp.Kvs[0].Value))
}else{
fmt.Println("租约过期了")
return
}
time.Sleep(2*time.Second)
}
}
输出结果:
租约ID: 7587855531968189791 key: TestLease value: 这是一个租约
租约ID: 7587855531968189791 key: TestLease value: 这是一个租约
租约ID: 7587855531968189791 key: TestLease value: 这是一个租约
租约ID: 7587855531968189791 key: TestLease value: 这是一个租约
租约过期了
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Println(err)
return
}
// 创建一个10秒的租约
leaseGrandResp,err := cli.Grant(context.TODO(), 10)
if err != nil {
fmt.Println(err)
return
}
// 获取租约ID
leaseID := leaseGrandResp.ID
// 10秒钟之后, lease 这个key就会被移除
// put创建key的时候使用clientv3.WithLease(leaseID)绑定租约ID
cli.Put(context.TODO(), "TestLease", "这是一个租约", clientv3.WithLease(leaseID))
// keepAlive 续租,2秒续租一次
for{
kach,_ := cli.KeepAlive(context.TODO(), leaseID)
ka := <-kach
fmt.Println("ttl:", ka.TTL)
getResp,_ := cli.Get(context.TODO(), "TestLease")
if getResp.Count > 0 {
fmt.Println("租约ID:", leaseID,"key:", string(getResp.Kvs[0].Key),"value:",string(getResp.Kvs[0].Value))
}else{
fmt.Println("租约过期了")
return
}
time.Sleep(2*time.Second)
}
}
输出结果:
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
ttl: 10
租约ID: 7587855531968189807 key: TestLease value: 这是一个租约
可以看到租约不会过期了
op操作是实现分布式锁的基础,Op 是一个抽象的操作,可以是 Put/Get/Delete… ;而 OpResponse 是一个抽象的结果,可以是 PutResponse/GetResponse…,根据不同的操作返回不同结果。如 OpPut()返回PutResponse
常用方法:
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Println(err)
return
}
// 创建OP
putOp := clientv3.OpPut("/op", "op操作")
// 执行OP
putResp,_ := cli.Do(context.TODO(), putOp)
fmt.Println("创建Revision:", putResp.Put().Header.Revision)
getOp := clientv3.OpGet("/op")
getResp,_ := cli.Do(context.TODO(), getOp)
fmt.Printf("Key:%s,Value:%s", string(getResp.Get().Kvs[0].Key), string(getResp.Get().Kvs[0].Value))
}
输出结果:
创建Revision: 125
Key:/op,Value:op操作
etcd 中事务是原子执行的,只支持 if … then … else … 这种表达式
使用txn实现一个分布式锁:
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"time"
)
func main() {
cli,err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5*time.Second,
})
if err != nil {
fmt.Println(err)
return
}
// lease实现锁自动过期:
// op操作
// txn事务: if else then
// 1, 上锁 (创建租约, 自动续租, 拿着租约去抢占一个key)
lease := clientv3.NewLease(cli)
// 申请一个5s的租约
leaseGrandResp,err := lease.Grant(context.TODO(), 5)
if err != nil{
fmt.Println(err)
return
}
// 获取租约ID
leaseID := leaseGrandResp.ID
// 创建一个用于取消自动续租的context
ctx, cancelFunc := context.WithCancel(context.TODO())
// 确保函数退出后, 自动续租会停止
defer cancelFunc()
defer lease.Revoke(context.TODO(), leaseID)
// 续租
keepRespChan, err := lease.KeepAlive(ctx, leaseID)
if err != nil {
fmt.Println(err)
return
}
// 处理续约应答的协程
go func() {
for {
select {
case keepResp := <-keepRespChan:
if keepResp == nil {
fmt.Println("租约过期了")
goto END
} else {
fmt.Println("收到自动续租应答:", keepResp.ID)
}
}
}
END:
}()
// if 不存在key, then 设置它, else 抢锁失败
kv := clientv3.NewKV(cli)
// 创建事务
txn := kv.Txn(context.TODO())
// 定义事务
// 如果key不存在
txn.If(clientv3.Compare(clientv3.CreateRevision("/task/job/job1"), "=", 0)).
Then(clientv3.OpPut("/task/job/job1", "xxx", clientv3.WithLease(leaseID))).
Else(clientv3.OpGet("/task/job/job1")) // 否则抢锁失败
// 提交事务
txnResp, err := txn.Commit()
if err != nil {
fmt.Println(err)
return // 没有问题
}
// 判断是否抢到了锁
if !txnResp.Succeeded {
fmt.Println("锁被占用:", string(
txnResp.Responses[0].GetResponseRange().Kvs[0].Value))
return
}
// 2, 处理业务,这里写自己的处理逻辑
fmt.Println("处理任务")
time.Sleep(5 * time.Second)
// 3, 释放锁(取消自动续租, 释放租约)
// defer 会把租约释放掉, 关联的KV就被删除了
}
启动两个终端:
wrz@192 txn % go run main.go
收到自动续租应答: 7587855531968189824
处理任务
收到自动续租应答: 7587855531968189824
收到自动续租应答: 7587855531968189824
wrz@192 txn % go run main.go
收到自动续租应答: 7587855531968189827
锁被占用: xxx
参考资料:
https://www.bookstack.cn/read/etcd/README.md
https://godoc.org/github.com/coreos/etcd/clientv3