etcd的使用(GO)

Etcd教程 — 第一章 Etcd简介、Etcd单机安装 阅读详情

etcd是一个非常可靠的kv存储系统,常在分布式系统中存储着关键的数据。它是由coreos团队开发并开源的分布式键值存储系统,具备以下特点:

  • 简单:提供定义明确且面向用户的API
  • 安全:支持SSL证书验证
  • 性能:基准压测支持1w+/sec写入
  • 可靠:采用Raft协议保证分布式系统数据的可用性和一致性。

etcd的这些特性,使得它常常出现在分布式设计场景下的工具集中。

1. 下载

1.1 windows

打开GitHub的上的etcd项目,直接下载压缩包就可以了
github地址
在这里插入图片描述
这里我下载的是3.3.13版本的,直接解压,然后运行etcd.exe就可以了
在这里插入图片描述
在这里插入图片描述
etcd 目前默认使用 2379提供 HTTP API 服务, 2380 端口和peer 通信(这两个端口已经被 IANA 官方预留给 etcd )

2. 连接

注意:
默认的etccdctrl使用的是v2版本的命令。我们需要设置环境变量来使用v3版本的API

2.1 修改环境变量(window)

控制台输入命令

set ETCDCTL_API=3

在这里插入图片描述

3. 简单使用

3.1 etcdctl

etcdctl 是一个命令行客户端,它能提供一些简洁的命令,供用户直接跟etcd服务打交道,而无需基于 HTTP API 方式。可以方便我们在对服务进行测试或者手动修改数据库内容。我们刚开始可以通过 etdctl 来熟悉相关操作。这些操作跟 HTTP API 基本上是对应的。etcdctl 在两个不同的 etcd 版本下的行为方式也完全不同。

put

使用etccdctl 来put一个键值对进去
在这里插入图片描述
返回OK
在这里插入图片描述

get

根据key来获取value
在这里插入图片描述

del

根据key删除键值对
在这里插入图片描述

3.2 Go语言

安装 Golang 的 Etcd 包

我们使用 v3 版本的 etcd client , 首先通过 go get 下载并编译安装 etcd clinet v3。

go get -v github.com/coreos/etcd/clientv3

该命令会将包下载到 $GOPATH/src/github.com/coreos/etcd/clientv3 中,所有相关依赖包会自动下载编译,包括protobuf、grpc等。

连接ETCD

用程序访问 etcd 首先要创建 client ,它需要传入一个 Config 配置,这里传了 2 个参数:

  • Endpoints : etcd 的多个节点服务地址;
  • DialTimeout :创建 client 的首次连接超时时间,这里传了 5 秒,如果 5 秒都没有连接成功就会返回 err ,一旦 client 创建成功,我们就不用再关心后续底层连接的状态了, client 内部会重连;
cli, err := clientv3.New(clientv3.Config{
   Endpoints:   []string{"localhost:2379"},
   // Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"}
   DialTimeout: 5 * time.Second,
})

返回的client,类型中的成员是 etcd 客户端核心功能模块的具体实现,它的类型具体如下:

type Client struct {
    Cluster  //向集群里增加 etcd 服务端节点之类,属于管理员操作。
    KV 		 //我们主要使用的功能,即 K-V 键值库的操作。
    Lease    //租约相关操作,比如申请一个 TTL=10 秒的租约(应用给 key 可以实现键值的自动过期)。
    Watcher  //观察订阅,从而监听最新的数据变化。
    Auth     //管理 etcd 的用户和权限,属于管理员操作。
    Maintenance   //维护 etcd ,比如主动迁移 etcd 的 leader 节点,属于管理员操作。
    
    // Username is a user name for authentication.
    Username string
    // Password is a password for authentication.
    Password string
    // contains filtered or unexported fields
}

我们需要使用什么功能,就去 client 里获取对应的成员即可。
Client.KV 是一个 interface ,提供了关于 K-V 操作的所有方法:

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 deletes a key, or optionally using WithRange(end), [key, end).
    Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)

    // Compact compacts etcd KV history before the given rev.
    Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)

    Do(ctx context.Context, op Op) (OpResponse, error)

    // Txn creates a transaction.
    Txn(ctx context.Context) Txn
}

我们通过方法clientv3.NewKV()来获得 KV 接口的实现(实现中内置了错误重试机制):

kv := clientv3.NewKV(cli)

接下来,我们就可以通过kv来操作etcd中的数据。

PUT
putResp, err := kv.Put(context.TODO(),"/test/key1", "Hello etcd!")

第一个参数是 goroutine 的上下文 Context 。后面两个参数分别是 key 和 value ,对于 etcd 来说, key=/test/key1 只是一个字符串而已,但是对我们而言却可以模拟出目录层级关系。

Put的声明如下:

// Put puts a key-value pair into etcd.
// Note that key,value can be plain bytes array and string is
// an immutable representation of that bytes array.
// To get a string of bytes, do string([]byte{0x10, 0x20}).
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)

除了上面例子中的三个的参数,还支持一个变长参数,可以传递一些控制项来影响 Put 的行为,例如可以携带一个 lease ID 来支持 key 过期。

Put 操作返回的是 PutResponse ,不同的 KV 操作对应不同的 response 结构,所有 KV 操作返回的 response 结构如下:

type (
   CompactResponse pb.CompactionResponse
   PutResponse     pb.PutResponse
   GetResponse     pb.RangeResponse
   DeleteResponse  pb.DeleteRangeResponse
   TxnResponse     pb.TxnResponse
)

程序代码里导入 clientv3 后在 VSCode 中可以很快定位到 PutResponse 的定义文件中, PutResponse 只是 pb.PutResponse 的类型别名,通过VSCode跳转过去后可以看到 PutResponse 的详细定义:

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 信息,而 PrevKv 可以返回 Put 覆盖之前的 value 是什么(目前是 nil ,后面会说原因),我们需要判断 err 来确定操作是否成功。
我们Put 其他 3 个 key ,用于后续演示:

putResp, err := kv.Put(context.TODO(),"/test/key1", "Hello etcd!")
if err != nil{
	fmt.print("put failed!")
}
kv.Put(context.TODO(),"/test/key2", "Hello World!")
// 再写一个同前缀的干扰项
kv.Put(context.TODO(), "/testspam", "spam")

现在 /test 目录下有两个键: key1 和 key2 , 而 /testspam 并不归属于 /test 目录。

代码:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"127.0.0.1:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 用于读写etcd的键值对
	kv := clientv3.NewKV(client)
	
	// clientv3.WithPrevKV() 是一个可选控制项,用于获取在设置当前键值对之前的该键的键值对
	// 有了该控制项后,putResp 才有 PrevKv 的属性,即获取之前的键值对。
	// context.TODO() 表示当前还不知道用哪个 context 控制该操作,先用该字段占位
	putResp, err := kv.Put(context.TODO(), "/demo/A/B", "hello", clientv3.WithPrevKV())

	if err != nil {
		fmt.Println(err)
	}

	fmt.Println("putResp is ", putResp)
	fmt.Println("Revision:", putResp.Header.Revision)
	if putResp.PrevKv != nil {
		fmt.Println("PrevValue:", string(putResp.PrevKv.Value))
	}
	putResp, err = kv.Put(context.TODO(),"/test/key1", "Hello etcd!")
	if err != nil{
		fmt.print("put failed!")
	}
	kv.Put(context.TODO(),"/test/key2", "Hello World!")
	// 再写一个同前缀的干扰项
	kv.Put(context.TODO(), "/testspam", "spam")
}

GET

使用 KV 的 Get 方法来读取给定键的值:

getResp, err := kv.Get(context.TODO(), "/test/key1")

函数声明如下:

// Get retrieves keys.
// By default, Get will return the value for "key", if any.
// When passed WithRange(end), Get will return the keys in the range [key, end).
// When passed WithFromKey(), Get returns keys greater than or equal to key.
// When passed WithRev(rev) with rev > 0, Get retrieves keys at the given revision;
// if the required revision is compacted, the request will fail with ErrCompacted .
// When passed WithLimit(limit), the number of returned keys is bounded by limit.
// When passed WithSort(), the keys will be sorted.
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)

和 Put 类似,函数注释里提示我们可以传递一些控制参数来影响 Get 的行为,比如: WithFromKey 表示读取从参数 key 开始递增的所有 key ,而不是读取单个 key 。

在上面的例子中,我没有传递 opOption ,所以就是获取 key=/test/key1 的最新版本数据。这里 err 并不能反馈出 key 是否存在(只能反馈出本次操作因为各种原因异常了),我们需要通过 GetResponse (实际上是 pb.RangeResponse )判断 key 是否存在
让我们看一下返回值的类型:

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 查询到的所有 k-v 对,因为上述例子只 Get 了一个单 key ,所以只需要判断一下 len(Kvs) 是否等于 1 即可知道 key 是否存在。

RangeResponse.More和Count,当我们使用withLimit()等选项进行 Get 时会发挥作用,相当于翻页查询。
接下来,我们通过给 Get 查询增加 WithPrefix 选项,获取 /test 目录下的所有子元素:

rangeResp, err := kv.Get(context.TODO(), "/test/", clientv3.WithPrefix())

WithPrefix()是指查找以/test/为前缀的所有 key ,因此可以模拟出查找子目录的效果。

etcd是一个有序的 k-v 存储,因此 /test/ 为前缀的 key 总是顺序排列在一起。

withPrefix()实际上会转化为范围查询,它根据前缀/test/生成了一个前闭后开的key range:[“/test/”, “/test0”),为什么呢?因为比/(47)大的字符是0(48),所以以/test0作为范围的末尾,就可以扫描到所有以/test/为前缀的 key 了。

在之前,我们 Put 了一个/testspam键值,因为不符合/test/前缀(注意末尾的 / ),所以就不会被这次Get获取到。但是,如果查询的前缀是/test,那么/testspam就会被返回,使用时一定要特别注意。

打印 rangeResp.Kvs 可以看到获得了两个键值:

[key:"/test/key1" create_revision:2 mod_revision:13 version:6 value:"Hello etcd!"  
key:"/test/key2" create_revision:5 mod_revision:14 version:4 value:"Hello World!" ]

代码:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"192.168.0.113:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 用于读写etcd的键值对
	kv := clientv3.NewKV(client)

	kv.Put(context.TODO(), "/demo/A/B", "BBB", clientv3.WithPrevKV())
	kv.Put(context.TODO(), "/demo/A/C", "CCC", clientv3.WithPrevKV())
	// 	读取/demo/A/为前缀的所有key
	// clientv3.WithPrefix() , clientv3.WithCountOnly() 可以有多个并以 逗号分隔即可
	getResp, err := kv.Get(context.TODO(), "/demo/A/", clientv3.WithPrefix() /*,clientv3.WithCountOnly()*/)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(getResp.Kvs, getResp.Count)
	for _, resp := range getResp.Kvs {
		fmt.Printf("key: %s, value:%s\n", string(resp.Key), string(resp.Value))
	}
}

结果:

[key:"/demo/A/B" create_revision:6 mod_revision:22 version:6 value:"BBB"  
key:"/demo/A/C" create_revision:7 mod_revision:23 version:12 value:"CCC" ] 2
key: /demo/A/B, value:BBB
key: /demo/A/C, value:CCC

Delete

代码:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"192.168.0.113:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 用于读写etcd的键值对
	kv := clientv3.NewKV(client)

	kv.Put(context.TODO(), "/demo/A/B1", "BBB", clientv3.WithPrevKV())
	kv.Put(context.TODO(), "/demo/A/B2", "CCC", clientv3.WithPrevKV())
	kv.Put(context.TODO(), "/demo/A/B3", "DDD", clientv3.WithPrevKV())
	/*
		clientv3.WithFromKey() 表示针对的key操作是大于等于当前给定的key
		clientv3.WithPrevKV() 表示返回的 response 中含有之前删除的值,否则
		下面的 delResp.PrevKvs 为空
	*/
	delResp, err := kv.Delete(context.TODO(), "/demo/A/B",
		clientv3.WithFromKey(), clientv3.WithPrevKV())
	if err != nil {
		fmt.Println(err)
	}
	// 查看被删除的 key 和 value 是什么
	if delResp.PrevKvs != nil {
		// if len(delResp.PrevKvs) != 0 {
		for _, kvpair := range delResp.PrevKvs {
			fmt.Println("已删除:", string(kvpair.Key), string(kvpair.Value))
		}
	}
}

结果:

已删除: /demo/A/B1 BBB
已删除: /demo/A/B2 CCC
已删除: /demo/A/B3 DDD
Lease续租

etcd客户端的Lease对象可以通过以下代码获取到

lease := clientv3.NewLease(cli)

其中,lease 对象是 Lease 接口的实现, Lease 接口的声明如下:

type Lease interface {
    // Grant 创建一个新租约
    Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
    // Revoke 销毁给定租约ID的租约
    Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
    // TimeToLive retrieves the lease information of the given lease ID.
    TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
    // Leases retrieves all leases.
    Leases(ctx context.Context) (*LeaseLeasesResponse, error)
    // KeepAlive keeps the given lease alive forever.
    KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
    // KeepAliveOnce renews the lease once. In most of the cases, KeepAlive
    // should be used instead of KeepAliveOnce.
    KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)
    // Close releases all resources Lease keeps for efficient communication
    // with the etcd server.
    Close() error
}

Lease 提供了以下功能:

  • Grant :分配一个租约;
  • Revoke :释放一个租约;
  • TimeToLive :获取剩余TTL时间;
  • Leases :列举所有etcd中的租约;
  • KeepAlive :自动定时的续约某个租约;
  • KeepAliveOnce :为某个租约续约一次;
  • Close :释放当前客户端建立的所有租约;
    要想实现 key 自动过期,首先得创建一个租约,下面的代码创建一个 TTL 为 10 秒的租约:
grantResp, err := lease.Grant(context.TODO(), 10)

返回的 grantResponse 的结构体声明如下:

// LeaseGrantResponse wraps the protobuf message LeaseGrantResponse.
type LeaseGrantResponse struct {
    *pb.ResponseHeader
    ID    LeaseID
    TTL   int64
    Error string
}

在应用程序代码中主要使用到的是租约 ID 。

接下来我们用这个 Lease 往 etcd 中存储一个 10 秒过期的 key :

kv.Put(context.TODO(), "/test/vanish", "vanish in 10s", clientv3.WithLease(grantResp.ID))

这里特别需要注意,有一种情况是在 Put 之前 Lease 已经过期了,那么这个 Put 操作会返回 error ,此时你需要重新分配 Lease 。

当我们实现服务注册时,需要主动给 Lease 进行续约,通常是以小于 TTL 的间隔循环调用 Lease 的 KeepAliveOnce() 方法对租约进行续期,一旦某个服务节点出错无法完成租约的续期,等 key 过期后客户端即无法在查询服务时获得对应节点的服务,这样就通过租约到期实现了服务的错误隔离。

keepResp, err := lease.KeepAliveOnce(context.TODO(), grantResp.ID)

或者使用KeepAlive()方法,其会返回<-chan *LeaseKeepAliveResponse只读通道,每次自动续租成功后会向通道中发送信号。

一般都用KeepAlive()方法, KeepAlive 和 Put 一样,如果在执行之前 Lease 就已经过期了,那么需要重新分配 Lease 。 etcd 并没有提供 API 来实现原子的 Put with Lease ,需要我们自己判断 err 重新分配 Lease 。

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"192.168.0.113:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 创建一个lease(租约)对象
	lease := clientv3.NewLease(client)
	// 申请一个10秒的租约
	leaseGrantResp, err := lease.Grant(context.TODO(), 10)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 拿到租约的ID
	leaseId := leaseGrantResp.ID

	// 自动永久续租
	keepRespChan, err := lease.KeepAlive(context.TODO(), 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:
	}()

	// 获得kv API子集
	kv := clientv3.NewKV(client)

	// Put一个KV, 让它与租约关联起来, 从而实现10秒后自动过期
	putResp, err := kv.Put(context.TODO(), "/demo/A/B1", "hello", clientv3.WithLease(leaseId))
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("写入成功:", putResp.Header.Revision)

	// 定时的看一下key过期了没有
	for {
		getResp, err := kv.Get(context.TODO(), "/demo/A/B1")
		if err != nil {
			fmt.Println(err)
			return
		}
		if getResp.Count == 0 {
			fmt.Println("kv过期了")
			break
		}
		fmt.Println("还没过期:", getResp.Kvs)
		time.Sleep(2 * time.Second)
	}
}

结果:

收到自动续租应答: 8488292048996991588
写入成功: 80
还没过期: [key:"/demo/A/B1" create_revision:80 mod_revision:80 version:1 value:"hello" lease:8488292048996991588 ]
还没过期: [key:"/demo/A/B1" create_revision:80 mod_revision:80 version:1 value:"hello" lease:8488292048996991588 ]
收到自动续租应答: 8488292048996991588
还没过期: [key:"/demo/A/B1" create_revision:80 mod_revision:80 version:1 value:"hello" lease:8488292048996991588 ]
还没过期: [key:"/demo/A/B1" create_revision:80 mod_revision:80 version:1 value:"hello" lease:8488292048996991588 ]
收到自动续租应答: 8488292048996991588
Op 获取设置联合操作

Op 字面意思就是”操作”, Get 和 Put 都属于 Op ,只是为了简化用户开发而开放的特殊 API 。

KV 对象有一个 Do 方法接受一个 Op :

// Do applies a single Op on KV without a transaction.
// Do is useful when creating arbitrary operations to be issued at a
// later time; the user can range over the operations, calling Do to
// execute them. Get/Put/Delete, on the other hand, are best suited
// for when the operation should be issued at the time of declaration.
Do(ctx context.Context, op Op) (OpResponse, error)

其参数 Op 是一个抽象的操作,可以是 Put/Get/Delete… ;而 OpResponse 是一个抽象的结果,可以是 PutResponse/GetResponse…

可以通过 Client 中定义的一些方法来创建 Op :

  • func OpDelete(key string, opts …OpOption) Op
  • func OpGet(key string, opts …OpOption) Op
  • func OpPut(key, val string, opts …OpOption) Op
  • func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op

其实和直接调用 KV.Put , KV.GET 没什么区别。下面是一个例子:

cli, err := clientv3.New(clientv3.Config{
    Endpoints:   endpoints,
    DialTimeout: dialTimeout,
})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()
ops := []clientv3.Op{
    clientv3.OpPut("put-key", "123"),
    clientv3.OpGet("put-key"),
    clientv3.OpPut("put-key", "456")}
for _, op := range ops {
    if _, err := cli.Do(context.TODO(), op); err != nil {
        log.Fatal(err)
    }
}

把 Op 交给 Do 方法执行,返回的 opResp 结构如下:

type OpResponse struct {
    put *PutResponse
    get *GetResponse
    del *DeleteResponse
    txn *TxnResponse
}

你的操作是什么类型,你就用哪个指针来访问对应的结果。

示例代码:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"192.168.0.113:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 获得kv API子集
	kv := clientv3.NewKV(client)

	// 创建Op: operation
	putOp := clientv3.OpPut("/demo/A/B1", "BBBBB")

	// 执行OP 	// kv.Do(op)
	opResp, err := kv.Do(context.TODO(), putOp)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("写入Revision:", opResp.Put().Header.Revision)

	// 创建Op
	getOp := clientv3.OpGet("/demo/A/B1")

	// 执行OP
	opResp, err = kv.Do(context.TODO(), getOp)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 打印 create rev == mod rev
	fmt.Println("数据Revision:", opResp.Get().Kvs[0].ModRevision) 
	fmt.Println("数据value:", string(opResp.Get().Kvs[0].Value))
}

结果:

写入Revision: 105
数据Revision: 105
数据value: BBBBB
Txn事务操作

etcd 中事务是原子执行的,只支持 if … then … else … 这种表达。首先来看一下 Txn 中定义的方法:

type Txn interface {
    // If takes a list of comparison. If all comparisons passed in succeed,
    // the operations passed into Then() will be executed. Or the operations
    // passed into Else() will be executed.
    If(cs ...Cmp) Txn
    // Then takes a list of operations. The Ops list will be executed, if the
    // comparisons passed in If() succeed.
    Then(ops ...Op) Txn
    // Else takes a list of operations. The Ops list will be executed, if the
    // comparisons passed in If() fail.
    Else(ops ...Op) Txn
    // Commit tries to commit the transaction.
    Commit() (*TxnResponse, error)
}

Txn 必须是这样使用的:If(满足条件) Then(执行若干Op) Else(执行若干Op)。

If 中支持传入多个 Cmp 比较条件,如果所有条件满足,则执行 Then 中的 Op (上一节介绍过Op),否则执行 Else中 的 Op 。

首先,我们需要开启一个事务,这是通过 KV 对象的方法实现的:

txn := kv.Txn(context.TODO())

下面的测试程序,判断如果 k1 的值大于 v1 并且 k1 的版本号是 2,则 Put 键值 k2 和 k3 ,否则 Put 键值 k4 和 k5 。

kv.Txn(context.TODO()).If(
 clientv3.Compare(clientv3.Value(k1), ">", v1),
 clientv3.Compare(clientv3.Version(k1), "=", 2)
).Then(
 clientv3.OpPut(k2,v2), clentv3.OpPut(k3,v3)
).Else(
 clientv3.OpPut(k4,v4), clientv3.OpPut(k5,v5)
).Commit()

类似于 clientv3.Value() 用于指定 key 属性的,有这么几个方法:

  • func CreateRevision(key string) Cmp:key=xxx的创建版本必须满足…
  • func LeaseValue(key string) Cmp:key=xxx的Lease ID必须满足…
  • func ModRevision(key string) Cmp:key=xxx的最后修改版本必须满足…
  • func Value(key string) Cmp:key=xxx的创建值必须满足…
  • func Version(key string) Cmp:key=xxx的累计更新次数必须满足…
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"192.168.0.113:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	// lease实现锁自动过期:
	// op操作
	// txn事务: if else then

	// 1, 上锁 (创建租约, 自动续租, 拿着租约去抢占一个key)
	lease := clientv3.NewLease(client)

	// 申请一个5秒的租约
	leaseGrantResp, err := lease.Grant(context.TODO(), 5)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 拿到租约的ID
	leaseId := leaseGrantResp.ID

	// 准备一个用于取消自动续租的context
	ctx, cancelFunc := context.WithCancel(context.TODO())

	// 确保函数退出后, 自动续租会停止
	defer cancelFunc()
	defer lease.Revoke(context.TODO(), leaseId)

	// 5秒后会取消自动续租
	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(client)

	// 创建事务
	txn := kv.Txn(context.TODO())

	// 定义事务

	// 如果key不存在
	txn.If(clientv3.Compare(clientv3.CreateRevision("/demo/A/B1"), "=", 0)).
		Then(clientv3.OpPut("/demo/A/B1", "xxx", clientv3.WithLease(leaseId))).
		Else(clientv3.OpGet("/demo/A/B1")) // 否则抢锁失败

	// 提交事务
	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就被删除了
}

结果:

收到自动续租应答: 8488292048996991680
锁被占用: BBBBB
Watch 监听操作

Watch 用于监听某个键的变化, Watch调用后返回一个WatchChan,它的类型声明如下:

type WatchChan <-chan WatchResponse
type WatchResponse struct {
    Header pb.ResponseHeader
    Events []*Event
    CompactRevision int64
    Canceled bool
    Created bool
}

当监听的 key 有变化后会向WatchChan发送WatchResponse。

Watch 的典型应用场景是应用于系统配置的热加载,我们可以在系统读取到存储在 etcd key 中的配置后,用 Watch 监听 key 的变化。在单独的 goroutine 中接收 WatchChan 发送过来的数据,并将更新应用到系统设置的配置变量中,比如像下面这样在 goroutine 中更新变量 appConfig ,这样系统就实现了配置变量的热加载。

type AppConfig struct {
  config1 string
  config2 string
}

var appConfig Appconfig

func watchConfig(clt *clientv3.Client, key string, ss interface{}) {
    watchCh := clt.Watch(context.TODO(), key)
    go func() {
        for res := range watchCh {
            value := res.Events[0].Kv.Value
            if err := json.Unmarshal(value, ss); err != nil {
                fmt.Println("now", time.Now(), "watchConfig err", err)
                continue
            }
            fmt.Println("now", time.Now(), "watchConfig", ss)
        }
    }()
}
watchConfig(client, "config_key", &appConfig)

代码:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/coreos/etcd/clientv3"
	"github.com/coreos/etcd/mvcc/mvccpb"
)

func main() {
	config := clientv3.Config{
		Endpoints:   []string{"192.168.0.113:2379"}, // 集群列表
		DialTimeout: 5 * time.Second,
	}

	// 建立一个客户端
	client, err := clientv3.New(config)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 获得kv API子集
	kv := clientv3.NewKV(client)

	// 模拟etcd中KV的变化
	go func() {
		for {
			kv.Put(context.TODO(), "/demo/A/B1", "i am B1")

			kv.Delete(context.TODO(), "/demo/A/B1")

			time.Sleep(1 * time.Second)
		}
	}()

	// 先GET到当前的值,并监听后续变化
	getResp, err := kv.Get(context.TODO(), "/demo/A/B1")
	if err != nil {
		fmt.Println(err)
		return
	}

	// 现在key是存在的
	if len(getResp.Kvs) != 0 {
		fmt.Println("当前值:", string(getResp.Kvs[0].Value))
	}

	// 当前etcd集群事务ID, 单调递增的
	watchStartRevision := getResp.Header.Revision + 1

	// 创建一个watcher
	watcher := clientv3.NewWatcher(client)

	// 启动监听
	fmt.Println("从该版本向后监听:", watchStartRevision)

	// 创建一个 5s 后取消的上下文
	ctx, cancelFunc := context.WithCancel(context.TODO())
	time.AfterFunc(5*time.Second, func() {
		cancelFunc()
	})

	// 该监听动作在 5s 后取消
	watchRespChan := watcher.Watch(ctx, "/demo/A/B1", clientv3.WithRev(watchStartRevision))

	// 处理kv变化事件
	for watchResp := range watchRespChan {
		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)
			}
		}
	}

}

结果:

从该版本向后监听: 94
修改为: i am B1 Revision: 94 94
删除了 Revision: 95
修改为: i am B1 Revision: 96 96
删除了 Revision: 97
修改为: i am B1 Revision: 98 98
删除了 Revision: 99
修改为: i am B1 Revision: 100 100
删除了 Revision: 101
修改为: i am B1 Revision: 102 102
删除了 Revision: 103

参考资料:
https://blog.youkuaiyun.com/wohu1104/article/details/108552649

【etcd】go使用etcd踩坑记录 go使用etcd,依赖包问题采坑记录 阅读详情

相关推荐

Go 学习笔记(58)— Go 第三方库之 etcd/clientv3(连接客户端、PUT、GET、Lease、Op、Txn、Watch 基础概念说明)

1. 安装 Golang 的 Etcd 包 我们使用 v3 版本的 etcd client , 首先通过 go get 下载并编译安装 etcd clinet v3。 go get -v github.com/coreos/etcd/clientv3 该命令会将包下载到 $GOPATH/src/github.com/coreos/etcd/clientv3 中,所有相关依赖包会自动下载编译,包括protobuf、grpc等。 我们主要梳理一下使用 etcd 时经常用到的主要 API 并进行演示。 2. 连

wohu1104的专栏 1万+

【etcd介绍及其用go语言使用etcd】

对etcd进行了概括,对其工作原理进行了梳理,常用的操作进行了整理,最后整理了go操作etcd的方法

m0_63296929的博客 979

使用Golang安装etcd的过程

本文介绍了在Golang中安装etcd的步骤,并提供了一个简单的etcd示例代码。首先,我们需要安装Golang。可以从Golang官方网站(https://golang.org/dl/)下载适合你操作系统的安装包,并按照官方指南进行安装。本文将详细介绍如何使用Golang安装etcd,并提供相应的源代码示例。在安装完Golang之后,我们需要创建一个新的项目目录,用于存放etcd相关的源代码和依赖项。在上述示例代码中,我们首先创建了一个etcd客户端,并指定了etcd服务的地址和连接超时时间。

HmoaAmp的博客 355

Go语言etcd实现服务注册发现

Go语言实现服务注册发现

m0_62729235的博客 1690

LAN9252 EEPROM配置全解析:从XML到上电加载的实战指南

EtherCAT从站开发中,EEPROM配置是核心环节,涉及XML描述、二进制生成和SPI/I2C烧录。LAN9252作为经典从站控制器,其配置机制基于ETG规范,通过ConfigData定义SyncManager、FMMU和PDO映射,确保过程数据正确通信。理解这一原理能提升从站开发效率,避免通信异常。应用场景包括伺服驱动、远程IO等。本文深入拆解LAN9252的EEPROM配置流程,涵盖XML结构、SPI模式选择、AN1907迁移及烧录实操,帮助开发者快速上手。

weixin_29172201的博客 333

Windows下用VS2022编译libcurl全流程(含openssl配置避坑指南)

本文详细介绍了在Windows平台使用Visual Studio 2022编译libcurl库的完整流程,重点解决了集成OpenSSL支持HTTPS功能时的常见配置难题。从环境准备、源码获取、依赖编译到工程配置,提供了清晰的步骤和针对非默认安装路径的避坑指南,帮助开发者高效完成网络库的集成工作。

weixin_29291185的博客 246

ETCD golang ClientV3的基本使用

etcd安装及简单使用

weixin_42681513的博客 1409

Ubuntu开发环境配置——持续更新

本文简要介绍了Ubuntu下的开发环境配置流程和问题分析与解决,更多内容持续更新中。

m0_53365889的博客 2225

golang使用etcd

etcd etcd 是一个分布式开源的kv数据库,它可以监听某个键的变化,可作为分布式锁 1. 下载安装etcd 官网: https://github.com/etcd-io/etcd/releases 选择对应的系统下载,解压即可,下面以 Linux系统做演示 wget https://github.com/etcd-io/etcd/releases/download/v3.3.18/etc...

跟派大星学编程 1279

Debian 12 安装nodejs(包括node、npm、npx、yarn、pnpm、cnpm、bun)

【代码】Debian 12 安装nodejs。

mirana的专栏 8032

山石网科Hillstone防火墙基础上网配置_CLI命令行(最新版)

山石网科Hillstone防火墙安全网关基础上网基础网络配置_CLI命令行(最新版)

路与肥的博客 1万+

Golang使用etcd构建分布式锁案例

在本教程中,我们使用Go等语言构建了简单分布式锁系统。该系统可用于远程Go开发人员管理对其应用程序中共享资源的访问,确保分布式系统中的一致性和防止竞争条件。

neweastsun的专栏 2038

mipi协议中的calibration和scramble模式

在MIPI(Mobile Industry Processor Interface)协议中,calibration(校准)和scramble(加扰)模式是两个重要的特性,它们分别用于优化数据传输的准确性和减少信号干扰。

flomingo1的专栏 2402

go-etcd 使用指南

go-etcd 使用指南 1. 项目介绍 go-etcd 是一个用 Go 语言编写的客户端库,用于访问 etcd 分布式键值存储系统。etcd 自身是基于 Raft 共识算法构建的高度可靠的关键值存储系统,广泛应用于确保分布式系统中配置和服务发现的一致性。它被许多公司采用在生产环境中,特别是在与 Kubernetes、fleet、locksmith、vulcand 等工具集成时,以支持关键的部署场...

gitblog_00829的博客 626

宏基因组分析|完整分析流程梳理

今天先从最基础的全流程框架说起,无论是刚接触的萌新,还是想查漏补缺的同行,都能找到有用的信息。

weixin_70911870的博客 3166

华为OD机考双机位C卷 - 区间交集(Java & Python& JS & C/C++ & GO )

华为OD机考双机位C卷 - 区间交集

算法大师! 1137

如何在 Go 语言中使用 etcd

etcd 是一个开源的分布式键值存储系统,它提供了可靠的数据存储和高效的读取功能,常用于分布式系统中的配置管理、服务发现等场景。以下是在 Go 语言中使用 etcd 的指南。

m0_57836225的博客 832

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

golang 使用etcd

SEO etcd编译时,报undefined: strings.Builder错误,如何解决? etcd镜像版本过低,如何拿到高版本的etcd镜像? etcd为什么本机能连,容器连不上? etcd如何进行服务注册,服务发现? 重点 解决etcd本机能连,容器连不上的问题。 解决etcd容器版本过低(2.0 – > 3.3) 解决通过etcd进行服务发现的工具操作 简介 etcd 是一个非...

fwhezfwhez的博客 3533

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

etcd 使用: golang 例子

一:连接到 etcd package main import ( "fmt" "go.etcd.io/etcd/clientv3" "time" ) //连接 func main() { //客户端配置 config := clientv3.Config{ Endpoints: []string{"192.168.1.109:2379...

weixin_30642561的博客 687

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Go使用Etcd

Go使用Etcd clientv3.New(clientv3.Config{})创建etcd客户端,在使用etcd客户端中具体操作与配置与etcdctl中的指令,参数类似,所以非常容易理解每个配置的含义。 cli, err := clientv3.New(clientv3.Config{}) 下面将讲解一下clientv3.Config类 type Config struct { // 配置etcd ip Endpoints []string `json:"endpoints"` //

a1023934860的博客 1958

【etcd】go etcd实战一:etcd基本使用

go etcd实战一,基本使用

SauryN的博客 1975

Etcd使用go module的灾难

最近几个月,或者最近一年,使用etcd做开发的朋友,如果你开启了go module的功能的话,没有出现翻车的现象吗?或者go get -u .更新一下项目依赖试试看。因为我使用visua...

Go中国 2444

go-etcd 安装与使用指南

go-etcd 安装与使用指南 1. 项目目录结构及介绍 go-etcd 是一个用于与 etcd 交互的 Go 语言客户端库。由于您提供的信息是关于 etcd 的一般指导而非特定于 go-etcd 直接的文档,我们将基于 Go 项目的一般结构和 etcd 使用场景来推断其可能的目录布局和功能: 通常,一个Go项目结构如下: cmd: 包含主要的可执行命令入口,如 main.go,这里可能是启动客...

gitblog_00844的博客 990

使用golang连接并简单使用ETCD

golang;ETCD;

weixin_40725055的博客 1049

go-zero(七) RPC服务和ETCD使用

在微服务中,我们是通过RPC来传递数据的,下面将通过一个简单的示例,说明如何使用go-zero框架和 Protocol Buffers 定义 RPC 服务。

Clown95 2645

golang使用etcd实现分布式锁

1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "go.etcd.io/etcd/clientv3" 9 ) 10 11 func main() { 12 var ( 13 client *clientv3.Clien...

weixin_33937499的博客 848
上一篇: Go自学遇到的问题记录(一)
下一篇: 利用Numpy+PIL读取图像实现手绘效果
Andy in boots
博客等级 码龄7年 73粉丝 69原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值