高併發架構系列:什麼是分佈式鎖?Redis實現分佈式鎖詳解

在很多場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。那具體什麼是分佈式鎖,分佈式鎖應用在哪些業務場景、如何來實現分佈式鎖呢?今天繼續由陳睿|mikechen來繼續分享Redis這個系列。


01.什麼是分佈式鎖

要介紹分佈式鎖,首先要提到與分佈式鎖相對應的是線程鎖、進程鎖。

1.線程鎖

主要用來給方法、代碼塊加鎖。當某個方法或代碼使用鎖,在同一時刻僅有一個線程執行該方法或該代碼段。線程鎖只在同一JVM中有效果,因爲線程鎖的實現在根本上是依靠線程之間共享內存實現的,比如synchronized是共享對象頭,顯示鎖Lock是共享某個變量(state)。

2.進程鎖

爲了控制同一操作系統中多個進程訪問某個共享資源,因爲進程具有獨立性,各個進程無法訪問其他進程的資源,因此無法通過synchronized等線程鎖實現進程鎖。

3.分佈式鎖

當多個進程不在同一個系統中,用分佈式鎖控制多個進程對資源的訪問。

02.分佈式鎖的要求


首先,爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:

1、互斥性:任意時刻,只能有一個客戶端獲取鎖,不能同時有兩個客戶端獲取到鎖。

2、安全性:鎖只能被持有該鎖的客戶端刪除,不能由其它客戶端刪除。

3、死鎖:獲取鎖的客戶端因爲某些原因(如down機等)而未能釋放鎖,其它客戶端再也無法獲取到該鎖。

4、容錯:當部分節點(redis節點等)down機時,客戶端仍然能夠獲取鎖和釋放鎖。

03.分佈式鎖的應用場景


在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLcok或synchronized)進行互斥控制。

但是在分佈式系統後,由於分佈式系統多線程、多進程並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,爲了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖的來源。

分佈式鎖應用場景大都是用在高併發,大流量場景。當多個進程不在同一個系統中,就需要用分佈式鎖控制多個進程對資源的訪問。

04.分佈式鎖的實現


分佈式鎖一般有三種實現方式:

1. 數據庫樂觀鎖;

2. 基於ZooKeeper的分佈式鎖;

3.基於Redis的分佈式鎖;

文章末尾有數據庫和zookeeper的詳細實現方案,這裏主要談redis的實現。

05.Redis實現分佈式鎖

基於Redis命令:SET key value NX EX max-lock-time

這裏補充下: 從2.6.12版本後, 就可以使用set來獲取鎖, Lua 腳本來釋放鎖。setnx是老黃曆了,set命令nx,xx等參數, 是爲了實現 setnx 的功能。

1.加鎖

public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 嘗試獲取分佈式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標識
* @param expireTime 超期時間
* @return 是否獲取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
        return true;}return false;}
}

jedis.set(String key, String value, String nxxx, String expx, int time)

這個set()方法一共有五個形參:

第一個爲key,我們使用key來當鎖,因爲key是唯一的。

第二個爲value,我們傳的是requestId,很多童鞋可能不明白,有key作爲鎖不就夠了嗎,爲什麼還要用到value?原因就是我們在上面講到可靠性時,分佈式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值爲requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用UUID.randomUUID().toString()方法生成。

第三個爲nxxx,這個參數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作;

第四個爲expx,這個參數我們傳的是PX,意思是我們要給這個key加一個過期的設置,具體時間由第五個參數決定。

第五個爲time,與第四個參數相呼應,代表key的過期時間。

2.解鎖

public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 釋放分佈式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標識
* @return 是否釋放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {return true;}return false;}
}


那麼這段Lua代碼的功能是什麼呢?其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那麼爲什麼要使用Lua語言來實現呢,留給大家討論探索。

06.其他分佈式鎖實現方案


還可以使用Redission(Redis 的客戶端)集成實現分佈式鎖,也可以使用數據庫、zookeeper等來實現,具體可以參考(官網搜索):

阿里P8架構師談:分佈式鎖的3種實現(數據庫、緩存、Zookeeper)。


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