etcd的简单应用

etcd是一个高可用的分布式键值存储系统,用于配置共享和服务发现。它提供强一致性,支持leader选举和机器故障容忍。文章介绍了etcd的安装、使用(包括Go客户端的put和get操作)、watch监控以及服务发现的实现。此外,还讨论了CAP原理和etcd的相关概念,如MVCC、WAL和Raft共识算法。
摘要由CSDN通过智能技术生成

etcd

etcd is a strongly consistent, distributed key-value store that provides a reliable way to store data that needs to be accessed by a distributed system or cluster of machines. It gracefully handles leader elections during network partitions and can tolerate machine failure, even in the leader node.
etcd 是一个用于配置共享和服务发现的键值存储系统。

官网:https://etcd.io/
github:https://github.com/etcd-io/etcd/

安装

下载,解压,运行即可
这里我已经将etcd的解压路径加入了全局PATH
在这里插入图片描述

测试

etcdctl --endpoints=localhost:2379 put foo bar
etcdctl --endpoints=localhost:2379 get foo
ophelia@Ophelia:~$ etcdctl --endpoints=localhost:2379 put foo bar
OK
ophelia@Ophelia:~$ etcdctl --endpoints=localhost:2379 get foo
foo
bar

Go客户端

https://github.com/etcd-io/etcd/tree/main/client/v3

put & get

package main

import (
	"context"
	"fmt"
	clientv3 "go.etcd.io/etcd/client/v3"
	"time"
)

func main() {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"localhost:2379"},
		DialTimeout: 5 * time.Second, //超时时间
	})
	if err != nil {
		panic(err)
	}
	defer cli.Close()

	// put
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	resp, err := cli.Put(ctx, "sample_key", "sample_value")
	defer cancel()
	if err != nil {
		panic(err)
	}
	// use the response
	fmt.Println(resp)

	// get
	resp1, err := cli.Get(ctx, "sample_key")
	if err != nil {
		panic(err)
	}

	for k, v := range resp1.Kvs {
		fmt.Println(k, v)
	}
}

watch 监控某个值

package main

import (
	"context"
	"fmt"
	clientv3 "go.etcd.io/etcd/client/v3"
	"time"
)

func main() {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"localhost:2379"},
		DialTimeout: 5 * time.Second, //超时时间
	})
	if err != nil {
		panic(err)
	}
	defer cli.Close()


	watcher := clientv3.NewWatcher(cli)
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()
	
	//wch :=cli.Watch(ctx,"zhagnsna")
	wch := watcher.Watch(ctx, "zhangsan")

	for w := range wch {
		for idx, val := range w.Events {
			fmt.Println(idx, val)
		}
	}
}

运行代码,然后在客户端修改zhangsan的值
etcdctl --endpoints=localhost:2379 put zhangsan ok
代码端输出:
0 &{PUT key:“zhangsan” create_revision:15 mod_revision:19 version:5 value:“ok” {} [] 0}

CAP原理

Consistency(一致性)
Availability(可用性)
Partition tolerance(分区容错性)

以上三点最多只能同时实现两者。

etcd中的一些概念

在这里插入图片描述

应用场景

1.键值对存储
2.服务注册与发现
3.消息发布与订阅
4.分布式锁

架构图

在这里插入图片描述
etcd Server:对外接受和处理客户端的请求
gRPC Server:etcd 与其他 etcd 节点之间通信和信息同步
MVCC:多版本并发控制,etcd 的存储模块,键值对的每一次操作行为都会被记录存储,这些数据底层存储在 BoltDB 数据库中
WAL:预写式日志,etcd中的数据提交前都会记录到日志
Snapshot:快照,防止WAL日志过多,用于存储某一时刻etcd的所有数据
以上二者相结合,etcd可以有效地进行数据存储和节点故障恢复等操作
Raft:共识算法模块,保证数据的一致性

服务发现

在这里插入图片描述
service.go

package main

import (
	"context"
	clientv3 "go.etcd.io/etcd/client/v3"
	"log"
	"time"
)

// ServiceReg 创建租约注册服务
type ServiceReg struct {
	client        *clientv3.Client // etcd 客户端
	lease         clientv3.Lease   // 租约
	leaseResp     *clientv3.LeaseGrantResponse
	cancelFunc    func()
	keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
	key           string
}

func NewServiceReg(addr []string, timeNum int64) (*ServiceReg, error) {
	conf := clientv3.Config{
		Endpoints:   addr,
		DialTimeout: 5 * time.Second,
	}
	var client = new(clientv3.Client)
	if clientTem, err := clientv3.New(conf); err == nil {
		client = clientTem
	} else {
		return nil, err
	}
	ser := &ServiceReg{
		client:        client,
		lease:         nil,
		leaseResp:     nil,
		cancelFunc:    nil,
		keepAliveChan: nil,
		key:           "",
	}
	if err := ser.setLease(timeNum); err != nil {
		return nil, err
	}
	go ser.ListenLeaseRespChan()
	return ser, nil
}

// 设置租约
func (s *ServiceReg) setLease(timeNum int64) error {
	lease := clientv3.NewLease(s.client)
	// 设置租约时间
	leaseResp, err := lease.Grant(context.TODO(), timeNum)
	if err != nil {
		return err
	}
	// 设置续租
	ctx, cancelFunc := context.WithCancel(context.TODO())
	leaseRespChan, err := lease.KeepAlive(ctx, leaseResp.ID)
	if err != nil {
		return err
	}
	s.lease = lease
	s.leaseResp = leaseResp
	s.cancelFunc = cancelFunc
	s.keepAliveChan = leaseRespChan
	return nil
}

// ListenLeaseRespChan 监听续租情况
func (s *ServiceReg) ListenLeaseRespChan() {
	for {
		select {
		case leaseKeepRest := <-s.keepAliveChan:
			if leaseKeepRest == nil {
				log.Println("已经关闭续租功能")
				return
			} else {
				log.Println("续租成功")
			}

		}
	}
}

// PutService 通过租约注册服务
func (s *ServiceReg) PutService(key, val string) error {
	kv := clientv3.NewKV(s.client)
	log.Printf("register user server for %s\n", val)
	_, err := kv.Put(context.TODO(), key, val, clientv3.WithLease(s.leaseResp.ID))
	return err
}

// RevokeLease 撤销租约
func (s *ServiceReg) RevokeLease() error {
	s.cancelFunc()
	time.Sleep(time.Second * 2)
	_, err := s.lease.Revoke(context.TODO(), s.leaseResp.ID)
	return err
}

func main() {
	ser, _ := NewServiceReg([]string{"localhost:2379"}, 5) // 服务心跳TTL 5
	defer ser.RevokeLease()
	if err := ser.PutService("/user", "http://localhost:8000"); err != nil {
		log.Panic(err)
	}
	// 阻塞,持续运行
	select {}
}

client.go

package main

import (
	"context"
	"fmt"
	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
	"log"
	"sync"
	"time"
)

// ClientInfo 客户端连接的结构体
type ClientInfo struct {
	client     *clientv3.Client
	serverList map[string]string
	lock       sync.Mutex
}

// NewClientInfo 初始化 etcd 客户端连接
func NewClientInfo(addr []string) (*ClientInfo, error) {
	conf := clientv3.Config{
		Endpoints:   addr,
		DialTimeout: 5 * time.Second,
	}
	if client, err := clientv3.New(conf); err == nil {
		return &ClientInfo{
			client:     client,
			serverList: make(map[string]string),
		}, nil
	} else {
		return nil, err
	}
}

// GetService 获取服务实例信息
func (s *ClientInfo) GetService(prefix string) ([]string, error) {
	if addrs, err := s.getServiceByName(prefix); err != nil {
		panic(err)
	} else {
		log.Println("get service", prefix, "for instance list:", addrs)
		go s.watcher(prefix)
		return addrs, nil
	}
}

// 监控指定键值对的变更
func (s *ClientInfo) watcher(prefix string) {
	rch := s.client.Watch(context.Background(), prefix, clientv3.WithPrefix())
	for wresp := range rch {
		for _, ev := range wresp.Events {
			switch ev.Type {
			case mvccpb.PUT: // 写入的事件
				s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
			case mvccpb.DELETE: // 删除的事件
				s.DelServiceList(string(ev.Kv.Key))
			}
		}
	}
}

// 根据服务名,获取服务实例信息
func (s *ClientInfo) getServiceByName(prefix string) ([]string, error) {
	resp, err := s.client.Get(context.Background(), prefix, clientv3.WithPrefix())
	if err != nil {
		return nil, err
	}
	addrs := s.extractAddrs(resp)
	return addrs, nil
}

// 根据etcd的响应,提取服务实例的数组
func (s *ClientInfo) extractAddrs(resp *clientv3.GetResponse) []string {
	addrs := make([]string, 0)
	if resp == nil || resp.Kvs == nil {
		return addrs
	}
	for i := range resp.Kvs {
		if v := resp.Kvs[i].Value; v != nil {
			s.SetServiceList(string(resp.Kvs[i].Key), string(resp.Kvs[i].Value))
			addrs = append(addrs, string(v))
		}
	}
	return addrs
}

func (s *ClientInfo) SetServiceList(key, val string) {
	s.lock.Lock()
	defer s.lock.Unlock()
	s.serverList[key] = val
	log.Println("set data key:", key, "val:", val)
}

func (s *ClientInfo) DelServiceList(key string) {
	s.lock.Lock()
	defer s.lock.Unlock()
	delete(s.serverList, key)
	log.Println("del data key:", key)
}

// SerList2Array 工具方法,转换数组
func (s *ClientInfo) SerList2Array() []string {
	s.lock.Lock()
	defer s.lock.Unlock()
	addrs := make([]string, 0)
	for _, v := range s.serverList {
		addrs = append(addrs, v)
	}
	return addrs
}

func main() {
	cli, _ := NewClientInfo([]string{"localhost:2379"})
	fmt.Println(cli.GetService("/user"))
	select {}
}

分布式锁

在这里插入图片描述

package main

import (
	"context"
	"fmt"
	clientv3 "go.etcd.io/etcd/client/v3"
	"time"
)

/*
基于缓存实现分布式锁

流程:
开始-->客户端配置-->建立连接-->创建租约-->
自动续租-->续约应答-->创建事务-->
在租约时间内去抢占锁-->
获取到锁-->执行业务逻辑-->释放锁-->结束
没有获取到锁--> 直接结束
*/
var (
	client         *clientv3.Client
	lease          clientv3.Lease
	leaseId        clientv3.LeaseID
	leaseGrantResp *clientv3.LeaseGrantResponse
	keepRespChan   <-chan *clientv3.LeaseKeepAliveResponse = make(<-chan *clientv3.LeaseKeepAliveResponse)
	txnResp        *clientv3.TxnResponse
)

func main() {

	// 客户端配置
	config := clientv3.Config{
		Endpoints:   []string{"localhost:2379"},
		DialTimeout: 5 * time.Second,
	}

	var err error
	// 建立连接
	if client, err = clientv3.New(config); err != nil {
		panic(err)
	}
	// 上锁并创建租约
	lease = clientv3.NewLease(client)
	if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil {
		panic(err)
	}
	leaseId = leaseGrantResp.ID
	// 创建一个可取消的租约,主要是为了退出的时候能够释放
	ctx, cancelFunc := context.WithCancel(context.TODO())
	//释放租约
	defer cancelFunc()
	defer lease.Revoke(context.TODO(), leaseId)
	if keepRespChan, err = lease.KeepAlive(ctx, leaseId); err != nil {
		panic(err)
	}

	// 续约应答
	go func() {
		for {
			select {
			case keepResp := <-keepRespChan:
				if keepResp == nil {
					fmt.Println("租约已经失效了")
					return
				} else { //每秒会续约一次,所以就会收到一次应答
					fmt.Println("收到自动续约应答:", keepResp.ID)
				}
			}
		}
	}()

	// 在租约时间内去抢占锁,(etcd里面的锁就是一个key)
	kv := clientv3.NewKV(client)
	// 创建事务
	txn := kv.Txn(context.TODO())
	// if 不存在key,Then设置它,Else抢占锁失败
	txn.If(clientv3.Compare(clientv3.CreateRevision("lock"), "=", 0)).
		Then(clientv3.OpPut("lock", "g", clientv3.WithLease(leaseId))).
		Else(clientv3.OpGet("lock"))
	// 提交事务
	if txnResp, err = txn.Commit(); err != nil {
		panic(err)
	}
	if !txnResp.Succeeded {
		fmt.Println("锁被占用:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))
		return
	}

	// 抢占到锁后执行业务逻辑,没有抢占到则推虎
	fmt.Println("处理任务")
	time.Sleep(time.Second * 5)

}

Reference
拉钩教育《etcd原理与实践》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

metabit

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值