ETCD 分布式鎖

概述

在傳統(tǒng)單體應(yīng)用單機(jī)部署的情況下,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或Synchronized)進(jìn)行互斥控制。在單機(jī)環(huán)境中,Java中提供了很多并發(fā)處理相關(guān)的API。但是,隨著業(yè)務(wù)發(fā)展的需要,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程、多進(jìn)程并且分布在不同機(jī)器上,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個(gè)問題就需要一種跨JVM的互斥機(jī)制來控制共享資源的訪問,這就是分布式鎖要解決的問題!

鎖是在執(zhí)行多線程時(shí)用于強(qiáng)行限制資源訪問的同步機(jī)制,在單機(jī)系統(tǒng)上,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或Synchronized)進(jìn)行互斥控制。而在分布式系統(tǒng)場(chǎng)景下,實(shí)例會(huì)運(yùn)行在多臺(tái)機(jī)器上,為了使多進(jìn)程(多實(shí)例上)對(duì)共享資源的讀寫同步,保證數(shù)據(jù)的最終一致性,引入了分布式鎖。

分布式鎖應(yīng)具備以下特點(diǎn):

  • 互斥性:在任意時(shí)刻,只有一個(gè)客戶端(進(jìn)程)能持有鎖
  • 安全性:避免死鎖情況,當(dāng)一個(gè)客戶端在持有鎖期間內(nèi),由于意外崩潰而導(dǎo)致鎖未能主動(dòng)解鎖,其持有的鎖也能夠被正確釋放,并保證后續(xù)其它客戶端也能加鎖
  • 可用性:分布式鎖需要有一定的高可用能力,當(dāng)提供鎖的服務(wù)節(jié)點(diǎn)故障(宕機(jī))時(shí)不影響服務(wù)運(yùn)行,避免單點(diǎn)風(fēng)險(xiǎn),如Redis的集群模式、哨兵模式,ETCD/zookeeper的集群選主能力等保證HA,保證自身持有的數(shù)據(jù)與故障節(jié)點(diǎn)一致。
  • 對(duì)稱性:對(duì)同一個(gè)鎖,加鎖和解鎖必須是同一個(gè)進(jìn)程,即不能把其他進(jìn)程持有的鎖給釋放了,這又稱為鎖的可重入性。

分布式鎖常見實(shí)現(xiàn)方式:

  1. 通過數(shù)據(jù)庫(kù)方式實(shí)現(xiàn):采用樂觀鎖、悲觀鎖或者基于主鍵唯一約束實(shí)現(xiàn)
  2. 基于分布式緩存實(shí)現(xiàn)的鎖服務(wù): Redis 和基于 Redis 的 RedLock(Redisson提供了參考實(shí)現(xiàn))
  3. 基于分布式一致性算法實(shí)現(xiàn)的鎖服務(wù):ZooKeeper、Chubby(google閉源實(shí)現(xiàn))和 Etcd

網(wǎng)上常見的是基于Redis和ZooKeeper的實(shí)現(xiàn),基于數(shù)據(jù)庫(kù)的因?yàn)閷?shí)現(xiàn)繁瑣且性能較差,不想維護(hù)第三方中間件的可以考慮。本文主要描述基于 ETCD 的實(shí)現(xiàn),etcd3 的client也給出了新的 api,使用上更為簡(jiǎn)單

基于 Redis 的實(shí)現(xiàn)

既然是鎖,核心操作無外乎加鎖、解鎖。

Redis的加鎖操作:

SET lock_name my_random_value NX PX 30000

Bash

Copy

  • lock_name,鎖的名稱,對(duì)于 Redis 而言,lock_name 就是 Key-Value 中的 Key,具有唯一性。
  • random_value,由客戶端生成的一個(gè)隨機(jī)字符串,它要保證在足夠長(zhǎng)的一段時(shí)間內(nèi),且在所有客戶端的所有獲取鎖的請(qǐng)求中都是唯一的,用于唯一標(biāo)識(shí)鎖的持有者。
  • NX 只有當(dāng) lock_name(key) 不存在的時(shí)候才能 SET 成功,從而保證只有一個(gè)客戶端能獲得鎖,而其它客戶端在鎖被釋放之前都無法獲得鎖。
  • PX 30000 表示這個(gè)鎖節(jié)點(diǎn)有一個(gè) 30 秒的自動(dòng)過期時(shí)間(目的是為了防止持有鎖的客戶端故障后,無法主動(dòng)釋放鎖而導(dǎo)致死鎖,因此要求鎖的持有者必須在過期時(shí)間之內(nèi)執(zhí)行完相關(guān)操作并釋放鎖)。

Redis的解鎖操作:

del lock_name

Bash

Copy

  • 在加鎖時(shí)為鎖設(shè)置過期時(shí)間,當(dāng)過期時(shí)間到達(dá),Redis 會(huì)自動(dòng)刪除對(duì)應(yīng)的 Key-Value,從而避免死鎖。注意,這個(gè)過期時(shí)間需要結(jié)合具體業(yè)務(wù)綜合評(píng)估設(shè)置,以保證鎖的持有者能夠在過期時(shí)間之內(nèi)執(zhí)行完相關(guān)操作并釋放鎖。
  • 正常執(zhí)行完畢,未到達(dá)鎖過期時(shí)間,通過del lock_name主動(dòng)釋放鎖。

基于 ETCD的分布式鎖

機(jī)制

etcd 支持以下功能,正是依賴這些功能來實(shí)現(xiàn)分布式鎖的:

  • Lease 機(jī)制:即租約機(jī)制(TTL,Time To Live),Etcd 可以為存儲(chǔ)的 KV 對(duì)設(shè)置租約,當(dāng)租約到期,KV 將失效刪除;同時(shí)也支持續(xù)約,即 KeepAlive。
  • Revision 機(jī)制:每個(gè) key 帶有一個(gè) Revision 屬性值,etcd 每進(jìn)行一次事務(wù)對(duì)應(yīng)的全局 Revision 值都會(huì)加一,因此每個(gè) key 對(duì)應(yīng)的 Revision 屬性值都是全局唯一的。通過比較 Revision 的大小就可以知道進(jìn)行寫操作的順序。
  • 在實(shí)現(xiàn)分布式鎖時(shí),多個(gè)程序同時(shí)搶鎖,根據(jù) Revision 值大小依次獲得鎖,可以避免 “羊群效應(yīng)” (也稱 “驚群效應(yīng)”),實(shí)現(xiàn)公平鎖。
  • Prefix 機(jī)制:即前綴機(jī)制,也稱目錄機(jī)制。可以根據(jù)前綴(目錄)獲取該目錄下所有的 key 及對(duì)應(yīng)的屬性(包括 key, value 以及 revision 等)。
  • Watch 機(jī)制:即監(jiān)聽機(jī)制,Watch 機(jī)制支持 Watch 某個(gè)固定的 key,也支持 Watch 一個(gè)目錄(前綴機(jī)制),當(dāng)被 Watch 的 key 或目錄發(fā)生變化,客戶端將收到通知。

過程

實(shí)現(xiàn)過程:

  • 步驟 1: 準(zhǔn)備
    客戶端連接 Etcd,以 /lock/mylock 為前綴創(chuàng)建全局唯一的 key,假設(shè)第一個(gè)客戶端對(duì)應(yīng)的 key=”/lock/mylock/UUID1″,第二個(gè)為 key=”/lock/mylock/UUID2″;客戶端分別為自己的 key 創(chuàng)建租約 – Lease,租約的長(zhǎng)度根據(jù)業(yè)務(wù)耗時(shí)確定,假設(shè)為 15s;
  • 步驟 2: 創(chuàng)建定時(shí)任務(wù)作為租約的“心跳”
    當(dāng)一個(gè)客戶端持有鎖期間,其它客戶端只能等待,為了避免等待期間租約失效,客戶端需創(chuàng)建一個(gè)定時(shí)任務(wù)作為“心跳”進(jìn)行續(xù)約。此外,如果持有鎖期間客戶端崩潰,心跳停止,key 將因租約到期而被刪除,從而鎖釋放,避免死鎖。
  • 步驟 3: 客戶端將自己全局唯一的 key 寫入 Etcd
    進(jìn)行 put 操作,將步驟 1 中創(chuàng)建的 key 綁定租約寫入 Etcd,根據(jù) Etcd 的 Revision 機(jī)制,假設(shè)兩個(gè)客戶端 put 操作返回的 Revision 分別為 1、2,客戶端需記錄 Revision 用以接下來判斷自己是否獲得鎖。
  • 步驟 4: 客戶端判斷是否獲得鎖
    客戶端以前綴 /lock/mylock 讀取 keyValue 列表(keyValue 中帶有 key 對(duì)應(yīng)的 Revision),判斷自己 key 的 Revision 是否為當(dāng)前列表中最小的,如果是則認(rèn)為獲得鎖;否則監(jiān)聽列表中前一個(gè) Revision 比自己小的 key 的刪除事件,一旦監(jiān)聽到刪除事件或者因租約失效而刪除的事件,則自己獲得鎖。
  • 步驟 5: 執(zhí)行業(yè)務(wù)
    獲得鎖后,操作共享資源,執(zhí)行業(yè)務(wù)代碼。
  • 步驟 6: 釋放鎖
    完成業(yè)務(wù)流程后,刪除對(duì)應(yīng)的key釋放鎖。

實(shí)現(xiàn)

自帶的 etcdctl 可以模擬鎖的使用:

// 第一個(gè)終端
$ ./etcdctl lock mutex1
mutex1/326963a02758b52d

// 第二終端
$ ./etcdctl lock mutex1

// 當(dāng)?shù)谝粋€(gè)終端結(jié)束了,第二個(gè)終端會(huì)顯示
mutex1/326963a02758b531

在etcd的clientv3包中,實(shí)現(xiàn)了分布式鎖。使用起來和mutex是類似的,為了了解其中的工作機(jī)制,這里簡(jiǎn)要的做一下總結(jié)。

etcd分布式鎖的實(shí)現(xiàn)在go.etcd.io/etcd/clientv3/concurrency包中,主要提供了以下幾個(gè)方法:

* func NewMutex(s *Session, pfx string) *Mutex, 用來新建一個(gè)mutex
* func (m *Mutex) Lock(ctx context.Context) error,它會(huì)阻塞直到拿到了鎖,并且支持通過context來取消獲取鎖。
* func (m *Mutex) Unlock(ctx context.Context) error,解鎖

因此在使用etcd提供的分布式鎖式非常簡(jiǎn)單,通常就是實(shí)例化一個(gè)mutex,然后嘗試搶占鎖,之后進(jìn)行業(yè)務(wù)處理,最后解鎖即可。

demo:

package main

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

func main() {  
    c := make(chan os.Signal)
    signal.Notify(c)

    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    lockKey := "/lock"

    go func () {
        session, err := concurrency.NewSession(cli)
        if err != nil {
            log.Fatal(err)
        }
        m := concurrency.NewMutex(session, lockKey)
        if err := m.Lock(context.TODO()); err != nil {
            log.Fatal("go1 get mutex failed " + err.Error())
        }
        fmt.Printf("go1 get mutex sucess\n")
        fmt.Println(m)
        time.Sleep(time.Duration(10) * time.Second)
        m.Unlock(context.TODO())
        fmt.Printf("go1 release lock\n")
    }()

    go func() {
        time.Sleep(time.Duration(2) * time.Second)
        session, err := concurrency.NewSession(cli)
        if err != nil {
            log.Fatal(err)
        }
        m := concurrency.NewMutex(session, lockKey)
        if err := m.Lock(context.TODO()); err != nil {
            log.Fatal("go2 get mutex failed " + err.Error())
        }
        fmt.Printf("go2 get mutex sucess\n")
        fmt.Println(m)
        time.Sleep(time.Duration(2) * time.Second)
        m.Unlock(context.TODO())
        fmt.Printf("go2 release lock\n")
    }()

    <-c
}

原理

Lock()函數(shù)的實(shí)現(xiàn)很簡(jiǎn)單:

// Lock locks the mutex with a cancelable context. If the context is canceled
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
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())
    cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
    // put self in lock waiters via myKey; oldest waiter holds lock
    put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
    // reuse key in case this session already holds the lock
    get := v3.OpGet(m.myKey)
    // fetch current holder to complete uncontended path with only one RPC
    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
    }
    m.myRev = resp.Header.Revision
    if !resp.Succeeded {
        m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
    }
    // if no key on prefix / the minimum rev is key, already hold the lock
    ownerKey := resp.Responses[1].GetResponseRange().Kvs
    if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
        m.hdr = resp.Header
        return nil
    }

    // wait for deletion revisions prior to myKey
    hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
    // release lock key if wait failed
    if werr != nil {
        m.Unlock(client.Ctx())
    } else {
        m.hdr = hdr
    }
    return werr
}

首先通過一個(gè)事務(wù)來嘗試加鎖,這個(gè)事務(wù)主要包含了4個(gè)操作: cmp、put、get、getOwner。需要注意的是,key是由pfx和Lease()組成的。

  • cmp: 比較加鎖的key的修訂版本是否是0。如果是0就代表這個(gè)鎖不存在。
  • put: 向加鎖的key中存儲(chǔ)一個(gè)空值,這個(gè)操作就是一個(gè)加鎖的操作,但是這把鎖是有超時(shí)時(shí)間的,超時(shí)的時(shí)間是session的默認(rèn)時(shí)長(zhǎng)。超時(shí)是為了防止鎖沒有被正常釋放導(dǎo)致死鎖。
  • get: get就是通過key來查詢
  • getOwner: 注意這里是用m.pfx來查詢的,并且?guī)Я瞬樵儏?shù)WithFirstCreate()。使用pfx來查詢是因?yàn)槠渌膕ession也會(huì)用同樣的pfx來嘗試加鎖,并且因?yàn)槊總€(gè)LeaseID都不同,所以第一次肯定會(huì)put成功。但是只有最早使用這個(gè)pfx的session才是持有鎖的,所以這個(gè)getOwner的含義就是這樣的。

接下來才是通過判斷來檢查是否持有鎖

m.myRev = resp.Header.Revision
if !resp.Succeeded {
    m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
// if no key on prefix / the minimum rev is key, already hold the lock
ownerKey := resp.Responses[1].GetResponseRange().Kvs
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
    m.hdr = resp.Header
    return nil
}

m.myRev是當(dāng)前的版本號(hào),resp.Succeeded是cmp為true時(shí)值為true,否則是false。這里的判斷表明當(dāng)同一個(gè)session非第一次嘗試加鎖,當(dāng)前的版本號(hào)應(yīng)該取這個(gè)key的最新的版本號(hào)。

下面是取得鎖的持有者的key。如果當(dāng)前沒有人持有這把鎖,那么默認(rèn)當(dāng)前會(huì)話獲得了鎖。或者鎖持有者的版本號(hào)和當(dāng)前的版本號(hào)一致, 那么當(dāng)前的會(huì)話就是鎖的持有者。

// wait for deletion revisions prior to myKey
hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
// release lock key if wait failed
if werr != nil {
    m.Unlock(client.Ctx())
} else {
    m.hdr = hdr
}

上面這段代碼就很好理解了,因?yàn)樽叩竭@里說明沒有獲取到鎖,那么這里等待鎖的刪除。

waitDeletes方法的實(shí)現(xiàn)也很簡(jiǎn)單,但是需要注意的是,這里的getOpts只會(huì)獲取比當(dāng)前會(huì)話版本號(hào)更低的key,然后去監(jiān)控最新的key的刪除。等這個(gè)key刪除了,自己也就拿到鎖了。

這種分布式鎖的實(shí)現(xiàn)和我一開始的預(yù)想是不同的。它不存在鎖的競(jìng)爭(zhēng),不存在重復(fù)的嘗試加鎖的操作。而是通過使用統(tǒng)一的前綴pfx來put,然后根據(jù)各自的版本號(hào)來排隊(duì)獲取鎖。效率非常的高。避免了驚群效應(yīng)


image.png

如圖所示,共有4個(gè)session來加鎖,那么根據(jù)revision來排隊(duì),獲取鎖的順序?yàn)閟ession2 -> session3 -> session1 -> session4。

這里面需要注意一個(gè)驚群效應(yīng),每一個(gè)client在鎖住/lock這個(gè)path的時(shí)候,實(shí)際都已經(jīng)插入了自己的數(shù)據(jù),類似/lock/LEASE_ID,并且返回了各自的index(就是raft算法里面的日志索引),而只有最小的才算是拿到了鎖,其他的client需要watch等待。例如client1拿到了鎖,client2和client3在等待,而client2拿到的index比client3的更小,那么對(duì)于client1刪除鎖之后,client3其實(shí)并不關(guān)心,并不需要去watch。所以綜上,等待的節(jié)點(diǎn)只需要watch比自己index小并且差距最小的節(jié)點(diǎn)刪除事件即可。

基于 ETCD的選主

機(jī)制

etcd有多種使用場(chǎng)景,Master選舉是其中一種。說起Master選舉,過去常常使用zookeeper,通過創(chuàng)建EPHEMERAL_SEQUENTIAL節(jié)點(diǎn)(臨時(shí)有序節(jié)點(diǎn)),我們選擇序號(hào)最小的節(jié)點(diǎn)作為Master,邏輯直觀,實(shí)現(xiàn)簡(jiǎn)單是其優(yōu)勢(shì),但是要實(shí)現(xiàn)一個(gè)高健壯性的選舉并不簡(jiǎn)單,同時(shí)zookeeper繁雜的擴(kuò)縮容機(jī)制也是沉重的負(fù)擔(dān)。

master 選舉根本上也是搶鎖,與zookeeper直觀選舉邏輯相比,etcd的選舉則需要在我們熟悉它的一系列基本概念后,調(diào)動(dòng)我們充分的想象力:

  • 1、MVCC,key存在版本屬性,沒被創(chuàng)建時(shí)版本號(hào)為0;
  • 2、CAS操作,結(jié)合MVCC,可以實(shí)現(xiàn)競(jìng)選邏輯,if(version == 0) set(key,value),通過原子操作,確保只有一臺(tái)機(jī)器能set成功;
  • 3、Lease租約,可以對(duì)key綁定一個(gè)租約,租約到期時(shí)沒預(yù)約,這個(gè)key就會(huì)被回收;
  • 4、Watch監(jiān)聽,監(jiān)聽key的變化事件,如果key被刪除,則重新發(fā)起競(jìng)選。

至此,etcd選舉的邏輯大體清晰了,但這一系列操作與zookeeper相比復(fù)雜很多,有沒有已經(jīng)封裝好的庫(kù)可以直接拿來用?etcd clientv3 concurrency中有對(duì)選舉及分布式鎖的封裝。后面進(jìn)一步發(fā)現(xiàn),etcdctl v3里已經(jīng)有master選舉的實(shí)現(xiàn)了,下面針對(duì)這部分代碼進(jìn)行簡(jiǎn)單注釋,在最后參考這部分代碼實(shí)現(xiàn)自己的選舉邏輯。

實(shí)現(xiàn)

官方示例:https://github.com/etcd-io/etcd/blob/master/clientv3/concurrency/example_election_test.go

如crontab 示例:

package main

import (
    "context"
    "fmt"
    "go.etcd.io/etcd/clientv3"
    "go.etcd.io/etcd/clientv3/concurrency"
    "log"
    "time"
)

const prefix = "/election-demo"
const prop = "local"

func main() {
    endpoints := []string{"szth-cce-devops00.szth.baidu.com:8379"}
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    campaign(cli, prefix, prop)

}

func campaign(c *clientv3.Client, election string, prop string) {
    for {
        // 租約到期時(shí)間:5s
        s, err := concurrency.NewSession(c, concurrency.WithTTL(5))
        if err != nil {
            fmt.Println(err)
            continue
        }
        e := concurrency.NewElection(s, election)
        ctx := context.TODO()

        log.Println("開始競(jìng)選")

        err = e.Campaign(ctx, prop)
        if err != nil {
            log.Println("競(jìng)選 leader失敗,繼續(xù)")
            switch {
            case err == context.Canceled:
                return
            default:
                continue
            }
        }

        log.Println("獲得leader")
        if err := doCrontab(); err != nil {
            log.Println("調(diào)用主方法失敗,辭去leader,重新競(jìng)選")
            _ = e.Resign(ctx)
            continue
        }
        return
    }
}

func doCrontab() error {
    for {
        fmt.Println("doCrontab")
        time.Sleep(time.Second * 4)
        //return fmt.Errorf("sss")
    }
}

原理

/*
 * 發(fā)起競(jìng)選
 * 未當(dāng)選leader前,會(huì)一直阻塞在Campaign調(diào)用
 * 當(dāng)選leader后,等待SIGINT、SIGTERM或session過期而退出
 * https://github.com/etcd-io/etcd/blob/master/etcdctl/ctlv3/command/elect_command.go
 */

func campaign(c *clientv3.Client, election string, prop string) error {
        //NewSession函數(shù)中創(chuàng)建了一個(gè)lease,默認(rèn)是60s TTL,并會(huì)調(diào)用KeepAlive,永久為這個(gè)lease自動(dòng)續(xù)約(2/3生命周期的時(shí)候執(zhí)行續(xù)約操作)
    s, err := concurrency.NewSession(c)
    if err != nil {
        return err
    }
    e := concurrency.NewElection(s, election)
    ctx, cancel := context.WithCancel(context.TODO())

    donec := make(chan struct{})
    sigc := make(chan os.Signal, 1)
    signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-sigc
        cancel()
        close(donec)
    }()

    //競(jìng)選邏輯,將展開分析
    if err = e.Campaign(ctx, prop); err != nil {
        return err
    }

    // print key since elected
    resp, err := c.Get(ctx, e.Key())
    if err != nil {
        return err
    }
    display.Get(*resp)

    select {
    case <-donec:
    case <-s.Done():
        return errors.New("elect: session expired")
    }

    return e.Resign(context.TODO())
}

/*
 * 類似于zookeeper的臨時(shí)有序節(jié)點(diǎn),etcd的選舉也是在相應(yīng)的prefix path下面創(chuàng)建key,該key綁定了lease并根據(jù)lease id進(jìn)行命名,
 * key創(chuàng)建后就有revision號(hào),這樣使得在prefix path下的key也都是按revision有序
 * https://github.com/etcd-io/etcd/blob/master/clientv3/concurrency/election.go
 */

func (e *Election) Campaign(ctx context.Context, val string) error {
    s := e.session
    client := e.session.Client()

    //真正創(chuàng)建的key名為:prefix + lease id
    k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
    //Txn:transaction,依靠Txn進(jìn)行創(chuàng)建key的CAS操作,當(dāng)key不存在時(shí)才會(huì)成功創(chuàng)建
    txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
    txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
    txn = txn.Else(v3.OpGet(k))
    resp, err := txn.Commit()
    if err != nil {
        return err
    }
    e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
    //如果key已存在,則創(chuàng)建失敗;
        //當(dāng)key的value與當(dāng)前value不等時(shí),如果自己為leader,則不用重新執(zhí)行選舉直接設(shè)置value;
        //否則報(bào)錯(cuò)。
    if !resp.Succeeded {
        kv := resp.Responses[0].GetResponseRange().Kvs[0]
        e.leaderRev = kv.CreateRevision
        if string(kv.Value) != val {
            if err = e.Proclaim(ctx, val); err != nil {
                e.Resign(ctx)
                return err
            }
        }
    }

    //一直阻塞,直到確認(rèn)自己的create revision為當(dāng)前path中最小,從而確認(rèn)自己當(dāng)選為leader
    _, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
    if err != nil {
        // clean up in case of context cancel
        select {
        case <-ctx.Done():
            e.Resign(client.Ctx())
        default:
            e.leaderSession = nil
        }
        return err
    }
    e.hdr = resp.Header

    return nil
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容