jedisLock—redis分佈式鎖實現

什麼是分佈式鎖

分佈式鎖是控制分佈式系統或者不一樣系統之間共同訪問共享資源的一種鎖實現,若是不一樣的系統或同一個系統的不一樣主機之間共享了某個資源,每每須要互斥來防止彼此干擾來保證一致性。java

分佈式鎖須要解決的問題

1.互斥性:任意時刻,只能用一個客戶端獲取鎖,不能同時有兩個客戶端獲取到鎖。 2.安全性:鎖只能被持有改鎖的客戶端刪除,不能由其餘客戶端刪除。 3.死鎖:獲取鎖的客戶端由於某些緣由(如down機等)而未能釋放鎖,其餘客戶端再也沒法獲取到該鎖。 4.容錯:當部分節點(redis節點等)down機時,客戶端仍可以獲取鎖和釋放鎖。node

常見的分佈式鎖方案

3.1 數據庫惟一性索引

輸入圖片說明

3.1.1 方案流程

(1)建立一個job,初始化: id(自增主鍵),jobName(惟一鍵),status(0)redis

(2)客戶端1執行turnOnJob(jobName)請求鎖,算法

(3)當前status=0,得到鎖成功,將status置爲1數據庫

(4)客戶端1進行業務處理緩存

(5)客戶端2執行turnOnJob(jobName)請求鎖安全

(6)當前status=1,得到鎖失敗服務器

(7)客戶端2捕得到到鎖失敗,進入重試隊列,由重試做業來進行下次鎖的獲取已及業務邏輯的處理。網絡

3.1.2 方案解決問題

(1)互斥性:利用數據庫的惟一性索引,使得鎖不會由兩個客戶端在同一時刻得到鎖。多線程

(2)安全性:數據庫插入成功後,會將status置爲1,並只會在業務執行完以後纔會將status置爲0。因此其餘客戶端是沒法刪除客戶端所持有的鎖的。

(3)死鎖:因爲一些異常狀況(重啓)使得鎖沒有釋放,這樣會致使死鎖,經過設置過時時間機制,來避免這種異常死鎖。經過一個Job,定時的清除已通過期的鎖,過時條件:Math.floor(|Now-CreatedTime|)>ExpiredTime

(4)容錯:利用數據庫主歷來進行容錯的處理。主節點發生故障時,從節點切換成主節點,從而不影響分佈式鎖服務的運行。

3.1.3 方案優點

(1)輕量級:能夠快速實現分佈式鎖的功能,快速上線。

3.1.4 方案不足

(1)因爲須要不斷的讀寫數據庫,系統開銷比較大,依賴數據庫的性能,對數據庫有必定程度的影響。對於小併發的場景下知足要求,可是在大併發或者在分佈式集羣下可能會有性能問題。

(2)跟數據庫的某些特性耦合的太緊,首先是不一樣的數據庫會有不一樣的特性,其次數據庫的鏈接等都是緊俏的資源,沒有針對分佈式鎖這種場景作特殊定製和優化。

3.2 Zookeeper分佈式鎖

3.1.1 ZK實現分佈式鎖的優點

分佈式系統在某些場景下,必須對客戶端請求肯定順序、分清主次,由於有些資源在同一時間只容許被一個進程/線程處理,這時就須要分佈式鎖。利用分佈式鎖能保證數據一致性,避免出現負庫存、撞單等錯誤。

雖然不少數據庫都提供了鎖的功能,可是DB鎖的效率極低,並且在高併發下把壓力都堆到DB上顯然是極其危險的行爲,在應用層實現分佈式鎖能夠有效的減輕DB的壓力,從而提升系統的穩定性和可用性。

zk主要是用來提供分佈式一致性的,因此很天然的想到利用zk來實現分佈式鎖。由於分佈式鎖是應用在高併發場景下的(低併發場景也須要鎖,但不須要分佈式),分佈式+高併發天然地要引入集羣,鎖就存在於集羣內的各個節點上,分佈式鎖在邏輯上只有一把,但物理上存在多把,必須保證各節點上同一把鎖的狀態一致,這正是分佈式一致性算法要作的事。

3.1.2 實現原理

zk實現分佈式鎖是利用對節點的操做來進行的,鎖操做主要涉及加鎖和解鎖,對應到zk裏,加鎖就是建立節點(臨時節點),解鎖就是刪除節點,全部客戶端都在同一父節點下進行加鎖和解鎖操做。具體實現思路有如下兩種:

1、單節點鎖

以某一固定節點爲鎖,全部客戶端爭搶這個節點,搶到即爲加鎖成功。

(1)某個client加鎖時,先嚐試建立這個節點,若是建立成功,說明該節點不存在,即當前無鎖,此client得到鎖;

(2)若是建立失敗,說明該節點已存在,即有其它client已得到鎖,此client阻塞(等待節點刪除的通知)或循環重試;

(3)client得到鎖並執行完業務邏輯後,刪除該節點(即解鎖),zk通知其它client(喚醒阻塞或跳出重試循環)。

2、多節點鎖

在單節點鎖中,全部client操做同一節點,當持有鎖的client釋放瑣時,其它全部client都從阻塞中喚醒,將以競爭的方式來爭搶鎖,誰先得到鎖取決於各client的網絡情況和zk集羣節點的cpu調度等不可控因素,和client的先來後到徹底無關。若是但願各client能按前後順序(至少在網絡差別不大的狀況下)來得到鎖,就須要多節點鎖來實現,即每一個客戶端建立本身的專屬節點(全部節點在同一父節點下),在知足特定條件時,本身的節點會成爲鎖。

zk有三種節點:永久節點、臨時節點和順序節點,能夠組合成四種節點類型(永久、臨時、永久順序和臨時順序),爲了實現先進先出的功能(先加鎖的先得到鎖),選擇臨時順序節點來充當鎖的角色。臨時節點的特色是當會話失效時節點會被zk自動清除,順序節點的特色是zk會爲節點加上一個遞增的序號做爲後綴,序號按節點建立時間的前後順序遞增。基本原理以下:

(1)某個client嘗試加鎖時,直接建立一個順序節點;

(2)load出父節點下的全部子節點(getChildren),判斷剛纔的節點序號是否最小,若是最小則表示當前client是第一個嘗試加鎖的,它將得到鎖,若是不是最小則阻塞;

(3)得到鎖並執行完業務邏輯後,刪除本身建立的節點,zk通知其它client;

(4)從阻塞中喚醒後,執行2中的操做

3.1.3 死鎖

分佈式鎖中的死鎖,不是指多線程中的無線迴路等待,而是指出現一個「幽靈鎖」,這把鎖在被建立後就和加鎖者失去聯繫,加鎖者也不知道本身建立過這把鎖,固然也就沒法對其進行解鎖,進而致使其它client沒法得到鎖。有兩種典型場景:

(1)client發送建立節點的請求後因異常(如網絡故障)和zk斷開鏈接,zk服務端接收到請求併成功建立節點,這個節點就成爲一個「幽靈節點」,它不與任何client關聯,沒法釋放(能夠設置失效時間,經過定時任務來清理,但顯然增長了系統的複雜度)。

(2)client建立節點成功後,在作業務邏輯的過程當中出現異常,和zk斷開鏈接,未能執行解鎖操做。

在單節點鎖和多節點鎖中,應對死鎖的方案有所不一樣,但核心思想都是重試+校驗。重試就是client在異常斷開後發起重試,再次嘗試加鎖,直到成功爲止(無限重試),校驗就是在建立節點前先檢查當前節點(若是存在)是不是本身以前建立的。

具體來講,對於單節點鎖,能夠把client的私有信息(如ip)寫入節點關聯的字符串,每次加鎖前先比較當前節點字符串中的信息是否和本身匹配,若是匹配就表示當前節點是本身以前建立的,直接視爲加鎖成功。對於多節點鎖,每一個client持有一個惟一ID(好比java的uuid),將此ID做爲節點名稱的前綴,也就是用於判斷節點歸屬的依據,每次建立節點前先load出父節點下的全部子節點,遍歷子節點列表判斷每一個子節點名稱中是否包含此id,若是包含說明這個子節點就是本身以前建立的,而後才判斷序號是否最小。

3.1.4 代碼實現

1、單節點鎖實現
/**
 * 單節點分佈式鎖
 */
public class SingleNodeLock {
    private String clientId;
    private String nodeName;
    private ZKClient zkClient;
    //鎖節點全路徑名稱
    private static final String LOCK_NAME = "/zk/lock/publiclock";
    public SingleNodeLock() throws Exception {
        clientId = UUID.randomUUID().toString().replaceAll("-", "") + "-";
        zkClient = new ZKClient();
    }
    /**
     * 加鎖
     * @return
     */
    public boolean lock() {
        //先嚐試獲取現有鎖節點數據
        String lockInfo = zkClient.getData(LOCK_NAME);
        if ("nonode".equals(lockInfo)) {
            System.out.println("當前無鎖," + clientId + "嘗試加鎖");
            //節點不存在(即無鎖),當前客戶端能夠嘗試加鎖(即建立節點)
            String lockNode = zkClient.Create(LOCK_NAME, clientId, CreateMode.PERSISTENT);
            if (LOCK_NAME.equals(lockNode)) {
                //建立節點成功即加鎖成功
                setNodeName(lockNode);
                return true;
            }
            /*即便開始判斷鎖節點不存在,當前客戶端也不必定能成功建立節點,在多線程下可能被其它線程搶先建立*/
            return false;
        } else {
            System.out.println("當前有鎖,client標記:" + lockInfo);
            //鎖存在且屬於當前客戶端
            return clientId.equals(lockInfo) ? true : false;
        }
    }
    /**
     * 解鎖
     */
    public void unlock() {
        System.out.println(clientId + "解鎖");
        zkClient.delete(nodeName);
    }
    //5個客戶端線程爭鎖
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(new Runnable() {
                public void run() {
                    try {
                        SingleNodeLock snl = new SingleNodeLock();
                        while(true) {
                            if (snl.lock()) {
                                System.out.println(snl.getClientId() + "得到鎖,3s後解鎖");
                                Thread.sleep(3000);
                                snl.unlock();
                                snl.getZkClient().close();
                                break;
                            }
                            System.out.println(snl.getClientId() + "未得到鎖,等待2s");
                            Thread.sleep(2000);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

3.3 Redis分佈式鎖

輸入圖片說明

Redis命令參考
SETEX key seconds value
setex:將值value關聯到key,而且設置key的生存時間爲 seconds(以秒爲單位)。
若是 key 已經存在, SETEX 命令將覆寫舊值。
這個命令相似於如下兩個命令:
SET key value
EXPIRE key seconds  # 設置生存時間
SETNX
SETNX key value

將 key 的值設爲 value ,當且僅當 key 不存在。

若給定的 key 已經存在,則 SETNX 不作任何動做。

SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫。
PSETEX
PSETEX key milliseconds value

這個命令和 SETEX 命令類似,但它以毫秒爲單位設置 key 的生存時間,而不是像 SETEX 命令那樣,以秒爲單位。

可用版本:
>= 2.6.0
時間複雜度:
O(1)
返回值:
設置成功時返回 OK 。
SET
SET key value [EX seconds] [PX milliseconds] [NX|XX]

將字符串值 value 關聯到 key 。

若是 key 已經持有其餘值, SET 就覆寫舊值,無視類型。

對於某個本來帶有生存時間(TTL)的鍵來講, 當 SET 命令成功在這個鍵上執行時, 這個鍵原有的 TTL 將被清除。

可選參數

從 Redis 2.6.12 版本開始, SET 命令的行爲能夠經過一系列參數來修改:

EX second :設置鍵的過時時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。
PX millisecond :設置鍵的過時時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value 。
NX :只在鍵不存在時,纔對鍵進行設置操做。 SET key value NX 效果等同於 SETNX key value 。
XX :只在鍵已經存在時,纔對鍵進行設置操做。

由於 SET 命令能夠經過參數來實現和 SETNX 、 SETEX 和 PSETEX 三個命令的效果,因此未來的 Redis 版本可能會廢棄並最終移除 SETNX 、 SETEX 和 PSETEX 這三個命令。

redis命令:SET resource_name my_value KEYX EXPTIME 30000

KEYX:若是緩存中不存在key值,則在緩存中設置該key值。

EXPTIME:過時時間,key值將在過時時間事後失效。

my_value:該key設置的對應的值。這個值必須在全部的客戶端和全部的 lock request 之間惟一。這個惟一值能夠用來保證刪除鎖時的安全性:刪除鎖時只有key值相等,而且key值對應的value相等的客戶端,才能夠刪除鎖。

key插入失敗,會拋出異常,客戶端可以捕獲,判斷是否插入成功。

3.3.1.1 方案流程

(1)客戶端1向redis申請鎖(key=1, value=1)

(2)redis中無鎖的記錄,客戶端1插入成功,得到鎖成功

(3)客戶端1進行業務邏輯處理

(4)客戶端2向redis申請鎖(key=1, value=2)

(5)redis中已經有此鎖記錄,客戶端2插入失敗,得到鎖

(6)客戶端2捕獲得到鎖失敗的異常,進入重試隊列,由重試做業來進行下次所的獲取以及邏輯得處理

(7)客戶端1釋放鎖(key=1, value=1)

(8)客戶端2從新得到鎖成功,並進行業務邏輯處理

3.3.1.2 方案解決問題

(1)互斥性:redis的命令能夠保證只有一個客戶端能夠寫入成功,其餘客戶端會寫入失敗。

(2)安全性:設置key的value時,經過構造全局惟一的 my_random_value,在刪除鎖時key和value都相等時才能夠進行刪除,以此來保證本客戶端的鎖不會被其餘客戶端刪除。

(3)死鎖:因爲一些異常狀況(好比:服務器重啓)使得鎖沒有釋放,這樣會致使死鎖,經過設置redis的key過時時間,來避免這種異常死鎖。

(4)容錯:沒解決

3.3.1.3 方案優點

(1)redis基於內存設置KV,性能好,可靠性高,對大並大能夠有很好的支持,單機QPS能夠達到5w。

3.3.1.4 方案不足

(1)單點故障,當單機redis出現故障時,沒法作到故障切換,會致使整個分佈式的服務不可用。

(2)須要人爲監控單機redis的機器狀態,有必定的維護成本。

應用場景

使用與單機redis環境。

3.3.2 方案2

輸入圖片說明

3.3.2.1 方案流程

同3.1.1

3.3.2.2 方案解決問題

(1)互斥性:沒解決

(2)安全性:設置key的value時,經過構造全局惟一的my_value,在刪除鎖時key和value否相等時才能夠進行刪除,以此來保證客戶端的鎖不會被其它客戶端刪除。

(3)死鎖:因爲一些異常狀況(好比:服務器重啓)使得鎖沒有釋放,這樣會致使死鎖,經過設置redis的key過時時間來避免這種異常的死鎖。

(4)容錯:當redis主節點down機時,redis從節點晉升爲主節點,繼續提供分佈式鎖服務。

3.3.2.3 方案優點

同3.3.1相比,redis主從模式提供了故障切換機制,能夠保證分佈式服務的正常提供。

3.3.2.4 方案不足

(1)互斥性沒法保證。redis主從複製是異步複製,當客戶端1在redis主節點設置鎖成功後,當尚未同步到從節點時,主節點down機,從節點升級爲主節點提供分佈式鎖服務,客戶端2再次申請得到取鎖服務,而剛剛升級完主節點的機器由於沒有key值,客戶端2會申請鎖成功,而此時客戶端1的業務邏輯並無處理完成,在這種狀況下,客戶端1和客戶端2就同時擁有了分佈式鎖,互斥性的條件沒法知足。

場景

主從備份機制:主機和從機的數據是一致的,在主機運行時,備份機時閒置的。

3.3.3 方案3

輸入圖片說明 3.3.2.1 方案流程

(1)客戶端1獲取系統當前時間 current_time(ms)

(2)客戶端1輪流用相同的key在N個redis節點上請求鎖。客戶端1在每一個master節點請求鎖時,會有一個比鎖的過時時間相比小不少的超時時間。好比鎖的過時時間是10s,那每一個節點鎖請求的超時時間多是5-50ms的範圍,這樣能夠防止客戶端在某個down掉的master節點上阻塞過長時間,若是一個master節點不可用,應儘快嘗試下一個master節點。

(3)客戶端1計算在第二步中獲取鎖所花費的時間cost_time_get_lock,只有當客戶端在大多數master節點上(N/2+1)成功獲取了鎖,並且鎖花費的時間不超過鎖的過時時間,那麼這個鎖就認爲是分配成功了。

(4)若是所獲取成功了,那麼鎖的自動釋放時間爲 鎖的過時時間- 得到鎖所花費的時間(expire_time - cost_time_get_lock)

(5)客戶端2獲取鎖失敗,多是由於成功獲取的鎖不超過 N/2+1,多是由於獲取鎖的時間超過了鎖過時時間,客戶端都必須在每一個master節點上釋放鎖,包括那些客戶端2認爲沒有成功獲取到的鎖。

3.3.2.2 方案解決問題

(1)互斥性:只有在大多數節點都獲取鎖成功時,才認爲客戶端獲取鎖成功,沒有獲取鎖成功的客戶端釋放全部已經持有的鎖。

(2)安全性:設置key的value時,經過構造全局惟一的my_value,在刪除鎖時key和value都相等時才能夠進行刪除,以此來保證本客戶端的鎖不被其餘客戶端刪除。

(3)死鎖:因爲一些異常狀況(好比:服務器重啓)使輸入圖片說明得鎖沒有釋放,這樣會致使死鎖,經過設置redis的key過時時間來避免這種異常死鎖。

(4)容錯:當redis主節點down機時,其餘redis節點繼續組成集羣,提供分佈式鎖服務。

3.3.2.3 方案優點

基本上解決了分佈式鎖應該解決的問題。

3.3.2.4 方案不足

(1)算法複雜,開發成本高,維護代價大。

相關文章
相關標籤/搜索