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


【etcd】go etcd实战一:etcd基本使用
【etcd】go etcd实战二:分布式锁


一、etcd基本介绍

    ETCD是用于共享配置和服务发现的分布式,一致性的KV存储系统。具体信息请参考[项目首页][Github]。ETCD是CoreOS公司发起的一个开源项目,授权协议为Apache。

二、基本使用

1.创建client

package main

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

func main() {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"10.1.30.79:12379"},
		DialTimeout: time.Second * 10,
	})
	if err != nil {
		panic(err)
	}
	defer cli.Close()
}

    这里可以看下etcd的源码,主要是Config,注释很详细,不赘述了。

/*摘自部分etcd源码*/


func New(cfg Config) (*Client, error) {
	if len(cfg.Endpoints) == 0 {
		return nil, ErrNoAvailableEndpoints
	}

	return newClient(&cfg)
}

type Config struct {
	// 地址列表
	Endpoints []string `json:"endpoints"`

	// 更新端点及其最新成员的间隔,0为禁止自动同步,默认禁止
	AutoSyncInterval time.Duration `json:"auto-sync-interval"`

	// 建立连接超时时间
	DialTimeout time.Duration `json:"dial-timeout"`

	// 存活探针时间
	DialKeepAliveTime time.Duration `json:"dial-keep-alive-time"`

	// 存活探针等待时间,超时关闭会连接
	DialKeepAliveTimeout time.Duration `json:"dial-keep-alive-timeout"`

	// 客户端发送字节数限制,默认2.0 MiB (2 * 1024 * 1024).
	MaxCallSendMsgSize int
	
	// 客户端接收字节数限制,默认math.MaxInt32.
	MaxCallRecvMsgSize int

	// TLS 配置
	TLS *tls.Config
	
	// 鉴权用户名 密码
	Username string `json:"username"`
	Password string `json:"password"`

	// 是否拒绝针对过时的集群创建客户端
	RejectOldCluster bool `json:"reject-old-cluster"`

	// grpc客户端连接选项,比如传入 grpc.WithBlock() 阻塞等待底层连接建立,否则立即返回,异步进行建立连接
	DialOptions []grpc.DialOption

	// 客户端默认上下文,可用于取消grpc连接和其他没有特定上下文的操作。
	Context context.Context

	// 客户端日志
	Logger *zap.Logger

	// 客户端日志配置
	LogConfig *zap.Config
	
	// 设置后将允许客户端在没有任何RPC活动流的情况下向服务器发送 keepalive ping
	PermitWithoutStream bool `json:"permit-without-stream"`
}

// Client provides and manages an etcd v3 client session.
type Client struct {
	Cluster
	KV
	Lease
	Watcher
	Auth
	Maintenance

	conn *grpc.ClientConn

	cfg      Config
	creds    grpccredentials.TransportCredentials
	resolver *resolver.EtcdManualResolver
	mu       *sync.RWMutex

	ctx    context.Context
	cancel context.CancelFunc

	// Username is a user name for authentication.
	Username string
	// Password is a password for authentication.
	Password        string
	authTokenBundle credentials.Bundle

	callOpts []grpc.CallOption

	lgMu *sync.RWMutex
	lg   *zap.Logger
}

2.设置和获取KV


func testKv(cli *clientv3.Client) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
	_, err := cli.Put(ctx, "/key", "value")
	if err != nil {
		panic(err)
	}
	cancel()

	ctx, cancel = context.WithTimeout(context.Background(), time.Second*3)
	defer cancel()

	rsp, err := cli.Get(ctx, "/key")
	if err != nil {
		panic(err)
	}
	fmt.Println("get:", string(rsp.Kvs[0].Value))
}

    运行的结果
在这里插入图片描述

    通过etcd后台也可以看到刚添加的kv。
在这里插入图片描述
    分析下源码,可以看到Client包含KV匿名接口

type KV interface {
	// 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)

	// 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)

	// 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 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)

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

    PutGet最后一个参数是可选参数,用于操作配置,常用的有clientv3.WithPrefix(),可以获取指定前缀对应key的所有内容。

3.监听指定key

func testWatch(cli *clientv3.Client) {
	wChan := cli.Watch(context.Background(), "/key")
	cli.Put(context.Background(), "/key", "watchValue")
	rsp := <-wChan
	for _, event := range rsp.Events {
		fmt.Println("event type:", event.Type, " key:", string(event.Kv.Key), " value:", string(event.Kv.Value))
	}
}

    源码中Client包含Watcher匿名接口

type Watcher interface {
	// Watch watches on a key or prefix. The watched events will be returned
	// through the returned channel. If revisions waiting to be sent over the
	// watch are compacted, then the watch will be canceled by the server, the
	// client will post a compacted error watch response, and the channel will close.
	// If the requested revision is 0 or unspecified, the returned channel will
	// return watch events that happen after the server receives the watch request.
	// If the context "ctx" is canceled or timed out, returned "WatchChan" is closed,
	// and "WatchResponse" from this closed channel has zero events and nil "Err()".
	// The context "ctx" MUST be canceled, as soon as watcher is no longer being used,
	// to release the associated resources.
	//
	// If the context is "context.Background/TODO", returned "WatchChan" will
	// not be closed and block until event is triggered, except when server
	// returns a non-recoverable error (e.g. ErrCompacted).
	// For example, when context passed with "WithRequireLeader" and the
	// connected server has no leader (e.g. due to network partition),
	// error "etcdserver: no leader" (ErrNoLeader) will be returned,
	// and then "WatchChan" is closed with non-nil "Err()".
	// In order to prevent a watch stream being stuck in a partitioned node,
	// make sure to wrap context with "WithRequireLeader".
	//
	// Otherwise, as long as the context has not been canceled or timed out,
	// watch will retry on other recoverable errors forever until reconnected.
	//
	// TODO: explicitly set context error in the last "WatchResponse" message and close channel?
	// Currently, client contexts are overwritten with "valCtx" that never closes.
	// TODO(v3.4): configure watch retry policy, limit maximum retry number
	// (see https://github.com/etcd-io/etcd/issues/8980)
	Watch(ctx context.Context, key string, opts ...OpOption) WatchChan

	// RequestProgress requests a progress notify response be sent in all watch channels.
	RequestProgress(ctx context.Context) error

	// Close closes the watcher and cancels all watch requests.
	Close() error
}

    Watch 接口返回一个类型为 WatchResponsechan

type WatchResponse struct {
	Header pb.ResponseHeader
	Events []*Event

	// CompactRevision is the minimum revision the watcher may receive.
	CompactRevision int64

	// Canceled is used to indicate watch failure.
	// If the watch failed and the stream was about to close, before the channel is closed,
	// the channel sends a final response that has Canceled set to true with a non-nil Err().
	Canceled bool

	// Created is used to indicate the creation of the watcher.
	Created bool

	closeErr error

	// cancelReason is a reason of canceling watch
	cancelReason string
}

    这里主要是看Events,事件类型定义如下

type Event struct {
	// type is the kind of event. If type is a PUT, it indicates
	// new data has been stored to the key. If type is a DELETE,
	// it indicates the key was deleted.
	Type Event_EventType `protobuf:"varint,1,opt,name=type,proto3,enum=mvccpb.Event_EventType" json:"type,omitempty"`
	// kv holds the KeyValue for the event.
	// A PUT event contains current kv pair.
	// A PUT event with kv.Version=1 indicates the creation of a key.
	// A DELETE/EXPIRE event contains the deleted key with
	// its modification revision set to the revision of deletion.
	Kv *KeyValue `protobuf:"bytes,2,opt,name=kv,proto3" json:"kv,omitempty"`
	// prev_kv holds the key-value pair before the event happens.
	PrevKv               *KeyValue `protobuf:"bytes,3,opt,name=prev_kv,json=prevKv,proto3" json:"prev_kv,omitempty"`
	XXX_NoUnkeyedLiteral struct{}  `json:"-"`
	XXX_unrecognized     []byte    `json:"-"`
	XXX_sizecache        int32     `json:"-"`
}

type Event_EventType int32

const (
	PUT    Event_EventType = 0
	DELETE Event_EventType = 1
)

    可以看到,Event Type事件有两种类型,PUTDELETE,分别对应相应事件。当 TypePUT 时,Kv中存放当前最新的 Kv 键值对,PrevKv看注释是修改之前的,但是我在测试时始终为nil;当 TypeDELETE 时,Kv中存放已被删除的Keyvaluenil

4.租约

    这里的意思就是给一对键值可以设置过期时间,如果需要键值对一直存在就要在租约时间内不停的刷新键值对时间,也就是续租。

func testLease(cli *clientv3.Client) {
	grant, err := cli.Grant(context.Background(), 3)
	if err != nil {
		fmt.Println(err)
		return
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	
	ch, err := cli.KeepAlive(ctx, grant.ID)
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = cli.Put(context.Background(), "/key", "lease", clientv3.WithLease(grant.ID))
	if err != nil {
		fmt.Println(err)
		return
	}

	start := time.Now()
	for i := 0; i < 3; i++ {
		rsp := <-ch
		fmt.Println(time.Now().Sub(start), rsp)
		start = time.Now()
		getRsp, err := cli.Get(context.Background(), "/key")
		if err != nil {
			fmt.Println(err)
		}
		for _, kv := range getRsp.Kvs {
			fmt.Println(string(kv.Key), string(kv.Value))
		}
	}
}

    运行结果:
在这里插入图片描述
    Client中包含的Lease接口

type Lease interface {
	// Grant creates a new lease.
	Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)

	// Revoke revokes the given lease.
	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 attempts to keep the given lease alive forever. If the keepalive responses posted
	// to the channel are not consumed promptly the channel may become full. When full, the lease
	// client will continue sending keep alive requests to the etcd server, but will drop responses
	// until there is capacity on the channel to send more responses.
	//
	// If client keep alive loop halts with an unexpected error (e.g. "etcdserver: no leader") or
	// canceled by the caller (e.g. context.Canceled), KeepAlive returns a ErrKeepAliveHalted error
	// containing the error reason.
	//
	// The returned "LeaseKeepAliveResponse" channel closes if underlying keep
	// alive stream is interrupted in some way the client cannot handle itself;
	// given context "ctx" is canceled or timed out.
	//
	// TODO(v4.0): post errors to last keep alive message before closing
	// (see https://github.com/etcd-io/etcd/pull/7866)
	KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)

	// KeepAliveOnce renews the lease once. The response corresponds to the
	// first message from calling KeepAlive. If the response has a recoverable
	// error, KeepAliveOnce will retry the RPC with a new keep alive message.
	//
	// 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
}

    这里主要用到 GrantKeepAlive 两个接口,Grant 用于生成一个新的租约,返回 *LeaseGrantResponse 中包含一个 ID 即租约ID,然后使用 Put 加上 clientv3.WithLease(grant.ID)选项,给键值对一个租约,拥有租约的过期时间。然后使用 KeepAlive 开启续租,KeepAlive 会返回一个 chan,类型为 *LeaseKeepAliveResponse,每次续租后会通过这个 chan 发送消息,业务中需要从这个 chan 中取消息。

5.事务

    etcd提供了事务的机制,可以在一个事务中实现多个原子性操作。

func testTxn(cli *clientv3.Client) {
	_, err := cli.Put(context.Background(), "/key", "txn")
	if err != nil {
		fmt.Println(err)
		return
	}
	// 判断 /key 对应的 value 如果等于 txn 则设置 /result 为 success,否则设置 /result 为 fail
	commit, err := cli.Txn(context.Background()).If(clientv3.Compare(clientv3.Value("/key"), "=", "txn")).
		Then(clientv3.OpPut("/result", "success")).
		Else(clientv3.OpPut("/result", "fail")).Commit()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(commit)

	rsp, err := cli.Get(context.Background(), "/result")
	if err != nil {
		fmt.Println(err)
		return
	}
	if len(rsp.Kvs) > 0 {
		fmt.Println("result", string(rsp.Kvs[0].Value))
	}
}

    运行结果:
在这里插入图片描述
    看下源码

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)
}

    If接口入参为Cmp,一般使用clientv3.Compare作为入参

// 部分代码
func Compare(cmp Cmp, result string, v interface{}) Cmp {
	var r pb.Compare_CompareResult

	switch result {
	case "=":
		r = pb.Compare_EQUAL
	case "!=":
		r = pb.Compare_NOT_EQUAL
	case ">":
		r = pb.Compare_GREATER
	case "<":
		r = pb.Compare_LESS
	default:
		panic("Unknown result op")
	}
	//...
}

    这边可以看到Compare的第一个入参也是Cmp类型用来控制对比指定key的哪部分信息,etcd提供了如下几种常用的;第二个入参是string类型 控制对比方式,从源码中看到,支持= != > <四种;第三个入参是需要对比的值。

// 指定key对应value
func Value(key string) Cmp {
	return Cmp{Key: []byte(key), Target: pb.Compare_VALUE}
}
// 指定key 版本
func Version(key string) Cmp {
	return Cmp{Key: []byte(key), Target: pb.Compare_VERSION}
}
// 指定key创建版本
func CreateRevision(key string) Cmp {
	return Cmp{Key: []byte(key), Target: pb.Compare_CREATE}
}
// 指定key修改版本
func ModRevision(key string) Cmp {
	return Cmp{Key: []byte(key), Target: pb.Compare_MOD}
}
// 指定key 租约ID
func LeaseValue(key string) Cmp {
	return Cmp{Key: []byte(key), Target: pb.Compare_LEASE}
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值