Redis分布式鎖的實(shí)現(xiàn)

分布式鎖一般有三種實(shí)現(xiàn)方式:1. 數(shù)據(jù)庫(kù)樂觀鎖;2. 基于Redis的分布式鎖;3. 基于ZooKeeper的分布式鎖。本文將基于Redis實(shí)現(xiàn)分布式鎖。

可靠性

為了確保分布式鎖可用,我們至少要確保鎖的實(shí)現(xiàn)同時(shí)滿足以下四個(gè)條件:

  1. 互斥性。在任意時(shí)刻,只有一個(gè)客戶端能持有鎖。
  2. 不會(huì)發(fā)生死鎖。即使有一個(gè)客戶端在持有鎖的期間崩潰而沒有主動(dòng)解鎖,也能保證后續(xù)其他客戶端能加鎖。
  3. 具有容錯(cuò)性。只要大部分的Redis節(jié)點(diǎn)正常運(yùn)行,客戶端就可以加鎖和解鎖。
  4. 加鎖和解鎖必須是同一個(gè)客戶端,客戶端自己不能把別人加的鎖給解了。

代碼實(shí)現(xiàn)

組件依賴

引入Jedis開源組件

<dependency>

<groupId>redis.clients</groupId>

<artifactId>jedis</artifactId>

<version>2.9.0</version>

</dependency>
加鎖代碼
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 請(qǐng)求標(biāo)識(shí)

* @param expireTime 超期時(shí)間

* @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),這個(gè)set()方法一共有五個(gè)形參:

  1. 第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。
  2. 第二個(gè)為value,我們傳的是requestId,有key作為鎖不就夠了嗎,為什么還要用到value?原因就是我們?cè)谏厦嬷v到可靠性時(shí),分布式鎖要滿足第四個(gè)條件解鈴還須系鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)。requestId可以使用UUID.randomUUID().toString()等方法生成。
  3. 第三個(gè)為nxxx,這個(gè)參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時(shí),我們進(jìn)行set操作;若key已經(jīng)存在,則不做任何操作。
  4. 第四個(gè)為expx,這個(gè)參數(shù)我們傳的是PX,意思是我們要給這個(gè)key加一個(gè)過期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定。
  5. 第五個(gè)為time,與第四個(gè)參數(shù)相呼應(yīng),代表key的過期時(shí)間。

執(zhí)行上面的set()方法就只會(huì)導(dǎo)致兩種結(jié)果:1. 當(dāng)前沒有鎖(key不存在),那么就進(jìn)行加鎖操作,并對(duì)鎖設(shè)置個(gè)有效期,同時(shí)value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。

加鎖代碼滿足我們可靠性里描述的三個(gè)條件。首先,set()加入了NX參數(shù),可以保證如果已有key存在,則函數(shù)不會(huì)調(diào)用成功,也就是只有一個(gè)客戶端能持有鎖,滿足互斥性。其次,由于我們對(duì)鎖設(shè)置了過期時(shí)間,即使鎖的持有者后續(xù)發(fā)生崩潰而沒有解鎖,鎖也會(huì)因?yàn)榈搅诉^期時(shí)間而自動(dòng)解鎖(即key被刪除),不會(huì)發(fā)生死鎖。最后,因?yàn)槲覀儗alue賦值為requestId,代表加鎖的客戶端請(qǐng)求標(biāo)識(shí),那么在客戶端在解鎖的時(shí)候就可以進(jìn)行校驗(yàn)是否是同一個(gè)客戶端。由于只考慮Redis單機(jī)部署的場(chǎng)景,所以容錯(cuò)性暫不考慮。

解鎖代碼
public class RedisTool {

private static final Long RELEASE_SUCCESS = 1L;

/**

* 釋放分布式鎖

* @param jedis Redis客戶端

* @param lockKey 鎖

* @param requestId 請(qǐng)求標(biāo)識(shí)

* @return 是否釋放成功

*/

public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

String = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

Object result = jedis.eval(, Collections.singletonList(lockKey), Collections.singletonList(requestId));

if (RELEASE_SUCCESS.equals(result)) {

return true;

}

return false;

}

}

可以看到,我們解鎖只需要兩行代碼就搞定了!第一行代碼,我們寫了一個(gè)簡(jiǎn)單的Lua腳本代碼。第二行代碼,我們將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。

那么這段Lua代碼的功能是什么呢?其實(shí)很簡(jiǎn)單,首先獲取鎖對(duì)應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那么為什么要使用Lua語(yǔ)言來(lái)實(shí)現(xiàn)呢?因?yàn)橐_保上述操作是原子性的。Redis eval()方法執(zhí)行Lua代碼的時(shí)候,Lua代碼將被當(dāng)成一個(gè)命令去執(zhí)行,并且直到eval命令執(zhí)行完成,Redis才會(huì)執(zhí)行其他命令。

下面代碼與正確代碼差不多,唯一區(qū)別的是分成兩條命令去執(zhí)行,代碼如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {

// 判斷加鎖與解鎖是不是同一個(gè)客戶端

if (requestId.equals(jedis.get(lockKey))) {

// 若在此時(shí),這把鎖突然不是這個(gè)客戶端的,則會(huì)誤解鎖

jedis.del(lockKey);

}

}

如代碼注釋,問題在于如果調(diào)用jedis.del()方法的時(shí)候,這把鎖已經(jīng)不屬于當(dāng)前客戶端的時(shí)候會(huì)解除他人加的鎖。那么是否真的有這種場(chǎng)景?答案是肯定的,比如客戶端A加鎖,一段時(shí)間之后客戶端A解鎖,在執(zhí)行jedis.del()之前,鎖突然過期了,此時(shí)客戶端B嘗試加鎖成功,然后客戶端A再執(zhí)行del()方法,則將客戶端B的鎖給解除了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容