go etcd分布式锁的实现和使用

etcd的分布式锁

在分布式系统中,通常需要使用分布式锁解决跨网络的多线程的共享资源竞争问题。
使用etcd实现分布式锁可以确保确保了锁的安全性、互斥性,同时可以通过Lease确保持有锁的client的活性,当client发送宕机或者网络分区的时候,不会出现死锁。并且基于Raft保证了数据的强一致性,不会因为etcd实例宕机出现锁数据丢失的问题。
这里介绍一下clientv3提供的concurrency包里的分布式锁的实现原理和使用示例。

concurrency分布式锁源码介绍

首先concurrency.NewSession方法创建Session对象,本质是创建了一份租约,可以通过续租保证锁的安全性,当持有锁的client宕机不能主动释放锁的时候,锁会因为租约到期而自动释放,避免产生死锁。

type Session struct {
	client *v3.Client
	opts   *sessionOptions
	id     v3.LeaseID

	cancel context.CancelFunc
	donec  <-chan struct{}
}

使用Session对象通过concurrency.NewMutex 创建了一个Mutex对象,这个对象是分布式锁的核心逻辑,咱们通过源码分析下

type Mutex struct {
	s *Session

	pfx   string //前缀,实际上就是加锁的key + "/"
	myKey string //在etcd中真实的key,就是pfx/LeaseID的格式
	myRev int64 //创建key的Revision
	hdr   *pb.ResponseHeader
}


func (m *Mutex) Lock(ctx context.Context) error {
	s := m.s
	client := m.s.Client()

    //根据要加锁资源的key和租约ID为这个锁创建一个myKey
	m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
	//使用事务,如果myKey的CreateRevision=0,则代表key不存在,就创建一个,如果存在就获取myKey的信息和同样prefix中Revision最小的key,
	//同样prefix代表锁的是同一个资源。
	cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
	put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
	get := v3.OpGet(m.myKey)
	getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
	resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit()
	if err != nil {
		return err
	}

	//如果myKey已经存在则代表是同一个租约再次请求锁,实现了可重入锁
	m.myRev = resp.Header.Revision
	if !resp.Succeeded {
		m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
	}


    //如果myKey的Revision是同样prefix的key中最小的,就直接获取到锁。
	ownerKey := resp.Responses[1].GetResponseRange().Kvs
	if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
		m.hdr = resp.Header
		return nil
	}

	//监听等待比自己Revision小的key释放锁
	hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
	//如果等待失败,释放锁键
	if werr != nil {
		m.Unlock(client.Ctx())
	} else {
		m.hdr = hdr
	}
	return werr
}

分布式锁的demo

下面是ectd分布式锁的应用,通过两个goroutine模拟两个client同时请求锁,可以看到后面的请求是阻塞的,直到持有锁的client释放。


package main

import (
	"log"
	"time"
	"github.com/coreos/etcd/clientv3"
	"github.com/coreos/etcd/clientv3/concurrency"
	"context"
	"fmt"
)

func main(){
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"127.0.0.1:2379"},
		DialTimeout: time.Second * 5,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()
	s1, err := concurrency.NewSession(cli, concurrency.WithTTL(5))
	if err != nil {
		log.Fatal(err)
	}
	defer s1.Close()

	//会话1上锁成功,然后开启goroutine去新建一个会话去上锁,5秒钟后会话1解锁。
	m1 := concurrency.NewMutex(s1, "mylock")
	if err := m1.Lock(context.TODO()); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("session1 上锁成功。 time:%d \n",time.Now().Unix())

	g2 := make(chan struct{})
	go func() {
		defer close(g2)
		s2, err := concurrency.NewSession(cli, concurrency.WithTTL(5))
		if err != nil {
			log.Fatal(err)
		}
		defer s2.Close()
		m2 := concurrency.NewMutex(s2, "mylock")
		if err := m2.Lock(context.TODO()); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("session2 上锁成功。 time:%d \n",time.Now().Unix())
		if err := m2.Unlock(context.TODO()); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("session2 解锁。 time:%d \n",time.Now().Unix())

	}()

	time.Sleep(5 * time.Second)
	if err := m1.Unlock(context.TODO()); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("session1 解锁。 time:%d \n",time.Now().Unix())
	<- g2
}

/**
结果输出:
session1 上锁成功。 time:1641454818 
session1 解锁。 time:1641454823 
session2 上锁成功。 time:1641454823 
session2 解锁。 time:1641454823 
*/
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang使用分布式锁可以通过使用第三方库来实现,比如 etcd、consul 等。以下使用 etcd 举例: 1. 安装 etcd: ``` $ go get go.etcd.io/etcd ``` 2. 引入 etcdv3 的客户端: ```go import ( "context" "go.etcd.io/etcd/clientv3" ) ``` 3. 创建 etcdv3 的客户端: ```go func NewClient() (*clientv3.Client, error) { config := clientv3.Config{ Endpoints: []string{"http://localhost:2379"}, // etcd 地址 } return clientv3.New(config) } ``` 4. 获取分布式锁: ```go func GetLock(client *clientv3.Client, key string) (*clientv3.LeaseKeepAliveResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 1. 创建租约 leaseResp, err := client.Grant(ctx, 10) if err != nil { return nil, err } // 2. 创建事务 txn := client.Txn(ctx) lockKey := fmt.Sprintf("/lock/%s", key) // 3. 事务操作 txn.If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)). Then(clientv3.OpPut(lockKey, "", clientv3.WithLease(leaseResp.ID))). Else(clientv3.OpGet(lockKey)) // 4. 提交事务 txnResp, err := txn.Commit() if err != nil { return nil, err } // 5. 获取锁状态 if !txnResp.Succeeded { return nil, errors.New("get lock failed") } // 6. 续约 keepAliveResp, err := client.KeepAlive(ctx, leaseResp.ID) if err != nil { return nil, err } return keepAliveResp, nil } ``` 5. 释放分布式锁: ```go func ReleaseLock(client *clientv3.Client, key string, keepAliveResp *clientv3.LeaseKeepAliveResponse) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() lockKey := fmt.Sprintf("/lock/%s", key) // 关闭续约 if _, err := client.Revoke(ctx, keepAliveResp.ID); err != nil { return err } // 删除锁 _, err := client.Delete(ctx, lockKey) if err != nil { return err } return nil } ``` 使用时可以按照以下方式调用: ```go func main() { client, err := NewClient() if err != nil { panic(err) } defer client.Close() // 获取锁 keepAliveResp, err := GetLock(client, "mylock") if err != nil { panic(err) } // 执行业务逻辑 // ... // 释放锁 if err := ReleaseLock(client, "mylock", keepAliveResp); err != nil { panic(err) } } ``` 以上代码仅供参考,具体实现可以根据实际情况进行调整和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值