go etcd分布式锁的方案代码

以下是一个基于 etcd 的分布式锁的完整 Go 代码实现,包含锁的获取、自动续租、释放锁等核心功能,并附有详细注释和注意事项。


完整代码实现

go

复制

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"go.etcd.io/etcd/client/v3"
)

// EtcdLock 基于 etcd 的分布式锁
type EtcdLock struct {
	client     *clientv3.Client // etcd 客户端
	leaseID    clientv3.LeaseID // 租约 ID
	key        string           // 锁的键名
	cancelFunc context.CancelFunc // 用于取消续租协程
}

// NewEtcdLock 创建分布式锁实例
func NewEtcdLock(client *clientv3.Client, key string) *EtcdLock {
	return &EtcdLock{
		client: client,
		key:    key,
	}
}

// Lock 尝试获取锁
// ttl: 锁的存活时间(秒)
func (l *EtcdLock) Lock(ttl int64) error {
	// 1. 创建租约
	lease := clientv3.NewLease(l.client)
	leaseResp, err := lease.Grant(context.Background(), ttl)
	if err != nil {
		return fmt.Errorf("创建租约失败: %v", err)
	}
	l.leaseID = leaseResp.ID

	// 2. 自动续租(防止任务未完成锁已过期)
	ctx, cancel := context.WithCancel(context.Background())
	l.cancelFunc = cancel
	keepAliveCh, err := lease.KeepAlive(ctx, l.leaseID)
	if err != nil {
		return fmt.Errorf("续租失败: %v", err)
	}
	go func() {
		for range keepAliveCh {
			// 续租成功,保持通道活跃
		}
	}()

	// 3. 事务操作(原子性获取锁)
	txn := l.client.Txn(context.Background())
	txn.If(clientv3.Compare(clientv3.CreateRevision(l.key), "=", 0).
		Then(clientv3.OpPut(l.key, "locked", clientv3.WithLease(l.leaseID))).
		Else(clientv3.OpGet(l.key))

	txnResp, err := txn.Commit()
	if err != nil {
		return fmt.Errorf("事务提交失败: %v", err)
	}

	// 4. 判断是否获取锁成功
	if !txnResp.Succeeded {
		return fmt.Errorf("锁已被占用")
	}

	return nil
}

// Unlock 释放锁
func (l *EtcdLock) Unlock() error {
	// 1. 取消续租协程
	if l.cancelFunc != nil {
		l.cancelFunc()
	}

	// 2. 释放租约
	if _, err := clientv3.NewLease(l.client).Revoke(context.Background(), l.leaseID); err != nil {
		return fmt.Errorf("释放租约失败: %v", err)
	}

	// 3. 删除键(可选,租约释放后 key 会自动删除)
	_, err := l.client.Delete(context.Background(), l.key)
	return err
}

// 使用示例
func main() {
	// 1. 创建 etcd 客户端
	client, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"localhost:2379"}, // etcd 地址
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		log.Fatal("连接 etcd 失败:", err)
	}
	defer client.Close()

	// 2. 创建锁实例
	lock := NewEtcdLock(client, "/my-lock")

	// 3. 尝试获取锁(TTL=10秒)
	if err := lock.Lock(10); err != nil {
		log.Fatal("获取锁失败:", err)
	}
	fmt.Println("成功获取锁")

	// 4. 执行业务逻辑
	go func() {
		time.Sleep(8 * time.Second) // 模拟业务处理(小于 TTL)
		fmt.Println("业务执行完成")
	}()

	// 5. 释放锁
	defer func() {
		if err := lock.Unlock(); err != nil {
			log.Fatal("释放锁失败:", err)
		}
		fmt.Println("锁已释放")
	}()

	// 保持程序运行
	select {}
}

核心机制说明

  1. 租约(Lease)

    • 通过 Grant 创建租约并设置 TTL(存活时间),锁在 TTL 到期后自动释放。

    • 使用 KeepAlive 自动续租,避免业务执行时间超过 TTL 导致锁提前释放。

  2. 事务(TXN)

    • 使用事务原子性地检查锁是否存在(CreateRevision == 0),不存在则写入锁键。

    • 事务操作确保多个客户端竞争时的原子性。

  3. 释放锁

    • 调用 Revoke 主动释放租约,关联的锁键会自动删除。

    • 删除锁键是可选操作(租约释放后自动删除),但显式删除更安全。


关键注意事项

  1. 租约时间

    • TTL 应大于业务执行的最长时间,否则锁可能提前失效。

    • 若业务可能超时,需在代码中增加心跳检测或动态调整 TTL。

  2. 网络问题

    • etcd 集群需要高可用部署,避免单点故障。

    • 客户端需处理网络重连(代码示例未包含,生产环境建议添加)。

  3. 错误处理

    • 获取锁失败时,可加入重试逻辑(如指数退避)。

    • 释放锁失败需记录日志并告警,避免死锁。

  4. 性能优化

    • 高频锁竞争场景下,etcd 的性能可能成为瓶颈(通常支持约 10k QPS)。


锁竞争测试

使用多个协程模拟并发竞争锁:

go

复制

func main() {
	client, _ := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
	defer client.Close()

	for i := 0; i < 5; i++ {
		go func(id int) {
			lock := NewEtcdLock(client, "/my-lock")
			if err := lock.Lock(5); err != nil {
				fmt.Printf("协程 %d 获取锁失败: %v\n", id, err)
				return
			}
			defer lock.Unlock()
			fmt.Printf("协程 %d 获得锁\n", id)
			time.Sleep(2 * time.Second)
		}(i)
	}

	select {}
}

输出

复制

协程 2 获得锁
协程 0 获取锁失败: 锁已被占用
协程 4 获取锁失败: 锁已被占用
协程 1 获取锁失败: 锁已被占用
协程 3 获取锁失败: 锁已被占用
...(协程2释放锁后,其他协程可再次竞争)

总结

  • 适用场景:需要强一致性的分布式系统(如 Kubernetes 控制器、分布式任务调度)。

  • 优势:基于 etcd 的 Raft 协议,保证锁的强一致性和高可用性。

  • 替代方案:若系统已使用 Redis/ZooKeeper,可直接复用现有基础设施。

你可能感兴趣的:(golang,etcd,分布式)