创建客户端
我们来看看创建客户端的代码:
func newClient(config *Config) *Client {
// etcdv3 的配置
conf := clientv3.Config{
Endpoints: config.Endpoints,
DialTimeout: config.ConnectTimeout,
DialKeepAliveTime: 10 * time.Second,
DialKeepAliveTimeout: 3 * time.Second,
DialOptions: []grpc.DialOption{
grpc.WithBlock(),
grpc.WithUnaryInterceptor(grpcprom.UnaryClientInterceptor),
grpc.WithStreamInterceptor(grpcprom.StreamClientInterceptor),
},
AutoSyncInterval: config.AutoSyncInterval,
}
...
// 调用 clientv3 方法连接
client, err := clientv3.New(conf)
if err != nil {
config.logger.Panic("client etcd start panic", xlog.FieldMod(ecode.ModClientETCD), xlog.FieldErrKind(ecode.ErrKindAny), xlog.FieldErr(err), xlog.FieldValueAny(config))
}
}
获取和存储
etcdv3 的 kv 接口:
type KV interface {
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
Do(ctx context.Context, op Op) (OpResponse, error)
Txn(ctx context.Context) Txn
}
- Put,Get,Delete 三个方法其实是 Do() 方法的具体行为的封装。
获取和通过前缀获取
// GetKeyValue queries etcd key, returns mvccpb.KeyValue
func (client *Client) GetKeyValue(ctx context.Context, key string) (kv *mvccpb.KeyValue, err error) {
rp, err := client.Client.Get(ctx, key)
...
}
// GetPrefix get prefix
func (client *Client) GetPrefix(ctx context.Context, prefix string) (map[string]string, error) {
resp, err := client.Get(ctx, prefix, clientv3.WithPrefix())
if err != nil {
return vars, err
}
...
}
获取单个 key/value 和 通过前缀获取 key/value 的区别在于通过一个 OpOption 方法 WithPrefix() 。
存储
存储在 jupiter 中没有单独进行封装,而是直接调用 etcdv3 的客户端方法。
func (kv *kv) Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) {
etcdv3 客户端库中的 Put 方法。在 grpc 服务注册就是讲服务信息写入到 etcd 中。
删除
// DelPrefix 按前缀删除
func (client *Client) DelPrefix(ctx context.Context, prefix string) (deleted int64, err error) {
resp, err := client.Delete(ctx, prefix, clientv3.WithPrefix())
...
}
根据前缀删除信息。
获取多个 key 的值
在 etcd 中要想获取到多个不同 key 的值,除了一个一个获取之外,可以通过 etcd 的事务方法 Txn() 来查询多个 key 的结果。
// GetValues queries etcd for keys prefixed by prefix.
func (client *Client) GetValues(ctx context.Context, keys ...string) (map[string]string, error) {
var (
firstRevision = int64(0)
vars = make(map[string]string)
maxTxnOps = 128 // 最大的提交数
getOps = make([]string, 0, maxTxnOps)
)
// 具体的事务查询处理方法
doTxn := func(ops []string) error {
txnOps := make([]clientv3.Op, 0, maxTxnOps)
// 添加查询操作
for _, k := range ops {
txnOps = append(txnOps, clientv3.OpGet(k,
clientv3.WithPrefix(),
clientv3.WithSort(clientv3.SortByKey, clientv3.SortDescend),
clientv3.WithRev(firstRevision)))
}
// 提交事务
result, err := client.Txn(ctx).Then(txnOps...).Commit()
if err != nil {
return err
}
// 处理返回的结果
for i, r := range result.Responses {
...
}
// 获取修订的最新版本号
if firstRevision == 0 {
firstRevision = result.Header.GetRevision()
}
return nil
}
for _, key := range keys {
// 添加需要提交的key,同时判断是否到达最大提交数,到达则提交进行事务查询
getOps = append(getOps, key)
if len(getOps) >= maxTxnOps {
if err := doTxn(getOps); err != nil {
return vars, err
}
getOps = getOps[:0]
}
}
// 判断是否存在还未提交的 key,有则进行查询
if len(getOps) > 0 {
if err := doTxn(getOps); err != nil {
return vars, err
}
}
return vars, nil
}
方法开始声明了 maxTxOps 最大提交数。doTxn 方法是实际事务查询函数,然后将 keys 分成 maxTxOps 最大数一份进行提交查询。这样保证不会因为 keys 大多而查询很慢而超时,也保证了速度。
持续监控
下面是创建一个批量持续监控 key 的变化:
// NewWatch 创建持续监控
func (client *Client) WatchPrefix(ctx context.Context, prefix string) (*Watch, error) {
resp, err := client.Get(ctx, prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
var w = &Watch{
revision: resp.Header.Revision,
eventChan: make(chan *clientv3.Event, 100),
incipientKVs: resp.Kvs,
}
xgo.Go(func() {
ctx, cancel := context.WithCancel(context.Background())
w.cancel = cancel
// 请求监控
rch := client.Client.Watch(ctx, prefix, clientv3.WithPrefix(), clientv3.WithCreatedNotify(), clientv3.WithRev(w.revision))
for {
for n := range rch {
...
for _, ev := range n.Events {
select {
case w.eventChan <- ev:
default:
xlog.Error("watch etcd with prefix", xlog.Any("err", "block event chan, drop event message"))
}
}
}
ctx, cancel := context.WithCancel(context.Background())
w.cancel = cancel
if w.revision > 0 {
rch = client.Watch(ctx, prefix, clientv3.WithPrefix(), clientv3.WithCreatedNotify(), clientv3.WithRev(w.revision))
} else {
rch = client.Watch(ctx, prefix, clientv3.WithPrefix(), clientv3.WithCreatedNotify())
}
}
})
return w, nil
}
这里通过 etcdv3 客户端的 Watch() 方法请求到一个监控通道,然后不断的接收 WatchChan 类型的值,它实际是一个 WatchResponse 结构体,有 etcd 推送过来的返回值。然后我们不断的处理返回值中的事件 Events 。
一个 Event 的有 PUT、DELETE 两种类型,表示更新和删除某些 key/value 。
租约机制
首先可以了解一下分布式系统技术系列--租约(lease)
在 etcd 中租约机制的本质是给 k-v 设置一个过期时间,之后服务每隔一段时间需要对 k-v 进行续约,不然 k-v 会被自动清理掉。
创建租约代码:
// 创建一个租约
func (reg *etcdv3Registry) getSession(k string, opts ...concurrency.SessionOption) (*concurrency.Session, error) {
...
sess, err := concurrency.NewSession(reg.client.Client, opts...)
if err != nil {
return sess, err
}
reg.rmu.Lock()
reg.sessions[k] = sess
reg.rmu.Unlock()
return sess, nil
}
// 使用租约
if ttl := reg.Config.ServiceTTL.Seconds(); ttl > 0 {
sess, err := reg.getSession(key, concurrency.WithTTL(int(ttl)))
if err != nil {
return err
}
opOptions = append(opOptions, clientv3.WithLease(sess.Lease()))
}
// 提交信息到 etcd
_, err := reg.client.Put(readCtx, key, val, opOptions...)
租约的创建时在 etcd 库的 concurrency 包中。使用的方式也很简单,在请求的 options 中加上 WithLease() 的参数。
分布式锁
etcd 的分布式锁和我们平常使用的 sync.Mutex 很类似,有两个方法 Lock() 和 Unlock() 。
// Mutex ...
type Mutex struct {
s *concurrency.Session
m *concurrency.Mutex
}
// NewMutex ...
func (client *Client) NewMutex(key string, opts ...concurrency.SessionOption) (mutex *Mutex, err error) {
mutex = &Mutex{}
// 默认session ttl = 60s
mutex.s, err = concurrency.NewSession(client.Client, opts...)
if err != nil {
return
}
mutex.m = concurrency.NewMutex(mutex.s, key)
return
}
// Lock ...
func (mutex *Mutex) Lock(timeout time.Duration) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return mutex.m.Lock(ctx)
}
// Unlock ...
func (mutex *Mutex) Unlock() (err error) {
err = mutex.m.Unlock(context.TODO())
if err != nil {
return
}
return mutex.s.Close()
}
不同的地方在于分布式锁都有一个过期时间。