Zookeeper1、原生ZK方案 Zookeeper中有一種節(jié)點叫做順序節(jié)點,假如我們在/lock/目錄下創(chuàng)建節(jié)3個點,ZooKeeper集群會按照提起創(chuàng)建的順序來創(chuàng)建節(jié)點,節(jié)點分別為/lock/0000000001、/lock/0000000002、/lock/0000000003。 ZooKeeper中還有一種名為臨時節(jié)點的節(jié)點,臨時節(jié)點由某個客戶端創(chuàng)建,當客戶端與ZooKeeper集群斷開連接,則開節(jié)點自動被刪除。 EPHEMERAL_SEQUENTIAL為臨時順序節(jié)點 實現(xiàn)分布式鎖的基本邏輯: 客戶端調(diào)用create()方法創(chuàng)建名為“locknode/guid-lock-”的節(jié)點,需要注意的是,這里節(jié)點的創(chuàng)建類型需要設(shè)置為EPHEMERAL_SEQUENTIAL。 客戶端調(diào)用getChildren(“locknode”)方法來獲取所有已經(jīng)創(chuàng)建的子節(jié)點。 客戶端獲取到所有子節(jié)點path之后,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點是所有節(jié)點中序號最小的,那么就認為這個客戶端獲得了鎖。 如果創(chuàng)建的節(jié)點不是所有節(jié)點中需要最小的,那么則監(jiān)視比自己創(chuàng)建節(jié)點的序列號小的最大的節(jié)點,進入等待。直到下次監(jiān)視的子節(jié)點變更的時候,再進行子節(jié)點的獲取,判斷是否獲取鎖。
釋放鎖的過程相對比較簡單,就是刪除自己創(chuàng)建的那個子節(jié)點即可。 以下是流程圖:
讀寫鎖:讀寫鎖的實現(xiàn)與互斥鎖類似,不同的地方在于創(chuàng)建自節(jié)點時讀鎖和寫鎖要區(qū)分類型。例如讀鎖的前綴可以設(shè)置為read,寫鎖的前綴可以設(shè)置為write。創(chuàng)建讀鎖的時候,檢查是否有編號小于自己的寫鎖存在,若存在則對編號剛好小于自己的寫鎖節(jié)點進行監(jiān)聽。創(chuàng)建寫鎖時,檢查創(chuàng)建的節(jié)點編號是否為最小,如不是最小,則需要對編號剛好小于自己的節(jié)點進行監(jiān)聽(此時不區(qū)分讀鎖和寫鎖) 2、Curator方案 封裝了zk的客戶端,其分布式實現(xiàn)方式和上面的基本相同。同時還提供了不同的鎖類型: 可重入鎖:實現(xiàn)類為InterProcessMutex,將線程對象,節(jié)點,鎖對象相關(guān)聯(lián)。InterProcessMutex內(nèi)部維護了一個使用線程為key,{thread,path}為值的map,所以對不同的線程和請求加鎖的節(jié)點進行一一對應(yīng)。提供方法acquire 和 release。 不可重入鎖:實現(xiàn)類為InterProcessSemaphoreMutex,類似InterProcessMutex,只是沒有維護線程的map。 可重入讀寫鎖:類似JDK的ReentrantReadWriteLock .一個讀寫鎖管理一對相關(guān)的鎖。 主要由兩個類實現(xiàn): 使用時首先創(chuàng)建一個InterProcessReadWriteLock 實例,然后再根據(jù)你的需求得到讀鎖或者寫鎖, 讀寫鎖的類型是InterProcessLock 。 讀寫鎖的實現(xiàn)與互斥鎖類似,不同的地方在于創(chuàng)建自節(jié)點時讀鎖和寫鎖要區(qū)分類型。例如讀鎖的前綴可以設(shè)置為read,寫鎖的前綴可以設(shè)置為write。創(chuàng)建讀鎖的時候,檢查是否有編號小于自己的寫鎖存在,若存在則對編號剛好小于自己的寫鎖節(jié)點進行監(jiān)聽。創(chuàng)建寫鎖時,檢查創(chuàng)建的節(jié)點編號是否為最小,如不是最小,則需要對編號剛好小于自己的節(jié)點進行監(jiān)聽(此時不區(qū)分讀鎖和寫鎖) 還有信號量和多鎖對象。 3、menagerie方案 menagerie基于Zookeeper實現(xiàn)了java.util.concurrent包的一個分布式版本。這個封裝是更大粒度上對各種分布式一致性使用場景的抽象。其中最基礎(chǔ)和常用的是一個分布式鎖的實現(xiàn): org.menagerie.locks.ReentrantZkLock,通過ZooKeeper的全局有序的特性和EPHEMERAL_SEQUENTIAL類型znode的支持,實現(xiàn)了分布式鎖。 Redis最常見互斥鎖方案: Redis的SETNX(即SET if Not eXists)和GETSET(先寫新值,返回舊值,原子性操作,可以用于分辨是不是首次操作)可以用于分布式鎖: C3發(fā)送SETNX lock.{orderid} 想要獲得鎖,由于C0還持有鎖,所以Redis返回給C3一個0, C3發(fā)送GET lock.{orderid} 以檢查鎖是否超時了,如果沒超時,則等待或重試。 反之,如果已超時,C3通過下面的操作來嘗試獲得鎖: GETSET lock.{orderid} <current Unix time + lock timeout + 1> 通過GETSET,C3拿到的時間戳如果仍然是超時的,那就說明,C3如愿以償拿到鎖了。 如果在C3之前,有個叫C4的客戶端比C3快一步執(zhí)行了上面的操作,那么C3拿到的時間戳是個未超時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒拿到鎖,但它改寫了C4設(shè)置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。
jeffkit的偽碼參考: # get lock lock = 0 while lock != 1: timestamp = current Unix time + lock timeout + 1 lock = SETNX lock.orderid timestamp if lock == 1 or (now() > (GET lock.orderid) and now() > (GETSET lock.orderid timestamp)): break else: sleep(10ms) do_your_job() # release lock if now() < GET lock.orderid: DEL lock.orderid
Tair設(shè)計思路和Medis類似,但實現(xiàn)略有不同。 美團維護的Tair中增加了expireLock和expireUnlock接口,通過鎖狀態(tài)和過期時間戳來共同判斷鎖是否存在:只有鎖已經(jīng)存在且沒有過期的狀態(tài)才判定為有鎖狀態(tài)。在有鎖狀態(tài)下,不能加鎖,能通過大于過期時間的時間戳進行解鎖;在無鎖狀態(tài)下,可以加鎖,加鎖成功會返回過期時間戳,用于解鎖使用。重要的是,expireLock的原子性可以保證加鎖和解鎖時不會因為線程搶占引起錯誤。 不可重入鎖:在加鎖時調(diào)用expireLock,解鎖時調(diào)用expireUnlock接口。傳入的參數(shù)為過期時間或者過期時間戳??梢苑乐巩斁€程拿到鎖之后阻塞或者宕機,鎖可以在過期之后釋放出來。同時可以滿足解鎖動作安全,當自己的鎖過期時不會誤刪別人的鎖。 可重入鎖:類似不可重入鎖,維護類似zk的一個線程數(shù)和鎖名的map。 可重入讀寫鎖: 讀線程:先用當前時間進行一次解鎖expireUnlock,如果能解開則說明沒有線程在寫,可以進行讀操作,同時incr,將計數(shù)器加1;完成讀之后進行decr。 寫線程:getCount讀取計數(shù)器,如果為0,則說明沒有線程在讀,否則則需要等待;再expireLock,如果成功說明獲取到了寫鎖,否則則說明已經(jīng)有線程在寫了;完成寫之后進行解鎖expireUnlock 缺陷:均有兩步操作,但無法保證原子性。
|