【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
}
Put和Get最后一个参数是可选参数,用于操作配置,常用的有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 接口返回一个类型为 WatchResponse 的 chan
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事件有两种类型,PUT 和 DELETE,分别对应相应事件。当 Type 为PUT 时,Kv中存放当前最新的 Kv 键值对,PrevKv看注释是修改之前的,但是我在测试时始终为nil;当 Type 为 DELETE 时,Kv中存放已被删除的Key,value是nil。
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
}
这里主要用到 Grant 和 KeepAlive 两个接口,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}
}

1万+

被折叠的 条评论
为什么被折叠?



