概述
在傳統(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)方式:
- 通過數(shù)據(jù)庫(kù)方式實(shí)現(xiàn):采用樂觀鎖、悲觀鎖或者基于主鍵唯一約束實(shí)現(xiàn)
- 基于分布式緩存實(shí)現(xiàn)的鎖服務(wù): Redis 和基于 Redis 的 RedLock(Redisson提供了參考實(shí)現(xiàn))
- 基于分布式一致性算法實(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)
如圖所示,共有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
}