1. 實現分布式鎖的組件們
在分布式系統中,常用于實現分布式鎖的組件有:Redis、zookeeper、etcd,下面針對各自的特性進行對比:
由上圖可以看出三種組件各自的特點,其中對于分布式鎖來說至關重要的一點是要求CP。但是,Redis集群卻不支持CP,而是支持AP。雖然,官方也給出了redlock的方案,但由于需要部署多個實例(超過一半實例成功才視為成功),部署、維護比較復雜。所以在對一致性要求很高的業務場景下(電商、銀行支付),一般選擇使用zookeeper或者etcd。對比zookeeper與etcd,如果考慮性能、并發量、維護成本來看。由于etcd是用Go語言開發,直接編譯為二進制可執行文件,并不依賴其他任何東西,則更具有優勢。本文,則選擇etcd來討論某些觀點。
2. 對于分布式鎖來說AP為什么不好
在CAP理論中,由于分布式系統中多節點通信不可避免出現網絡延遲、丟包等問題一定會造成網絡分區,在造成網絡分區的情況下,一般有兩個選擇:CP or AP。
① 選擇AP模型實現分布式鎖時,client在通過集群主節點加鎖成功之后,則立刻會獲取鎖成功的反饋。此時,在主節點還沒來得及把數據同步給從節點時發生down機的話,系統會在從節點中選出一個節點作為新的主節點,新的主節點沒有老的主節點對應的鎖數據,導致其他client可以在新的主節點上拿到相同的鎖。這個時候,就會導致多個進程/線程/協程來操作相同的臨界資源數據,從而引發數據不一致性等問題。
② 選擇CP模型實現分布式鎖,只有在主節點把數據同步給大于1/2的從節點之后才被視為加鎖成功。此時,主節點由于某些原因down機,系統會在從節點中選取出來數據比較新的一個從節點作為新的主節點,從而避免數據丟失等問題。
所以,對于分布式鎖來說,在對數據有強一致性要求的場景下,AP模型不是一個好的選擇。如果可以容忍少量數據丟失,出于維護成本等因素考慮,AP模型的Redis可優先選擇。
3. 分布式鎖的特點以及操作
對于分布式鎖來說,操作的動作包含:
- 獲取鎖
- 釋放鎖
- 業務處理過程中過程中,另起線程/協程進行鎖的續約
4. 關于etcd
官方文檔永遠是最好的學習資料,官方介紹etcd如是說:
- 分布式系統使用etcd作為配置管理、服務發現和協調分布式工作的一致鍵值存儲。許多組織使用etcd來實現生產系統,如容器調度器、服務發現服務和分布式數據存儲。使用etcd的常見分布式模式包括leader選舉、分布式鎖和監視機器活動。
- Distributed systems use etcd as a consistent key-value store for configuration management, service discovery, and coordinating distributed work. Many organizations use etcd to implement production systems such as container schedulers, service discovery services, and distributed data storage. Common distributed patterns using etcd include leader election, distributed locks, and monitoring machine liveness.
- https://etcd.io/docs/v3.4/learning/why/
分布式鎖僅是etcd可以實現眾多功能中的一項,服務注冊與發現在etcd中用的則會更多。
官方也對眾多組件進行了對比,并整理如下:
通過對比可以看出各自的特點,至于具體選擇哪一款,你心中可能也有了自己的答案。
5. etcd實現分布式鎖的相關接口
對于分布式鎖,主要用到etcd對應的添加、刪除、續約接口。
- // KV:鍵值相關操作
- type KV interface {
- // 存放.
- Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
- // 獲取.
- Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
- // 刪除.
- Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
- // 壓縮rev指定版本之前的歷史數據.
- Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
- // 通用的操作執行命令,可用于操作集合的遍歷。Put/Get/Delete也是基于Do.
- Do(ctx context.Context, op Op) (OpResponse, error)
- // 創建一個事務,只支持If/Then/Else/Commit操作.
- Txn(ctx context.Context) Txn
- }
- // Lease:租約相關操作
- type Lease interface {
- // 分配一個租約.
- Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
- // 釋放一個租約.
- Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
- // 獲取剩余TTL時間.
- TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
- // 獲取所有租約.
- Leases(ctx context.Context) (*LeaseLeasesResponse, error)
- // 續約保持激活狀態.
- KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
- // 僅續約激活一次.
- KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)
- // 關閉續約激活的功能.
- Close() error
- }
6. etcd實現分布式鎖代碼示例
- package main
- import (
- "context"
- "fmt"
- "go.etcd.io/etcd/clientv3"
- "time"
- )
- var conf clientv3.Config
- // 鎖結構體
- type EtcdMutex struct {
- Ttl int64//租約時間
- Conf clientv3.Config //etcd集群配置
- Key string//etcd的key
- cancel context.CancelFunc //關閉續租的func
- txn clientv3.Txn
- lease clientv3.Lease
- leaseID clientv3.LeaseID
- }
- // 初始化鎖
- func (em *EtcdMutex) init() error {
- var err error
- var ctx context.Context
- client, err := clientv3.New(em.Conf)
- if err != nil {
- return err
- }
- em.txn = clientv3.NewKV(client).Txn(context.TODO())
- em.lease = clientv3.NewLease(client)
- leaseResp, err := em.lease.Grant(context.TODO(), em.Ttl)
- if err != nil {
- return err
- }
- ctx, em.cancel = context.WithCancel(context.TODO())
- em.leaseID = leaseResp.ID
- _, err = em.lease.KeepAlive(ctx, em.leaseID)
- return err
- }
- // 獲取鎖
- func (em *EtcdMutex) Lock() error {
- err := em.init()
- if err != nil {
- return err
- }
- // LOCK
- em.txn.If(clientv3.Compare(clientv3.CreateRevision(em.Key), "=", 0)).
- Then(clientv3.OpPut(em.Key, "", clientv3.WithLease(em.leaseID))).Else()
- txnResp, err := em.txn.Commit()
- if err != nil {
- return err
- }
- // 判斷txn.if條件是否成立
- if !txnResp.Succeeded {
- return fmt.Errorf("搶鎖失敗")
- }
- returnnil
- }
- //釋放鎖
- func (em *EtcdMutex) UnLock() {
- // 租約自動過期,立刻過期
- // cancel取消續租,而revoke則是立即過期
- em.cancel()
- em.lease.Revoke(context.TODO(), em.leaseID)
- fmt.Println("釋放了鎖")
- }
- // groutine1
- func try2lock1() {
- eMutex1 := &EtcdMutex{
- Conf: conf,
- Ttl: 10,
- Key: "lock",
- }
- err := eMutex1.Lock()
- if err != nil {
- fmt.Println("groutine1搶鎖失敗")
- return
- }
- defer eMutex1.UnLock()
- fmt.Println("groutine1搶鎖成功")
- time.Sleep(10 * time.Second)
- }
- // groutine2
- func try2lock2() {
- eMutex2 := &EtcdMutex{
- Conf: conf,
- Ttl: 10,
- Key: "lock",
- }
- err := eMutex2.Lock()
- if err != nil {
- fmt.Println("groutine2搶鎖失敗")
- return
- }
- defer eMutex2.UnLock()
- fmt.Println("groutine2搶鎖成功")
- }
- // 測試代碼
- func EtcdRunTester() {
- conf = clientv3.Config{
- Endpoints: []string{"127.0.0.1:2379"},
- DialTimeout: 5 * time.Second,
- }
- // 啟動兩個協程競爭鎖
- go try2lock1()
- go try2lock2()
- time.Sleep(300 * time.Second)
- }
總結
可以提供分布式鎖功能的組件有多種,但是每一種都有自己的脾氣與性格。至于選擇哪一種組件,則要看數據對業務的重要性,數據要求強一致性推薦支持CP的etcd、zookeeper,數據允許少量丟失、不要求強一致性的推薦支持AP的Redis。
原文鏈接:https://www.toutiao.com/i6963570087278182923/