GO语言开发之etcd入门

etcd简介

etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。

etcd具有以下特点:

  • 完全复制:集群中的每个节点都可以使用完整的存档
  • 高可用性:Etcd可用于避免硬件的单点故障或网络问题
  • 一致性:每次读取都会返回跨多主机的最新写入
  • 简单:包括一个定义良好、面向用户的API(gRPC)
  • 安全:实现了带有可选的客户端证书身份验证的自动化TLS
  • 快速:每秒10000次写入的基准速度
  • 可靠:使用Raft算法实现了强一致、高可用的服务存储目录

etcd使用场景

1.服务发现

所有服务将元信息存储到以某个 prefix 开头的 key 中,然后消费者从这些 key 中获取服务信息并调用。消费者也可以 watch 这些 key 的变更,以便在服务增加和减少时及时获得通知。
GO语言开发之etcd入门_第1张图片

2.配置共享

应用将配置信息存放到 etcd,当配置信息被更改时可以通过 watch 机制从 etcd 及时获得通知。

3.分布式锁

因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。由于 etcd 中的数据是一致的,当多个应用同时去创建一个 key 时只有一个会成功,创建成功的应用即获取了锁。etcd 还有更多的应用场景,例如集群监控,Leader 竞选等。需要注意的是,应该使用 etcd 来存储一个关键的控制数据,对于应用数据应该只在数据量较小时存储。

etcd安装

安装包下载地址:https://github.com/etcd-io/etcd/releases,选择对应系统下载
下载完成后解压,解压文件中有etcd,etcdurl两个二进制文件,分别是etcd服务端和客户端,执行./etcd启动服务,etcd目前默认使用2379端口提供HTTP API服务

本文主要介绍如何使用Go语言操作etcd,命令行使用不做介绍

Golang操作etcd

导入包:

 go get go.etcd.io/etcd/clientv3
1. connect 连接etcd
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")
}

2. put,get,delete操作

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

2. watch操作

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

3. lease 租约

Lease 提供了以下功能:

  • Grant :分配一个租约
  • Revoke :释放一个租约
  • TimeToLive :获取剩余TTL时间
  • Leases :列举所有etcd中的租约
  • KeepAlive :自动定时的续约某个租约
  • KeepAliveOnce :为某个租约续约一次
  • Close :释放当前客户端建立的所有租约
租约实现自动过期:
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: 这是一个租约
租约过期了
使用keepAlive定时续租
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: 这是一个租约

可以看到租约不会过期了

4. op操作

op操作是实现分布式锁的基础,Op 是一个抽象的操作,可以是 Put/Get/Delete… ;而 OpResponse 是一个抽象的结果,可以是 PutResponse/GetResponse…,根据不同的操作返回不同结果。如 OpPut()返回PutResponse

常用方法:

  • 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
  • func Do(ctx, Op) OpResponse
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操作
5. Txn 事务

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

你可能感兴趣的:(GO,go语言,etcd)