本文以参与[新人创作礼]活动,一起开启掘金创作之路
资料
看图轻松了解 etcd etcd 常用操作介绍 golang etcd 简明教程
启动 etcd
etcd --listen-client-urls 'http://0.0.0.0:2379' --advertise-client-urls 'http://0.0.0.0:2379'
连接
package main
import (
"context"
"fmt"
"github.com/coreos/etcd/clientv3"
"time"
)
func main(){
var (
config clientv3.Config
client *clientv3.Client
err error
kv clientv3.KV
putRes *clientv3.PutResponse
getRes *clientv3.GetResponse
)
//配置
config = clientv3.Config{
Endpoints:[]string{"127.0.0.1:2379"},
DialTimeout:5*time.Second,
}
//建立连接
if client,err = clientv3.New(config);err!=nil{
fmt.Println("client ok")
return
}
defer client.Close()
//写etcd中的键值对
kv = clientv3.NewKV(client)
if putRes,err = kv.Put(context.TODO(),"/cront/job/1","ok",clientv3.WithPrevKV());err!=nil{
fmt.Println(err)
}else{
//putRes.PrevKv输出如下:
/*
key:"/cron/jobs/job1" create_revision:43 mod_revision:46 version:4 value:"{\"name\":\"job1\",\"command\":\"echo hello\",\"crontab\":\"\"}"
*/
fmt.Println(putRes.Header.Revision) //输出Revision
fmt.Println(putRes.PrevKv.Value) //输出修改前的值
}
//读取etcd的键值
if getRes,err = kv.Get(context.TODO(),"/cront/job/1");err!=nil{
fmt.Println(err)
}else{
fmt.Println(getRes.Kvs)
}
}
租约
package main
import (
"context"
"fmt"
"github.com/coreos/etcd/clientv3"
"time"
)
func main() {
var (
config clientv3.Config
err error
lease clientv3.Lease
leaseGrantResp *clientv3.LeaseGrantResponse
leaseId clientv3.LeaseID
putRes *clientv3.PutResponse
kv clientv3.KV
client *clientv3.Client
keepRes *clientv3.LeaseKeepAliveResponse
keepResChan <-chan *clientv3.LeaseKeepAliveResponse
)
////配置
config = clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
}
//创建客户端
if client, err = clientv3.New(config); err != nil {
fmt.Println(err)
return
}
defer client.Close()
//申请一个lease
lease = clientv3.NewLease(client)
//申请一个10秒的租约
if leaseGrantResp, err = lease.Grant(context.TODO(), 10); err != nil {
fmt.Println(err)
return
}
//拿到租约ID
leaseId = leaseGrantResp.ID
//续租
if keepResChan,err = lease.KeepAlive(context.TODO(),leaseId);err!=nil{
fmt.Println(err)
return
}
//处理续租应答的协程
go func(){
for{
select{
case keepResp = <-keepResChan:
if keepResChan == nil{
fmt.Println("租约已经失效")
goto END
}else{
//每秒会续租一次,所以会收到一次应答
fmt.Println("收到自动续租应答",keepRes.ID)
}
}
}
END:
}()
//获取kv对象
kv = clientv3.NewKV(client)
//put一个kv让他和租约关联起来,从而实现10秒自动过期
if putRes,err = kv.Put(context.TODO(),"/cron/job/1","",clientv3.WithLease(leaseId));err!=nil{
fmt.Println(err)
return
}
fmt.Println("写入成功",putRes.Header.Revision)
}
watch
package main
import (
"context"
"fmt"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/mvcc/mvccpb"
"time"
)
func main() {
var (
config clientv3.Config
client *clientv3.Client
err error
kv clientv3.KV
getRes *clientv3.GetResponse
watchStartRevision int64
watcher clientv3.Watcher
watchResChan <-chan clientv3.WatchResponse
watchResp clientv3.WatchResponse
event *clientv3.Event
)
config = clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
}
if client, err = clientv3.New(config); err != nil {
fmt.Println(err)
return
}
defer client.Close()
kv = clientv3.NewKV(client)
go func() {
kv.Put(context.TODO(), "/cront/job/1", "ok")
kv.Delete(context.TODO(), "/cront/job/1")
time.Sleep(1 * time.Second)
}()
if getRes, err = kv.Get(context.TODO(), "/cront/job/1"); err != nil {
fmt.Println(err)
return
}
//现在key是存在的
if len(getRes.Kvs) != 0 {
fmt.Println("当前值:", getRes.Kvs)
fmt.Println("当前值:", string(getRes.Kvs[0].Value))
}
//当前etcd集群事务ID,单调递增的
watchStartRevision = getRes.Header.Revision + 1
//创建一个watcher
watcher = clientv3.NewWatcher(client)
//启动监听
fmt.Println("从该版本开始向后监听", watchStartRevision)
watchResChan = watcher.Watch(context.TODO(), "/cront/job/1", clientv3.WithRev(watchStartRevision))
//处理kv的变化事件
for watchResp = range watchResChan {
for _, event = range watchResp.Events {
switch event.Type {
case mvccpb.PUT:
fmt.Println("修改为:", string(event.Kv.Value), "Revision:", event.Kv.CreateRevision, event.Kv.ModRevision)
case mvccpb.DELETE:
fmt.Println("删除:", "Revision:", event.Kv.ModRevision)
}
}
}
}
事务
package main
import (
"context"
"fmt"
"time"
"github.com/coreos/etcd/clientv3"
)
func main() {
var (
config clientv3.Config
client *clientv3.Client
lease clientv3.Lease
leaseId clientv3.LeaseID
leaseGrantResp *clientv3.LeaseGrantResponse
keepResChan <-chan *clientv3.LeaseKeepAliveResponse
keepRes *clientv3.LeaseKeepAliveResponse
err error
ctx context.Context
cancelFunc context.CancelFunc
txn clientv3.Txn
txnResp *clientv3.TxnResponse
kv clientv3.KV
)
config = clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 10 * time.Second,
}
if client, err = clientv3.New(config); err != nil {
fmt.Println(err)
return
}
//1. 上锁,创建租约
lease = clientv3.NewLease(client)
//申请一个5秒的租约
if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil {
fmt.Println(err)
return
}
//拿到租约ID
leaseId = leaseGrantResp.ID
//取消续租
ctx, cancelFunc = context.WithCancel(context.TODO())
//确保函数推出后,自动续租会停止
defer cancelFunc()
defer lease.Revoke(context.TODO(), leaseId)
//续租
if keepResChan, err = lease.KeepAlive(ctx, leaseId); err != nil {
fmt.Println(err)
return
}
//处理续租应答的协程
go func() {
for {
select {
case keepRes = <-keepResChan:
if keepResChan == nil {
fmt.Println("租约已经失效")
goto END
} else {
//每秒会续租一次,所以会收到一次应答
fmt.Println("收到自动续租应答", keepRes.ID)
}
}
}
END:
}()
//if不存在key,then设置它,else抢锁失败
kv = clientv3.NewKV(client)
//创建事务
txn = kv.Txn(context.TODO())
//定义事务
txn.If(clientv3.Compare(clientv3.CreateRevision("/cront/lock/1"), "=", 0)).Then(clientv3.OpPut("/cron/lock/1", "", clientv3.WithLease(leaseId))).
Else(clientv3.OpGet("/cron/lock/1")) //否则抢锁失败
//提交事务
if txnResp, err = txn.Commit(); err != nil {
fmt.Println(err)
return
}
//判断是否抢到锁
if !txnResp.Succeeded {
fmt.Println("锁被占用:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))
}
//2.处理事务
fmt.Println("处理事务")
time.Sleep(5 * time.Second)
//defer释放锁
}
go.etcd.io/etcd/clientv3 库
安装
go get go.etcd.io/etcd/clientv3
put 和 get 操作
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
// etcd client put/get demo
// use etcd/clientv3
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
// handle error!
fmt.Printf("connect to etcd failed, err:%v\n", err)
return
}
fmt.Println("connect to etcd success")
defer cli.Close()
// put
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_, err = cli.Put(ctx, "q1mi", "dsb")
cancel()
if err != nil {
fmt.Printf("put to etcd failed, err:%v\n", err)
return
}
// get
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
resp, err := cli.Get(ctx, "q1mi")
cancel()
if err != nil {
fmt.Printf("get from etcd failed, err:%v\n", err)
return
}
for _, ev := range resp.Kvs {
fmt.Printf("%s:%s\n", ev.Key, ev.Value)
}
}
watch 操作
watch 用来获取未来更改的通知。
package main
import (
"context"
"fmt"
"time"
"go.etcd.io/etcd/clientv3"
)
// watch demo
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")
defer cli.Close()
// watch key:q1mi change
rch := cli.Watch(context.Background(), "q1mi") // <-chan WatchResponse
for wresp := range rch {
for _, ev := range wresp.Events {
fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
}
将上面的代码保存编译执行,此时程序就会等待 etcd 中 q1mi 这个 key 的变化。
例如:我们打开终端执行以下命令修改、删除、设置 q1mi 这个 key。
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb2"
OK
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 del q1mi
1
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb3"
OK
上面的程序都能收到如下通知。
watch>watch.exe
connect to etcd success
Type: PUT Key:q1mi Value:dsb2
Type: DELETE Key:q1mi Value:
Type: PUT Key:q1mi Value:dsb3
lease 租约
package main
import (
"fmt"
"time"
)
// etcd lease
import (
"context"
"log"
"go.etcd.io/etcd/clientv3"
)
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: time.Second * 5,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to etcd success.")
defer cli.Close()
// 创建一个5秒的租约
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
// 5秒钟之后, /nazha/ 这个key就会被移除
_, err = cli.Put(context.TODO(), "/nazha/", "dsb", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
}
keepAlive
package main
import (
"context"
"fmt"
"log"
"time"
"go.etcd.io/etcd/clientv3"
)
// etcd keepAlive
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: time.Second * 5,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("connect to etcd success.")
defer cli.Close()
resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
log.Fatal(err)
}
_, err = cli.Put(context.TODO(), "/nazha/", "dsb", clientv3.WithLease(resp.ID))
if err != nil {
log.Fatal(err)
}
// the key 'foo' will be kept forever
ch, kaerr := cli.KeepAlive(context.TODO(), resp.ID)
if kaerr != nil {
log.Fatal(kaerr)
}
for {
ka := <-ch
fmt.Println("ttl:", ka.TTL)
}
}
基于 etcd 实现分布式锁
go.etcd.io/etcd/clientv3/concurrency 在 etcd 之上实现并发操作,如分布式锁、屏障和选举。导入该包:
import "go.etcd.io/etcd/clientv3/concurrency"
基于 etcd 实现的分布式锁示例:
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
// 创建两个单独的会话用来演示锁竞争
s1, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "/my-lock/")
s2, err := concurrency.NewSession(cli)
if err != nil {
log.Fatal(err)
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "/my-lock/")
// 会话s1获取锁
if err := m1.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("acquired lock for s1")
m2Locked := make(chan struct{})
go func() {
defer close(m2Locked)
// 等待直到会话s1释放了/my-lock/的锁
if err := m2.Lock(context.TODO()); err != nil {
log.Fatal(err)
}
}()
if err := m1.Unlock(context.TODO()); err != nil {
log.Fatal(err)
}
fmt.Println("released lock for s1")
<-m2Locked
fmt.Println("acquired lock for s2")
输出:
acquired lock for s1
released lock for s1
acquired lock for s2