微服務架構上篇:基於 etcd 實現分佈式鎖

  1. 微服務架構上篇 ==========

  2. 基於 etcd 實現分佈式鎖


1. 前言

通過 etcd 實現分佈式鎖,同樣需要滿足一致性互斥性可靠性等要求。etcd 中的事務 txnlease 租約以及 watch 監聽特性,能夠使得基於 etcd 實現上述要求的分佈式鎖

2. 思路分析

2.1 正常獲取鎖 (etcd 的事務 IF-Then-Else)

通過 etcd 的事務特性可以幫助我們實現一致性和互斥性。etcd 的事務特性,使用的 IF-Then-Else 語句,IF 語言判斷 etcd 服務端是否存在指定的 key,即該 key 創建版本號 create_revision 是否爲 0 來檢查 key 是否已存在,因爲該 key 已存在的話,它的 create_revision 版本號就不是 0。滿足 IF 條件的情況下則使用 then 執行 put 操作,否則 else 語句返回搶鎖失敗的結果。當然,除了使用 key 是否創建成功作爲 IF 的判斷依據,還可以創建前綴相同的 key,比較這些 key 的 revision 來判斷分佈式鎖應該屬於哪個請求。

2.2 獲取鎖異常

客戶端請求在獲取到分佈式鎖之後,如果發生異常,需要及時將鎖給釋放掉。因此需要租約,當我們申請分佈式鎖的時候需要指定租約時間。超過 lease 租期時間將會自動釋放鎖,保證了業務的可用性。是不是這樣就夠了呢?在執行業務邏輯時,如果客戶端發起的是一個耗時的操作,操作未完成的請情況下,租約時間過期,導致其他請求獲取到分佈式鎖,造成不一致。這種情況下則需要續租,即刷新租約,使得客戶端能夠和 etcd 服務端保持心跳。

3. 實現分佈式鎖的流程圖

我們基於如上分析的思路,繪製出實現 etcd 分佈式鎖的流程圖,如下所示:

4. 代碼實現

package main

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

func main() {
 // 客戶端配置
 config := clientv3.Config{
  Endpoints:   []string{"localhost:2379"},
  DialTimeout: 5 * time.Second,
 }

 var client *clientv3.Client
 var err error

 // 建立連接
 if client, err = clientv3.New(config); err != nil {
  fmt.Println(err)
  return
 }

 // 1. 上鎖並創建租約
 lease := clientv3.NewLease(client)
 var leaseGrantResp *clientv3.LeaseGrantResponse
 if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil {
  panic(err)
 }

 leaseId := leaseGrantResp.ID

 // 2 自動續約
 // 創建一個可取消的租約,主要是爲了退出的時候能夠釋放
 ctx, cancelFunc := context.WithCancel(context.TODO())

 // 3. 釋放租約
 defer cancelFunc()
 defer lease.Revoke(context.TODO(), leaseId)

 if keepRespChan, err := lease.KeepAlive(ctx, leaseId); err != nil {
  panic(err)
 } else {
  // 續約應答
  go func() {
   for {
    select {
    case keepResp := <-keepRespChan:
     if keepRespChan == nil {
      fmt.Println("租約已經失效了")
      goto END
     } else { // 每秒會續租一次, 所以就會受到一次應答
      fmt.Println("收到自動續租應答:", keepResp.ID)
     }
    }
   }
  END:
  }()
 }


 // 1.3 在租約時間內去搶鎖(etcd 裏面的鎖就是一個 key)
 kv := clientv3.NewKV(client)

 // 創建事務
 txn := kv.Txn(context.TODO())

 //if 不存在 key,then 設置它,else 搶鎖失敗
 txn.If(clientv3.Compare(clientv3.CreateRevision("lock")"=", 0)).
  Then(clientv3.OpPut("lock""g", clientv3.WithLease(leaseId))).
  Else(clientv3.OpGet("lock"))

 // 提交事務
 if txnResp, err := txn.Commit(); err != nil {
  panic(err)
 } else {
  if !txnResp.Succeeded {
   fmt.Println("鎖被佔用:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value))
   return
  }

  // 搶到鎖後執行業務邏輯,沒有搶到退出
  fmt.Println("處理任務")
  time.Sleep(5 * time.Second)
 }
}

預期的執行結果如下所示:

收到自動續租應答: 6825622810871743294
處理任務
收到自動續租應答: 6825622810871743294
收到自動續租應答: 6825622810871743294

Process finished with exit code 0

總得來說,如上關於 etcd 分佈式鎖的實現過程分爲四個步驟:

創建租約的時候,需要創建一個可取消的租約,主要是爲了退出的時候能夠釋放。釋放鎖對應的步驟,在上面的 defer 語句中。當 defer 租約關掉的時候,分佈式鎖對應的 key 就會被釋放掉了。

5. 小結

本文主要介紹了基於 etcd 實現分佈式鎖的案例。首先介紹了分佈式鎖產生的背景以及必要性,分佈式架構不同於單體架構,涉及到多服務之間多個實例的調用,跨進程的情況下使用編程語言自帶的併發原語沒有辦法實現數據的一致性,因此分佈式鎖出現,用來解決分佈式環境中的資源互斥操作。

接着本文重點介紹了基於 etcd 實現分佈式鎖的方案,根據 etcd 的特點,利用事務 txn、lease 租約以及 watch 監測實現分佈式鎖。

在我們上面的案例中,搶鎖失敗,客戶端就直接返回了。那麼當該鎖被釋放之後,或者持有鎖的客戶端出現了故障退出了,其他鎖如何快速獲取鎖呢?所以上述代碼可以基於 watch 監測特性進行改進,各位同學可以自行試試。(可以參考:https://github.com/zieckey/etcdsync)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6tC7zqMzrTS-FqJWTf1zuA