etcd

380 阅读3分钟

本文以参与[新人创作礼]活动,一起开启掘金创作之路

资料

看图轻松了解 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