分佈式概念:分佈式鎖(分佈式互斥的一種實現方式)

分佈式系統中,多個節點都需要訪問一個臨界資源,但是同一時刻只能有一個節點可以訪問,爲了解決這個問題就是要通過分佈式互斥來實現;分佈式鎖就是實現分佈式互斥的一種實現方式。

鎖是實現多線程同時訪問同一共享資源,保證同一時刻只有一個線程可訪問共享資源所做的一種標記。
分佈式鎖是指分佈式環境下,系統部署在多個機器中,實現多進程分佈式互斥的一種鎖。爲了保證多個進程能看到鎖,鎖被存在公共存儲(比如 Redis、數據庫等三方存儲中),以實現多個進程併發訪問同一個臨界資源,同一時刻只有一個進程可訪問共享資源,確保數據的一致性。

分佈式鎖的三種實現方法
1.基於關係數據庫實現分佈式鎖
(1)創建一張鎖表,然後通過操作該表中的數據來實現。
(2)當我們要鎖住某個資源時,就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。
(3)數據庫對共享資源做了唯一性約束,如果有多個請求被同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,操作成功的那個線程就獲得了訪問共享資源的鎖。
缺點:
1.因爲數據庫需要落到硬盤上,頻繁讀取數據庫會導致 IO 開銷大,因此這種分佈式鎖適用於併發量低,對性能要求低的場景。
2.容易引起單點故障


2.基於緩存實現分佈式鎖
把數據存放在計算機內存中,不需要寫入磁盤,減少了 IO 讀寫。
實例:Redis 通過隊列來維持進程訪問共享資源的先後順序。
(1)Redis 通常可以使用 setnx(key, value) 函數來實現分佈式鎖。
(2)key 和 value 就是基於緩存的分佈式鎖的兩個屬性,其中 key 表示鎖 id,value = currentTime + timeOut,表示當前時間 + 超時時間。
(3)某個進程獲得 key 這把鎖後,如果在 value 的時間內未釋放鎖,系統就會主動釋放鎖。
(4)setnx 函數的返回值有 0 和 1:返回 1,說明該服務器獲得鎖,setnx 將 key 對應的 value 設置爲當前時間 + 鎖的有效時間。返回 0,說明其他服務器已經獲得了鎖,進程不能進入臨界區。該服務器可以不斷嘗試 setnx 操作,以獲得鎖。


3.基於 ZooKeeper 實現分佈式鎖
ZooKeeper 基於樹形數據存儲結構實現分佈式鎖。
節點類型:
持久節點。這是默認的節點類型,一直存在於 ZooKeeper 中。
持久順序節點。也就是說,在創建節點時,ZooKeeper 根據節點創建的時間順序對節點進行編號。
臨時節點。與持久節點不同,當客戶端與 ZooKeeper 斷開連接後,該進程創建的臨時節點就會被刪除。
臨時順序節點。按時間順序編號的臨時節點。

以電商售賣吹風機的場景爲例。假設用戶 A、B、C 同時在 11 月 11 日的零點整提交了購買吹風機的請求,ZooKeeper 會採用如下方法來實現分佈式鎖:
1.在與該方法對應的持久節點 shared_lock 的目錄下,爲每個進程創建一個臨時順序節點。吹風機就是一個擁有 shared_lock 的目錄,當有人買吹風機時,會爲他創建一個臨時順序節點。
2.每個進程獲取 shared_lock 目錄下的所有臨時節點列表,註冊子節點變更的 Watcher,並監聽節點。
3.每個節點確定自己的編號是否是 shared_lock 下所有子節點中最小的,若最小,則獲得鎖。例如,用戶 A 的訂單最先到服務器,因此創建了編號爲 1 的臨時順序節點 LockNode1。該節點的編號是持久節點目錄下最小的,因此獲取到分佈式鎖,可以訪問臨界資源,從而可以購買吹風機。
4.若本進程對應的臨時節點編號不是最小的,則分爲兩種情況:
   a. 本進程爲讀請求,如果比自己序號小的節點中有寫請求,則等待;
   b. 本進程爲寫請求,如果比自己序號小的節點中有讀請求,則等待。

例如,用戶 B 也想要買吹風機,但在他之前,用戶 C 想看看吹風機的庫存量。因此,用戶 B 只能等用戶 A 買完吹風機、用戶 C 查詢完庫存量後,才能購買吹風機。

ZooKeeper 分佈式鎖的可靠性最高,有封裝好的框架,很容易實現分佈式鎖的功能,並且幾乎解決了數據庫鎖和緩存式鎖的不足

etcd分佈式鎖實現原理:

1.利用租約在etcd集羣中創建一個key,這個key有兩種形態,存在和不存在,而這兩種形態就是互斥量。
2.如果這個key不存在,那麼線程創建key,成功則獲取到鎖,該key就爲存在狀態。
3.如果該key已經存在,那麼線程就不能創建key,則獲取鎖失敗。

鎖結構體:

在使用該鎖時,需要傳入Ttl,Conf,Key字段來初始化鎖

type EtcdMutex struct {
    Ttl int64  //租約時間
    Conf clientv3.Config  //etcd集羣配置
    Key string   //etcd的key
    cancel context.CancelFunc  //關閉續租的func
    lease clientv3.Lease
    leaseID clientv3.LeaseID
    txn clientv3.Txn
}

初始化鎖:

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
    }
    if !txnResp.Succeeded{   //判斷txn.if條件是否成立
        return fmt.Errof("搶鎖失敗")        
    }
    return nil
}

釋放鎖:

func(em *EtcdMutex)UnLock(){
    em.cancel()
    em.lease.Revoke(context.TODO(),em.leaseID)
    fmt.Println("釋放了鎖")
}

調用鎖:

func main(){
    var conf = clientv3.Config{
        Endpoints:   []string{"172.16.196.129:2380", "192.168.50.250:2380"},
        DialTimeout: 5 * time.Second,
    }
    eMutex1 := &EtcdMutex{
        Conf:conf,
        Ttl:10,
        Key:"lock",
    }
     eMutex2 := &EtcdMutex{
        Conf:conf,
        Ttl:10,
        Key:"lock",
    }
    //groutine1 
    go func() {
        err := eMutex1.Lock()
        if err != nil{
            fmt.Println("groutine1搶鎖失敗")
            fmt.Println(err)
            return
        }
        fmt.Println("groutine1搶鎖成功")
        time.Sleep(10*time.Second)
        defer eMutex.UnLock()
    }()

    //groutine2
    go func() {
        err := eMutex2.Lock()
        if err != nil{
            fmt.Println("groutine2搶鎖失敗")
            fmt.Println(err)
            return
        }
        fmt.Println("groutine2搶鎖成功")
        defer eMutex.UnLock()
    }()
    time.Sleep(30*time.Second)
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章