Go 客户端 - 事务和锁
本章将深入学习 etcd 的事务机制和并发控制,包括分布式锁、选举等高级用法。
事务机制
事务概述
etcd 事务提供原子性的多操作执行,支持条件判断。
Txn 结构:
┌─────────────────────────────────────┐
│ Compare (条件判断) │
│ ├── Key1.Value == "expected" │
│ ├── Key2.CreateRevision > 0 │
│ └── Key3.Version < 10 │
├─────────────────────────────────────┤
│ Success (条件满足时执行) │
│ ├── Put Key1 "new-value" │
│ ├── Put Key2 "another-value" │
│ └── Get Key3 │
├─────────────────────────────────────┤
│ Failure (条件不满足时执行) │
│ ├── Get Key1 │
│ └── Get Key2 │
└─────────────────────────────────────┘基本事务
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()
// 基本事务:检查键是否存在
txnResp, err := cli.Txn(ctx).
If(clientv3.Compare(clientv3.Value("/foo"), "=", "bar")).
Then(clientv3.OpPut("/foo", "new-bar")).
Else(clientv3.OpGet("/foo")).
Commit()
if err != nil {
panic(err)
}
if txnResp.Succeeded {
fmt.Println("Transaction succeeded, value updated")
} else {
fmt.Println("Transaction failed, value not equal to 'bar'")
getResp := (*clientv3.GetResponse)(txnResp.Responses[0].GetResponseRange())
if len(getResp.Kvs) > 0 {
fmt.Printf("Current value: %s\n", getResp.Kvs[0].Value)
}
}
}Compare 操作
go
// 多种比较类型
txnResp, _ := cli.Txn(ctx).
If(
// 比较值
clientv3.Compare(clientv3.Value("/key"), "=", "expected"),
// 比较创建 revision
clientv3.Compare(clientv3.CreateRevision("/key"), "=", 0),
// 比较修改 revision
clientv3.Compare(clientv3.ModRevision("/key"), ">", 100),
// 比较版本
clientv3.Compare(clientv3.Version("/key"), "<", 10),
// 比较租约
clientv3.Compare(clientv3.Lease("/key"), "=", leaseID),
).
Then(clientv3.OpPut("/key", "new-value")).
Commit()比较运算符
| 运算符 | 说明 |
|---|---|
"=" | 等于 |
"<" | 小于 |
"<=" | 小于等于 |
">" | 大于 |
">=" | 大于等于 |
"!=" | 不等于 |
多条件事务
go
// 多个条件(AND 逻辑)
txnResp, _ := cli.Txn(ctx).
If(
clientv3.Compare(clientv3.Value("/key1"), "=", "value1"),
clientv3.Compare(clientv3.Value("/key2"), "=", "value2"),
).
Then(
clientv3.OpPut("/key1", "new-value1"),
clientv3.OpPut("/key2", "new-value2"),
).
Else(
clientv3.OpGet("/key1"),
clientv3.OpGet("/key2"),
).
Commit()批量操作(无条件)
go
// 无条件执行批量操作
txnResp, _ := cli.Txn(ctx).
Then(
clientv3.OpPut("/key1", "value1"),
clientv3.OpPut("/key2", "value2"),
clientv3.OpPut("/key3", "value3"),
).
Commit()读取批量操作结果
go
// 执行多个 Get 操作
txnResp, _ := cli.Txn(ctx).
Then(
clientv3.OpGet("/key1"),
clientv3.OpGet("/key2"),
clientv3.OpGet("/key3"),
).
Commit()
// 解析结果
for i, resp := range txnResp.Responses {
getResp := (*clientv3.GetResponse)(resp.GetResponseRange())
if len(getResp.Kvs) > 0 {
fmt.Printf("Key %d: %s\n", i+1, getResp.Kvs[0].Value)
}
}分布式锁
原理
etcd 分布式锁基于事务和租约实现:
- 检查锁键是否存在(CreateRevision == 0)
- 如果不存在,创建锁键并绑定租约
- 如果存在,等待或返回失败
基本锁实现
go
func acquireLock(cli *clientv3.Client, key string, ttl int64) (clientv3.LeaseID, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 创建租约
leaseResp, err := cli.Lease.Grant(ctx, ttl)
if err != nil {
return 0, err
}
// 尝试获取锁
txnResp, err := cli.Txn(ctx).
If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).
Then(clientv3.OpPut(key, "locked", clientv3.WithLease(leaseResp.ID))).
Commit()
if err != nil {
cli.Lease.Revoke(ctx, leaseResp.ID)
return 0, err
}
if !txnResp.Succeeded {
cli.Lease.Revoke(ctx, leaseResp.ID)
return 0, 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 leaseResp.ID, nil
}
func releaseLock(cli *clientv3.Client, leaseID clientv3.LeaseID) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := cli.Lease.Revoke(ctx, leaseID)
return err
}等待锁释放
go
func waitForLock(cli *clientv3.Client, key string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 监听锁键的删除事件
watchCh := cli.Watch(ctx, key)
for watchResp := range watchCh {
for _, event := range watchResp.Events {
if event.Type == clientv3.EventTypeDelete {
fmt.Println("Lock released")
return nil
}
}
}
return fmt.Errorf("timeout waiting for lock")
}使用 Mutex SDK
etcd 官方提供了并发控制 SDK:go.etcd.io/etcd/client/v3/concurrency
bash
go get go.etcd.io/etcd/client/v3/concurrency@v3.5.17go
import (
"context"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
)
func mutexExample() {
cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
// 创建 Session(管理租约)
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))
// 创建 Mutex
mutex := concurrency.NewMutex(session, "/locks/my-resource")
// 获取锁(阻塞)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := mutex.Lock(ctx); err != nil {
fmt.Println("Failed to acquire lock:", err)
return
}
fmt.Println("Lock acquired")
// 执行关键操作
doCriticalWork()
// 释放锁
mutex.Unlock(context.Background())
fmt.Println("Lock released")
session.Close()
}尝试获取锁(非阻塞)
go
// TryLock 不阻塞,立即返回结果
if err := mutex.TryLock(ctx); err != nil {
fmt.Println("Lock not available:", err)
return
}
// 成功获取锁
doCriticalWork()
mutex.Unlock(ctx)带超时的锁
go
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := mutex.Lock(ctx); err != nil {
if err == context.DeadlineExceeded {
fmt.Println("Timeout acquiring lock")
}
return
}
// 成功获取锁
mutex.Unlock(context.Background())Leader 选举
原理
Leader 选举基于分布式锁:获得锁的节点成为 Leader,其他节点成为 Follower。
候选者流程:
1. 尝试获取选举锁
2. 成功 → 成为 Leader,保持锁
3. 失败 → 成为 Follower,监听锁释放
4. 锁释放 → 再次尝试获取锁使用 Election SDK
go
func electionExample() {
cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
// 创建 Session
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))
// 创建 Election
election := concurrency.NewElection(session, "/elections/my-leader")
// 参加选举
ctx := context.Background()
// Campaign 成为 Leader(阻塞)
if err := election.Campaign(ctx, "node-1"); err != nil {
fmt.Println("Campaign failed:", err)
return
}
fmt.Println("I am the Leader!")
// 执行 Leader 工作
go doLeaderWork()
// 监听 Leader 变更
go observeLeader(cli, election)
// 保持 Leader 状态
select {}
// 主动放弃 Leader
election.Resign(ctx)
}
func observeLeader(cli *clientv3.Client, election *concurrency.Election) {
ch := election.Observe(context.Background())
for resp := range ch {
if len(resp.Kvs) > 0 {
leader := string(resp.Kvs[0].Value)
fmt.Printf("Current leader: %s\n", leader)
}
}
}查询当前 Leader
go
// 查询当前 Leader
leader, _ := election.Leader(context.Background())
fmt.Printf("Current leader: %s\n", leader)
// Proclaim 更新 Leader 值
election.Proclaim(ctx, "new-leader-value")选举示例:主备切换
go
type LeaderService struct {
cli *clientv3.Client
session *concurrency.Session
election *concurrency.Election
isLeader bool
}
func NewLeaderService(cli *clientv3.Client, name string) *LeaderService {
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))
election := concurrency.NewElection(session, "/elections/service")
ls := &LeaderService{
cli: cli,
session: session,
election: election,
}
// 启动选举循环
go ls.runElection(name)
return ls
}
func (ls *LeaderService) runElection(name string) {
for {
ctx := context.Background()
// 参加选举
err := ls.election.Campaign(ctx, name)
if err != nil {
fmt.Println("Campaign error:", err)
time.Sleep(1 * time.Second)
continue
}
// 成为 Leader
ls.isLeader = true
fmt.Println("I am the Leader now!")
// 执行 Leader 工作
ls.doLeaderWork()
// Leader 工作结束(可能是主动 Resign 或 Session 失效)
ls.isLeader = false
fmt.Println("I am no longer the Leader")
}
}
func (ls *LeaderService) doLeaderWork() {
// Leader 负责的工作
// 例如:定期执行任务、处理关键请求等
}
func (ls *LeaderService) Resign() error {
return ls.election.Resign(context.Background())
}
func (ls *LeaderService) Close() {
ls.session.Close()
}STM(软件事务内存)
etcd 提供 STM(Software Transactional Memory)支持更复杂的事务逻辑。
基本 STM
go
import "go.etcd.io/etcd/client/v3/concurrency"
func stmExample(cli *clientv3.Client) {
// STM 事务:原子性地读取和修改多个键
resp, err := concurrency.NewSTM(cli, func(stm concurrency.STM) error {
// 读取值
value1 := stm.Get("/key1")
value2 := stm.Get("/key2")
// 修改值
stm.Put("/key1", value1+"-modified")
stm.Put("/key2", value2+"-modified")
return nil
})
if err != nil {
fmt.Println("STM failed:", err)
}
}银行转账示例
go
func transfer(cli *clientv3.Client, from, to string, amount int) error {
_, err := concurrency.NewSTM(cli, func(stm concurrency.STM) error {
// 获取账户余额
fromBalance := stm.Get("/accounts/" + from)
toBalance := stm.Get("/accounts/" + to)
// 解析余额
fromInt, _ := strconv.Atoi(fromBalance)
toInt, _ := strconv.Atoi(toBalance)
// 检查余额
if fromInt < amount {
return fmt.Errorf("insufficient balance")
}
// 更新余额
stm.Put("/accounts/"+from, strconv.Itoa(fromInt-amount))
stm.Put("/accounts/"+to, strconv.Itoa(toInt+amount))
return nil
})
return err
}STM 嵌套操作
go
// STM 支持复杂的条件逻辑
resp, err := concurrency.NewSTM(cli, func(stm concurrency.STM) error {
value := stm.Get("/config/feature")
if value == "enabled" {
stm.Put("/config/count", stm.Get("/config/count")+"1")
} else {
stm.Put("/config/feature", "enabled")
}
return nil
})实用示例
限流器
go
func rateLimiter(cli *clientv3.Client, key string, limit int, window time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
now := time.Now().Unix()
windowStart := now - int64(window.Seconds())
// 获取窗口内的计数
resp, _ := cli.Get(ctx, key)
var count int64 = 0
if len(resp.Kvs) > 0 {
count, _ = strconv.ParseInt(string(resp.Kvs[0].Value), 10, 64)
}
// 检查是否超过限制
if count >= int64(limit) {
return false
}
// 增加计数(事务)
txnResp, _ := cli.Txn(ctx).
If(clientv3.Compare(clientv3.Value(key), "=", strconv.FormatInt(count, 10))).
Then(clientv3.OpPut(key, strconv.FormatInt(count+1, 10))).
Commit()
return txnResp.Succeeded
}唯一 ID 生成
go
func generateUniqueID(cli *clientv3.Client, prefix string) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
key := "/ids/" + prefix
// 原子性地获取并递增 ID
txnResp, err := cli.Txn(ctx).
Then(
clientv3.OpGet(key),
clientv3.OpPut(key, "next-value"),
).
Commit()
if err != nil {
return 0, err
}
// 解析当前 ID
getResp := (*clientv3.GetResponse)(txnResp.Responses[0].GetResponseRange())
var id int64 = 1
if len(getResp.Kvs) > 0 {
id, _ = strconv.ParseInt(string(getResp.Kvs[0].Value), 10, 64)
id++
}
// 更新 ID(使用事务保证原子性)
_, err = cli.Txn(ctx).
If(clientv3.Compare(clientv3.Value(key), "=", string(getResp.Kvs[0].Value))).
Then(clientv3.OpPut(key, strconv.FormatInt(id, 10))).
Commit()
return id, err
}配置更新(原子性)
go
func updateConfig(cli *clientv3.Client, key string, updater func(string) string) error {
return concurrency.NewSTM(cli, func(stm concurrency.STM) error {
current := stm.Get(key)
updated := updater(current)
stm.Put(key, updated)
return nil
})
}性能考虑
事务限制
- 单个事务最多支持 128 个操作
- 事务中的操作应尽量少,避免大事务
事务冲突
- 多个事务并发修改同一键时,只有一个能成功
- 设计时应减少冲突概率
STM 重试
STM 会自动重试失败的事务,但重试次数有限:
go
// 设置 STM 重试选项
resp, err := concurrency.NewSTM(cli, func(stm concurrency.STM) error {
stm.Put("/key", "value")
return nil
}, concurrency.WithAbortContext(context.Background()))最佳实践
事务设计
- 操作尽量少,避免大事务
- 减少热点键的冲突
- 使用合适的比较条件
锁使用
- 设置合理的 TTL
- 实现 KeepAlive 保持锁
- 及时释放锁
选举设计
- 监听 Leader 变更
- 实现 Follower 备份逻辑
- 处理网络抖动
错误处理
- 区分超时、冲突等错误
- 实现重试逻辑
- 处理 Session 失效
总结
本章学习了:
- 事务机制:Compare、Then、Else
- 分布式锁:基于事务和租约
- Leader 选举:基于选举锁
- STM:复杂事务逻辑
- 实用示例:限流器、唯一 ID、配置更新
这些机制是构建分布式系统的核心工具。在下一章中,我们将学习 etcd 的生产部署和运维实践。