Go 客户端 - Watch 和 Lease
本章将学习 etcd 的监听机制和租约机制,这两个特性在实时通知和临时数据场景中非常重要。
Watch 监听
基本概念
Watch 用于监听键或前缀的变更事件,实现实时通知。
客户端 etcd
│ │
│ Watch /foo │
│─────────────────>│
│ │
│ WatchResponse │
│<─────────────────│ Put /foo = "bar"
│ │
│ WatchResponse │
│<─────────────────│ Put /foo = "bar2"
│ │
│ WatchResponse │
│<─────────────────│ Delete /foo
│ │监听单个键
go
package main
import (
"context"
"fmt"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
// 监听单个键
watchCh := cli.Watch(context.Background(), "/foo")
for watchResp := range watchCh {
for _, event := range watchResp.Events {
switch event.Type {
case clientv3.EventTypePut:
fmt.Printf("Put: %s = %s\n", event.Kv.Key, event.Kv.Value)
case clientv3.EventTypeDelete:
fmt.Printf("Delete: %s\n", event.Kv.Key)
}
}
}
}监听前缀
go
// 监听所有配置变更
watchCh := cli.Watch(context.Background(), "/config/", clientv3.WithPrefix())
for watchResp := range watchCh {
for _, event := range watchResp.Events {
fmt.Printf("Event: %s, Key: %s, Value: %s\n",
event.Type, event.Kv.Key, event.Kv.Value)
}
}监听范围
go
// 监听 [start, end) 范围
watchCh := cli.Watch(context.Background(), "/a", clientv3.WithRange("/b"))监听历史变更
go
// 从指定 revision 开始监听(包括历史事件)
// 获取当前 revision
getResp, _ := cli.Get(context.Background(), "/foo")
startRev := getResp.Header.Revision
// 监听从 startRev 开始的所有变更
watchCh := cli.Watch(context.Background(), "/foo", clientv3.WithRev(startRev))
for watchResp := range watchCh {
for _, event := range watchResp.Events {
fmt.Printf("Rev: %d, Type: %s, Key: %s\n",
event.Kv.ModRevision, event.Type, event.Kv.Key)
}
}Watch 事件类型
go
type EventType int
const (
EventTypePut EventType = 0 // Put 或 Update
EventTypeDelete EventType = 1 // Delete
)
// 事件详情
type Event struct {
Type EventType
Kv *KeyValue // 当前值(Put 时有)
PrevKv *KeyValue // 先前值(WithPrevKV() 时有)
}获取先前值
go
// 监听时获取变更前的值
watchCh := cli.Watch(context.Background(), "/foo", clientv3.WithPrevKV())
for watchResp := range watchCh {
for _, event := range watchResp.Events {
if event.PrevKv != nil {
fmt.Printf("Old: %s, New: %s\n", event.PrevKv.Value, event.Kv.Value)
}
}
}Watch 响应处理
go
func handleWatchResponse(watchResp clientv3.WatchResponse) {
// 检查错误
if watchResp.Err() != nil {
fmt.Printf("Watch error: %v\n", watchResp.Err())
return
}
// 检查取消
if watchResp.Canceled {
fmt.Println("Watch canceled")
return
}
// 处理事件
for _, event := range watchResp.Events {
switch event.Type {
case clientv3.EventTypePut:
fmt.Printf("Put: %s = %s (Rev: %d)\n",
event.Kv.Key, event.Kv.Value, event.Kv.ModRevision)
case clientv3.EventTypeDelete:
fmt.Printf("Delete: %s (Rev: %d)\n",
event.Kv.Key, event.Kv.ModRevision)
}
}
}Watch 进度通知
go
// 创建 Watch 时请求进度通知
watchCh := cli.Watch(context.Background(), "/foo",
clientv3.WithProgressNotify())
for watchResp := range watchCh {
if watchResp.IsProgressNotify() {
fmt.Printf("Progress notify, Revision: %d\n", watchResp.Header.Revision)
continue
}
for _, event := range watchResp.Events {
fmt.Printf("Event: %s\n", event.Type)
}
}取消监听
go
ctx, cancel := context.WithCancel(context.Background())
watchCh := cli.Watch(ctx, "/foo")
// 在需要时取消
cancel()
// Watch channel 会关闭
for watchResp := range watchCh {
if watchResp.Canceled {
break
}
}Watch 重连
go
func watchWithRetry(cli *clientv3.Client, key string) {
var watchRev int64 = 0
for {
ctx, cancel := context.WithCancel(context.Background())
opts := []clientv3.OpOption{}
if watchRev > 0 {
opts = append(opts, clientv3.WithRev(watchRev))
}
watchCh := cli.Watch(ctx, key, opts...)
for watchResp := range watchCh {
if watchResp.Err() != nil {
fmt.Printf("Watch error: %v, reconnecting...\n", watchResp.Err())
cancel()
time.Sleep(1 * time.Second)
break
}
for _, event := range watchResp.Events {
watchRev = event.Kv.ModRevision + 1
fmt.Printf("Event: %s\n", event.Type)
}
}
cancel()
}
}Lease 租约
基本概念
租约(Lease)为键设置生存时间(TTL),租约到期后关联的键自动删除。
┌──────────────────────────────────────────────┐
│ Lease (TTL=60s) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Key1 │ │Key2 │ │Key3 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 租约到期 → 所有键自动删除 │
└──────────────────────────────────────────────┘创建租约
go
func main() {
cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 创建 60 秒租约
leaseResp, err := cli.Lease.Grant(ctx, 60)
if err != nil {
panic(err)
}
fmt.Printf("Lease ID: %d, TTL: %d\n", leaseResp.ID, leaseResp.TTL)
// 将键绑定到租约
_, err = cli.Put(ctx, "/services/app/instance1", "192.168.1.10:8080",
clientv3.WithLease(leaseResp.ID))
if err != nil {
panic(err)
}
}查询租约
go
// 查询租约剩余时间
leaseID := leaseResp.ID
ttlResp, err := cli.Lease.TimeToLive(ctx, leaseID)
if err != nil {
panic(err)
}
fmt.Printf("Lease ID: %d, TTL: %d, GrantedTTL: %d\n",
ttlResp.ID, ttlResp.TTL, ttlResp.GrantedTTL)
// 查询租约关联的键
if ttlResp.Keys != nil {
for _, key := range ttlResp.Keys {
fmt.Printf("Key: %s\n", key)
}
}KeepAlive 续租
go
// 单次续租
keepAliveResp, err := cli.Lease.KeepAliveOnce(ctx, leaseID)
if err != nil {
panic(err)
}
fmt.Printf("KeepAlive succeeded, TTL: %d\n", keepAliveResp.TTL)自动续租
go
// 持续自动续租
func keepAlive(cli *clientv3.Client, leaseID clientv3.LeaseID) {
keepAliveCh, err := cli.Lease.KeepAlive(context.Background(), leaseID)
if err != nil {
panic(err)
}
for resp := range keepAliveCh {
if resp == nil {
fmt.Println("KeepAlive channel closed")
return
}
fmt.Printf("KeepAlive: LeaseID=%d, TTL=%d\n", resp.ID, resp.TTL)
}
}
// 在后台运行
go keepAlive(cli, leaseResp.ID)撤销租约
go
// 手动撤销租约(立即删除关联的键)
_, err := cli.Lease.Revoke(ctx, leaseID)
if err != nil {
panic(err)
}
fmt.Println("Lease revoked")租约列表
go
// 查询所有租约
leasesResp, err := cli.Lease.Leases(ctx)
if err != nil {
panic(err)
}
for _, lease := range leasesResp.Leases {
fmt.Printf("Lease ID: %d\n", lease.ID)
}实用示例
动态配置监听
go
type ConfigWatcher struct {
cli *clientv3.Client
prefix string
config map[string]string
}
func NewConfigWatcher(cli *clientv3.Client, prefix string) *ConfigWatcher {
cw := &ConfigWatcher{
cli: cli,
prefix: prefix,
config: make(map[string]string),
}
// 初始化:读取当前配置
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
resp, _ := cli.Get(ctx, prefix, clientv3.WithPrefix())
cancel()
for _, kv := range resp.Kvs {
key := string(kv.Key)[len(prefix):]
cw.config[key] = string(kv.Value)
}
// 启动监听
go cw.watch()
return cw
}
func (cw *ConfigWatcher) watch() {
watchCh := cw.cli.Watch(context.Background(), cw.prefix, clientv3.WithPrefix())
for watchResp := range watchCh {
for _, event := range watchResp.Events {
key := string(event.Kv.Key)[len(cw.prefix):]
switch event.Type {
case clientv3.EventTypePut:
cw.config[key] = string(event.Kv.Value)
fmt.Printf("Config updated: %s = %s\n", key, cw.config[key])
case clientv3.EventTypeDelete:
delete(cw.config, key)
fmt.Printf("Config deleted: %s\n", key)
}
}
}
}
func (cw *ConfigWatcher) Get(key string) string {
return cw.config[key]
}服务注册(带租约)
go
type ServiceRegistry struct {
cli *clientv3.Client
leaseID clientv3.LeaseID
keepAlive context.CancelFunc
}
func RegisterService(cli *clientv3.Client, serviceName, instanceID, addr string, ttl int64) (*ServiceRegistry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// 创建租约
leaseResp, err := cli.Lease.Grant(ctx, ttl)
if err != nil {
cancel()
return nil, err
}
// 注册服务
key := fmt.Sprintf("/services/%s/%s", serviceName, instanceID)
_, err = cli.Put(ctx, key, addr, clientv3.WithLease(leaseResp.ID))
if err != nil {
cancel()
return nil, err
}
cancel()
// 启动 KeepAlive
keepAliveCtx, keepAliveCancel := context.WithCancel(context.Background())
keepAliveCh, err := cli.Lease.KeepAlive(keepAliveCtx, leaseResp.ID)
if err != nil {
keepAliveCancel()
return nil, err
}
// 处理 KeepAlive 响应
go func() {
for {
select {
case resp := <-keepAliveCh:
if resp == nil {
fmt.Println("KeepAlive stopped")
return
}
case <-keepAliveCtx.Done():
return
}
}
}()
return &ServiceRegistry{
cli: cli,
leaseID: leaseResp.ID,
keepAlive: keepAliveCancel,
}, nil
}
func (sr *ServiceRegistry) Unregister() {
// 取消 KeepAlive
sr.keepAlive()
// 撤销租约
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
sr.cli.Lease.Revoke(ctx, sr.leaseID)
cancel()
}分布式锁(基于 Lease)
go
type DistributedLock struct {
cli *clientv3.Client
key string
leaseID clientv3.LeaseID
}
func AcquireLock(cli *clientv3.Client, key string, ttl int64) (*DistributedLock, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 创建租约
leaseResp, err := cli.Lease.Grant(ctx, ttl)
if err != nil {
return nil, err
}
// 尝试获取锁(事务)
txnResp, err := cli.Txn(ctx).
If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).
Then(clientv3.OpPut(key, "locked", clientv3.WithLease(leaseResp.ID))).
Else(clientv3.OpGet(key)).
Commit()
if err != nil {
cli.Lease.Revoke(ctx, leaseResp.ID)
return nil, err
}
if !txnResp.Succeeded {
cli.Lease.Revoke(ctx, leaseResp.ID)
return nil, fmt.Errorf("lock already held")
}
// 启动 KeepAlive
go func() {
keepAliveCh, _ := cli.Lease.KeepAlive(context.Background(), leaseResp.ID)
for resp := range keepAliveCh {
if resp == nil {
return
}
}
}()
return &DistributedLock{
cli: cli,
key: key,
leaseID: leaseResp.ID,
}, nil
}
func (lock *DistributedLock) Release() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 撤销租约(自动删除键)
_, err := lock.cli.Lease.Revoke(ctx, lock.leaseID)
return err
}会话管理(租约)
go
type Session struct {
cli *clientv3.Client
leaseID clientv3.LeaseID
prefix string
}
func NewSession(cli *clientv3.Client, ttl int64) (*Session, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
leaseResp, err := cli.Lease.Grant(ctx, ttl)
if err != nil {
return nil, err
}
keepAliveCh, err := cli.Lease.KeepAlive(context.Background(), leaseResp.ID)
if err != nil {
cli.Lease.Revoke(ctx, leaseResp.ID)
return nil, err
}
go func() {
for resp := range keepAliveCh {
if resp == nil {
return
}
}
}()
return &Session{
cli: cli,
leaseID: leaseResp.ID,
prefix: fmt.Sprintf("/session/%d/", leaseResp.ID),
}, nil
}
func (s *Session) Put(key, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
fullKey := s.prefix + key
_, err := s.cli.Put(ctx, fullKey, value, clientv3.WithLease(s.leaseID))
return err
}
func (s *Session) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := s.cli.Lease.Revoke(ctx, s.leaseID)
return err
}性能优化
Watch 批量处理
go
// Watch 响应是批量返回的,减少处理频率
for watchResp := range watchCh {
// 批量处理事件
events := watchResp.Events
for _, event := range events {
// ...
}
}租约复用
go
// 多个键共享一个租约
leaseResp, _ := cli.Lease.Grant(ctx, 60)
cli.Put(ctx, "/session/user1/data1", "value1", clientv3.WithLease(leaseResp.ID))
cli.Put(ctx, "/session/user1/data2", "value2", clientv3.WithLease(leaseResp.ID))
cli.Put(ctx, "/session/user1/data3", "value3", clientv3.WithLease(leaseResp.ID))
// 租约到期时,所有键一起删除减少网络往返
go
// 使用 ProgressNotify 减少不必要的网络请求
watchCh := cli.Watch(context.Background(), "/foo", clientv3.WithProgressNotify())
// KeepAlive 批量发送
// etcd 会批量处理 KeepAlive 请求最佳实践
Watch 使用
- 使用前缀监听而不是多个单键监听
- 处理错误和取消情况
- 合理设置 revision 起始点
KeepAlive 管理
- 确保 KeepAlive goroutine 能正确退出
- 使用 context 控制取消
- 处理 KeepAlive channel 关闭
租约设计
- TTL 设置合理的值(如 10-60 秒)
- 相关数据共享租约
- 及时撤销不需要的租约
资源清理
- 取消监听时关闭 context
- 退出时撤销租约
- 避免 goroutine 泄漏
总结
本章学习了:
- Watch 监听:监听变更、获取事件、重连处理
- Lease 租约:创建、查询、续租、撤销
- 实用示例:动态配置、服务注册、分布式锁、会话管理
在下一章中,我们将学习事务和分布式锁的高级用法。