Go 客户端 - KV 操作
本章将深入学习 etcd 的键值操作,包括 Put、Get、Delete、范围查询等核心功能。
Put 操作
基本写入
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()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 基本写入
resp, err := cli.Put(ctx, "/config/database/host", "localhost")
if err != nil {
panic(err)
}
fmt.Printf("Put succeeded, revision: %d\n", resp.Header.Revision)
}带租约写入
go
func putWithLease(cli *clientv3.Client, key, value string, ttl int64) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 创建租约
leaseResp, err := cli.Lease.Grant(ctx, ttl)
if err != nil {
return err
}
// 带租约写入
_, err = cli.Put(ctx, key, value, clientv3.WithLease(leaseResp.ID))
return err
}
// 使用
putWithLease(cli, "/services/app/instance1", "192.168.1.10:8080", 60)带先前值检查(CAS)
go
// 只有当键不存在时才写入(Create 操作)
resp, err := cli.Put(ctx, "/lock/my-resource", "locked",
clientv3.WithPrevKV())
if resp.PrevKv != nil {
fmt.Println("键已存在,Put 操作成功但返回先前值")
fmt.Printf("先前值: %s\n", resp.PrevKv.Value)
}Get 操作
精确查询
go
func get(cli *clientv3.Client, key string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := cli.Get(ctx, key)
if err != nil {
return "", err
}
if len(resp.Kvs) == 0 {
return "", fmt.Errorf("key not found: %s", key)
}
return string(resp.Kvs[0].Value), nil
}查询详细信息
go
func getWithDetails(cli *clientv3.Client, key string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, _ := cli.Get(ctx, key)
if len(resp.Kvs) > 0 {
kv := resp.Kvs[0]
fmt.Printf("Key: %s\n", kv.Key)
fmt.Printf("Value: %s\n", kv.Value)
fmt.Printf("CreateRevision: %d\n", kv.CreateRevision)
fmt.Printf("ModRevision: %d\n", kv.ModRevision)
fmt.Printf("Version: %d\n", kv.Version)
fmt.Printf("Lease: %d\n", kv.Lease)
}
}获取 Revision
go
// 获取当前全局 revision
resp, _ := cli.Get(ctx, "any-key")
currentRevision := resp.Header.Revision
fmt.Printf("Current cluster revision: %d\n", currentRevision)范围查询
前缀查询
go
func getByPrefix(cli *clientv3.Client, prefix string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := cli.Get(ctx, prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
values := make([]string, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
values = append(values, string(kv.Value))
}
return values, nil
}
// 查询所有配置
configs, _ := getByPrefix(cli, "/config/")
for i, config := range configs {
fmt.Printf("Config %d: %s\n", i+1, config)
}范围查询
go
// 查询 [start, end) 范围内的键
func getByRange(cli *clientv3.Client, start, end string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, _ := cli.Get(ctx, start, clientv3.WithRange(end))
for _, kv := range resp.Kvs {
fmt.Printf("Key: %s, Value: %s\n", kv.Key, kv.Value)
}
}
// 查询 /a 到 /b 之间的键(不包括 /b)
getByRange(cli, "/a", "/b")限制数量
go
// 只返回前 10 个键
resp, _ := cli.Get(ctx, "/services/",
clientv3.WithPrefix(),
clientv3.WithLimit(10))
fmt.Printf("Total keys: %d, returned: %d\n", resp.Count, len(resp.Kvs))
// 如果 resp.More == true,表示还有更多数据
if resp.More {
fmt.Println("还有更多数据")
}分页查询
go
func paginate(cli *clientv3.Client, prefix string, pageSize int64) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var lastKey string
pageNum := 1
for {
opts := []clientv3.OpOption{
clientv3.WithPrefix(),
clientv3.WithLimit(pageSize),
clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend),
}
if lastKey != "" {
opts = append(opts, clientv3.WithFromKey())
}
resp, err := cli.Get(ctx, lastKey, opts...)
if err != nil {
panic(err)
}
fmt.Printf("Page %d: %d keys\n", pageNum, len(resp.Kvs))
for _, kv := range resp.Kvs {
fmt.Printf(" %s\n", kv.Key)
}
if !resp.More {
break
}
lastKey = string(resp.Kvs[len(resp.Kvs)-1].Key) + "\x00"
pageNum++
}
}排序查询
go
// 按键升序排序(默认)
resp, _ := cli.Get(ctx, "/services/",
clientv3.WithPrefix(),
clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
// 按版本降序排序
resp, _ := cli.Get(ctx, "/services/",
clientv3.WithPrefix(),
clientv3.WithSort(clientv3.SortByVersion, clientv3.SortDescend))
// 按 ModRevision 排序
resp, _ := cli.Get(ctx, "/services/",
clientv3.WithPrefix(),
clientv3.WithSort(clientv3.SortByModRevision, clientv3.SortAscend))只获取键数量
go
// 只统计数量,不返回数据
resp, _ := cli.Get(ctx, "/services/",
clientv3.WithPrefix(),
clientv3.WithCountOnly())
fmt.Printf("Total keys: %d\n", resp.Count)获取所有键
go
// 获取所有键(从空字符串开始)
resp, _ := cli.Get(ctx, "", clientv3.WithPrefix())
fmt.Printf("Total keys in cluster: %d\n", resp.Count)
for _, kv := range resp.Kvs {
fmt.Printf("Key: %s\n", kv.Key)
}历史版本查询
查询指定 Revision
go
// 查询 revision 100 时的值
resp, _ := cli.Get(ctx, "/foo", clientv3.WithRev(100))
if len(resp.Kvs) > 0 {
fmt.Printf("Value at revision 100: %s\n", resp.Kvs[0].Value)
}查询修改历史
go
// 注意:需要先设置 --auto-compaction-retention 才能查询历史版本
// 默认情况下,历史版本很快会被压缩删除
// 查询键的所有历史版本(从创建到当前)
func getAllVersions(cli *clientv3.Client, key string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 获取当前 revision
currentResp, _ := cli.Get(ctx, key)
currentRev := currentResp.Header.Revision
// 获取创建 revision
if len(currentResp.Kvs) > 0 {
createRev := currentResp.Kvs[0].CreateRevision
// 查询历史
for rev := createRev; rev <= currentRev; rev++ {
resp, _ := cli.Get(ctx, key, clientv3.WithRev(rev))
if len(resp.Kvs) > 0 {
fmt.Printf("Revision %d: %s\n", rev, resp.Kvs[0].Value)
}
}
}
}Delete 操作
删除单个键
go
func delete(cli *clientv3.Client, key string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := cli.Delete(ctx, key)
if err != nil {
return err
}
fmt.Printf("Deleted %d keys\n", resp.Deleted)
return nil
}删除前缀
go
func deleteByPrefix(cli *clientv3.Client, prefix string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, _ := cli.Delete(ctx, prefix, clientv3.WithPrefix())
fmt.Printf("Deleted %d keys\n", resp.Deleted)
}
// 删除所有服务注册信息
deleteByPrefix(cli, "/services/app/")范围删除
go
// 删除 [start, end) 范围内的键
resp, _ := cli.Delete(ctx, "/a", clientv3.WithRange("/b"))
fmt.Printf("Deleted %d keys\n", resp.Deleted)返回被删除的键值
go
resp, _ := cli.Delete(ctx, "/foo", clientv3.WithPrevKV())
fmt.Printf("Deleted %d keys\n", resp.Deleted)
if len(resp.PrevKvs) > 0 {
for _, kv := range resp.PrevKvs {
fmt.Printf("Deleted: %s = %s\n", kv.Key, kv.Value)
}
}批量操作
批量写入
使用事务实现批量操作:
go
func batchPut(cli *clientv3.Client, kvs map[string]string) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 构建 Put 操作列表
ops := make([]clientv3.Op, 0, len(kvs))
for key, value := range kvs {
ops = append(ops, clientv3.OpPut(key, value))
}
// 执行事务(无条件执行)
_, err := cli.Txn(ctx).Then(ops...).Commit()
return err
}
// 使用
batchPut(cli, map[string]string{
"/config/database/host": "localhost",
"/config/database/port": "3306",
"/config/database/username": "root",
"/config/database/password": "password",
})批量读取
go
func batchGet(cli *clientv3.Client, keys []string) (map[string]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 构建 Get 操作列表
ops := make([]clientv3.Op, 0, len(keys))
for _, key := range keys {
ops = append(ops, clientv3.OpGet(key))
}
// 执行事务
txnResp, err := cli.Txn(ctx).Then(ops...).Commit()
if err != nil {
return nil, err
}
// 解析结果
results := make(map[string]string)
for i, resp := range txnResp.Responses {
getResp := (*clientv3.GetResponse)(resp.GetResponseRange())
if len(getResp.Kvs) > 0 {
results[keys[i]] = string(getResp.Kvs[0].Value)
}
}
return results, nil
}Op 操作
使用 Op 对象
go
// 创建 Op 对象
putOp := clientv3.OpPut("/foo", "bar")
getOp := clientv3.OpGet("/foo")
deleteOp := clientv3.OpDelete("/foo")
// 执行 Op
resp, err := cli.Do(ctx, putOp)
resp, err = cli.Do(ctx, getOp)
resp, err = cli.Do(ctx, deleteOp)Op 配置
go
// 带 Lease 的 Put Op
leaseResp, _ := cli.Lease.Grant(ctx, 60)
putWithLease := clientv3.OpPut("/foo", "bar", clientv3.WithLease(leaseResp.ID))
// 前缀查询 Op
getByPrefix := clientv3.OpGet("/services/", clientv3.WithPrefix())
// 带 Revision 的 Get Op
getWithRev := clientv3.OpGet("/foo", clientv3.WithRev(100))实用示例
配置管理
go
type ConfigManager struct {
cli *clientv3.Client
prefix string
}
func NewConfigManager(cli *clientv3.Client, prefix string) *ConfigManager {
return &ConfigManager{cli: cli, prefix: prefix}
}
func (cm *ConfigManager) Set(key, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := cm.cli.Put(ctx, cm.prefix+key, value)
return err
}
func (cm *ConfigManager) Get(key string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := cm.cli.Get(ctx, cm.prefix+key)
if err != nil {
return "", err
}
if len(resp.Kvs) == 0 {
return "", fmt.Errorf("config not found")
}
return string(resp.Kvs[0].Value), nil
}
func (cm *ConfigManager) GetAll() (map[string]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
resp, err := cm.cli.Get(ctx, cm.prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
configs := make(map[string]string)
for _, kv := range resp.Kvs {
key := string(kv.Key)[len(cm.prefix):]
configs[key] = string(kv.Value)
}
return configs, nil
}
func (cm *ConfigManager) Delete(key string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := cm.cli.Delete(ctx, cm.prefix+key)
return err
}服务注册
go
func registerService(cli *clientv3.Client, serviceName, instanceID, addr string, ttl int64) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
key := fmt.Sprintf("/services/%s/%s", serviceName, instanceID)
// 创建租约
leaseResp, err := cli.Lease.Grant(ctx, ttl)
if err != nil {
return err
}
// 注册服务
_, err = cli.Put(ctx, key, addr, clientv3.WithLease(leaseResp.ID))
if err != nil {
return err
}
// 保持租约(在后台运行)
go func() {
keepAliveCtx, keepAliveCancel := context.WithCancel(context.Background())
defer keepAliveCancel()
keepAliveCh, err := cli.Lease.KeepAlive(keepAliveCtx, leaseResp.ID)
if err != nil {
return
}
for {
select {
case resp := <-keepAliveCh:
if resp == nil {
return
}
case <-keepAliveCtx.Done():
return
}
}
}()
return nil
}服务发现
go
func discoverServices(cli *clientv3.Client, serviceName string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
prefix := fmt.Sprintf("/services/%s/", serviceName)
resp, err := cli.Get(ctx, prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
}
instances := make([]string, 0, len(resp.Kvs))
for _, kv := range resp.Kvs {
instances = append(instances, string(kv.Value))
}
return instances, nil
}性能优化
使用串行化读
go
// 读取频繁但一致性要求不高时,使用串行化读
resp, _ := cli.Get(ctx, "/config/",
clientv3.WithPrefix(),
clientv3.WithSerializable())批量操作
go
// 一次性获取多个键,减少网络往返
txnResp, _ := cli.Txn(ctx).Then(
clientv3.OpGet("/key1"),
clientv3.OpGet("/key2"),
clientv3.OpGet("/key3"),
).Commit()避免大值
go
// 大值建议存储在对象存储,etcd 只存引用
largeData := "very large data..." // 1MB+
// 不推荐:cli.Put(ctx, "/large", largeData)
// 推荐:存储引用
cli.Put(ctx, "/large", "s3://bucket/path/to/data")最佳实践
键命名
- 使用有意义的前缀组织数据
- 避免过长的键名
查询优化
- 使用前缀查询减少网络往返
- 只需要数量时用 WithCountOnly
- 合理使用分页避免大量数据传输
写入优化
- 批量写入使用事务
- 临时数据使用租约
- 避免频繁写入大值
错误处理
- 区分键不存在和异常错误
- 网络错误时重试
资源管理
- 及时释放租约
- 避免创建过多 Watch
总结
本章学习了:
- Put 操作:基本写入、带租约写入、CAS
- Get 操作:精确查询、前缀查询、范围查询、排序
- Delete 操作:删除键、删除前缀、返回被删除值
- 批量操作:使用事务批量读写
- 实用示例:配置管理、服务注册发现
在下一章中,我们将学习 Watch 和 Lease 的高级用法。