etcd实现分布式锁

前言

分布式锁要解决两个问题:

1、锁竞争

2、死锁

以redis为例,redis提供了setnx来保证原子写入,只有一个客户端能写入成功,也就能成功获得锁。同时为了防止客户端异常导致锁没有及时释放,可以对这个锁设置过期s时间,命令如下:

SET lock_name my_random_value NX PX 30000

除了锁自动过期以外,还需要能手动释放锁,命令如下:

del lock_name

etcd的实现方式

实现流程如下:

  1. 写入key

    客户端连接 etcd,以 /etcd/lock 为前缀创建全局唯一的 key,并设置租约。比如第一个客户端对应的 key="/etcd/lock/UUID1",第二个为 key="/etcd/lock/UUID2"。

  2. 客户端判断是否获得锁
    (1). 通过WithFirstCreate()选项获取/etcd/lock 目录下最早创建的那个key,即CreateVersion最小的那个key;

    (2). 写入key时会返回一个Header.Revirsion,表示的是本次更新后的ModVersion,如果是第一次创建,则和它的CreateVersion是一致的,可以直接拿这个值和最小的那个CreateVersion做比较,如果相等则表示自己是最早创建的,因此就可以获取锁;

    (3). 如果这个key之前已经创建了,则直接获取获取它的CreateVersion,再和最小的那个CreateVersion作比较,如果相等也可以获取锁。

  3. 执行业务

    获得锁后,操作共享资源,执行业务代码。

  4. 解锁

    删除这个key,会触发其他客户端的抢锁。

使用示例

官方包里(github.com/coreos/etcd/clientv3/concurrency)已经实现了上述流程,我们只需要做下简单的调用就可以实现分布式锁了。

package main

import (
	"context"
	"flag"
	"github.com/coreos/etcd/clientv3"
	"go.etcd.io/etcd/clientv3/concurrency"
	"log"
	"strings"
	"time"
)

var (
	addr     = flag.String("addr", "http://127.0.0.1:2379", "etcd addresss")
	lockName = flag.String("name", "mylock", "lock name")
)

func main() {
	flag.Parse()
	endpoints := strings.Split(*addr, ",")
	cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()
	userLock(cli) //测试锁
}

func userLock(cli1 *clientv3.Client) {
	//为锁生成session
	session, err := concurrency.NewSession(cli1)
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()
	m := concurrency.NewMutex(session, *lockName)
	ctx := context.TODO()
	//  请求锁
	log.Println("acquiring lock")
	m.Lock(ctx)
	log.Println("acquired lock")

	time.Sleep(time.Duration(10 * time.Second))
	m.Unlock(ctx) //释放锁
	log.Println("released lock")
}

源码解析

加锁

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

	m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
	// 写入key之前先判断这个key是否存在
	cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
	// 写入空值,并加上租约
	put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
	// 如果写入这个key之前这个key已经存在,则直接获取key的信息
	get := v3.OpGet(m.myKey)
	// 无论key是否存在,都要获取一次前缀里CreateVersion最小的那个key的信息
	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
	}
	if resp.Succeeded {
		// 如果key不存在,则通过可以通过Header.Revision来获取它的ModVersion,因为是首次创建,因此ModVersion=CreateVersion
		m.myRev = resp.Header.Revision
	}else {
		// 否则直接获取它的CreateVersion
		m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
	}
	// 获取前缀目录中最小的那个CreateVersion
	ownerKey := resp.Responses[1].GetResponseRange().Kvs
	// 如果自己的CreateVersion等于这个这小的CreateVersion,则获取锁返回
	if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
		m.hdr = resp.Header
		return nil
	}

	// 走到这里说明没有获取锁,那么它必须要等待锁的释放,但这里它不是监听那个最小的CreateVersion的key的删除事件
	hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
	if werr != nil {
		m.Unlock(client.Ctx())
	} else {
		m.hdr = hdr
	}
	return werr
}
  1. 写入key时先判断这个key是否存在;
  2. 如果存在,则直接获取这个key的CreateVersion
  3. 如果不存在,则写入key,并通过PutResponse.Header.Version获取ModVersion,因为是首次创建,因此它的CreateVersion等于ModVersion
  4. 无论如何要获取一次前缀目录下CreateVersion最小的那个key的信息;
  5. 将上述步骤组合到一起做一次事务操作;
  6. 判断是否获取到锁,如果自己的CreateVersion等于这个最小的CreateVersion,则获取到锁,并退出;
  7. 如果不相等则没有获取到锁,那它必须要去监听比它的CreateVersion小的key的删除事件,如果获监听到事件则可以获取到锁。

监听key的删除事件

func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
	// 先从前缀目录中找出比当前CreateVersion小的那些key,然后从中再获取CreateVersion最大(离当前key最近)的的那个key
	getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
	for {
		resp, err := client.Get(ctx, pfx, getOpts...)
		if err != nil {
			return nil, err
		}
		// 如果没有key则返回
		if len(resp.Kvs) == 0 {
			return resp.Header, nil
		}
		lastKey := string(resp.Kvs[0].Key)
		// 获取到这个key并监听它的删除事件
		if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
			return nil, err
		}
	}
}
  1. 先从前缀目录中找出比当前CreateVersion小的那些key,然后从中再获取CreateVersion最大(离当前key最近)的的那个key;
  2. 如果找不到则获取锁,直接返回;
  3. 如果找到,则监听它的删除事件;

解锁

func (m *Mutex) Unlock(ctx context.Context) error {
	client := m.s.Client()
	if _, err := client.Delete(ctx, m.myKey); err != nil {
		return err
	}
	m.myKey = "\x00"
	m.myRev = -1
	return nil
}

直接删除这个key就可以了,至此整个流程介绍完毕。

posted @ 2021-10-02 19:16  独揽风月  阅读(2940)  评论(0编辑  收藏  举报