GO语言开发之etcd入门

etcd简介

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

etcd具有以下特点:

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

etcd使用场景

1.服务发现

所有服务将元信息存储到以某个 prefix 开头的 key 中,然后消费者从这些 key 中获取服务信息并调用。消费者也可以 watch 这些 key 的变更,以便在服务增加和减少时及时获得通知。
在这里插入图片描述

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值