etcd Watch 必须加 WithPrefix 或 WithRev,否则会丢变更;需用 WithPrevKV 补历史、WithRev 精确起点;Watch 循环须检查 resp.Err() 并按错误类型重试或退出;Put 应绑定 lease 防残留;本地缓存更新需加锁或原子替换。

clientv3.Watch 必须加 WithPrefix 或 WithRev,否则会丢变更
重启服务后配置没更新,或者只收到部分变更,大概率是 Watch 没指定起始版本或监听范围。etcd 的 Watch 默认从当前 revision 开始监听,如果服务启动时 etcd 已有历史写入,这些变更就直接跳过了。
正确做法是:监听前缀用 clientv3.WithPrefix();想补全历史变更,再叠加 clientv3.WithPrevKV();需要精确控制起点,就先 client.Get() 拿到最新 revision,再传给 clientv3.WithRev(rev)。
- 不要写
client.Watch(ctx, "/config/")—— 这实际等价于监听单个 key/config/,不是前缀 - 必须写成
client.Watch(ctx, "/config/", clientv3.WithPrefix()) - 如果 key 路径固定且数量少(比如就 3 个),显式列出比
WithPrefix()更省资源:client.Watch(ctx, "/config/a", "/config/b", "/config/c")
Watch 循环里不检查 resp.Err() 就 panic
网络抖动、lease 过期、etcd 集群滚动升级都会导致 Watch 流中断。此时 watchChan 关闭,继续 range 会触发 send on closed channel 或 invalid memory address panic。
标准处理模式是:每次循环开头判断 if !ok || resp.Err() != nil,然后根据错误类型决定退出还是重试。
立即学习“go语言免费学习笔记(深入)”;
-
resp.Err()返回context.Canceled或context.DeadlineExceeded→ 主动退出并指数退避重连 - 返回
rpc error: code = Canceled desc = context canceled→ 同上,不是 etcd 问题,是 context 被 cancel - 其他错误(如连接拒绝)建议记录日志后立即重试,不要 sleep 固定秒数
Put 操作不绑 lease,配置残留难清理
微服务下线或配置误写后,key 会长期滞留在 etcd 中,既占空间又干扰 Watch。etcd 提供租约(lease)机制,让 key 在 lease 过期后自动删除。
所有写操作都应绑定 lease,尤其用于服务注册、临时开关、动态限流阈值这类有生命周期的配置。
- 创建 lease:
leaseResp, err := client.Grant(ctx, 30)(单位秒) - 写入时带上:
client.Put(ctx, key, value, clientv3.WithLease(leaseResp.ID)) - 读操作(
Get)和监听(Watch)不需要 lease - 注意:lease ID 是 int64,别当成字符串传错
本地缓存更新没加锁,多 goroutine 并发读写 panic
Watch 回调函数可能被多个 goroutine 并发调用(尤其当一次批量更新触发多个事件),如果直接更新全局结构体字段,会出现 data race 或 panic。
最轻量安全的做法是:用 sync.RWMutex 包裹整个配置结构体,读用 RLock(),写用 Lock();更高级场景可用 atomic.Pointer 原子替换指针,但要求新旧配置对象完全不可变。
- 错误示范:
config.LogLevel = newValue(无锁直写) - 正确写法:先
config.mu.Lock(),再赋值,最后config.mu.Unlock() - 如果配置解析失败(如 JSON 解析出错),必须丢弃本次变更,继续用旧值 —— 不能 panic,也不能覆盖为零值


















