Java | Redis 分布式鎖的正確實現(xiàn)方式

說明

使用第三方開源組件Jedis實現(xiàn)Redis客戶端,且只考慮Redis服務(wù)端單機部署的場景

前言

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

可靠性

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

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

代碼實現(xiàn)

Maven依賴

<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 請求標識
     * @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來當(dāng)鎖,因為key是唯一的。
  • 第二個為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?原因就是我們在上面講到可靠性時,分布式鎖要滿足第四個條件解鈴還須系鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據(jù)。requestId可以使用UUID.randomUUID().toString()方法生成。
  • 第三個為nxxx,這個參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時,我們進行set操作;若key已經(jīng)存在,則不做任何操作;
  • 第四個為expx,這個參數(shù)我們傳的是PX,意思是我們要給這個key加一個過期的設(shè)置,具體時間由第五個參數(shù)決定。
  • 第五個為time,與第四個參數(shù)相呼應(yīng),代表key的過期時間。
    總的來說,執(zhí)行上面的set()方法就只會導(dǎo)致兩種結(jié)果:1. 當(dāng)前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設(shè)置個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。

心細的童鞋就會發(fā)現(xiàn)了,我們的加鎖代碼滿足我們可靠性里描述的三個條件。首先,set()加入了NX參數(shù),可以保證如果已有key存在,則函數(shù)不會調(diào)用成功,也就是只有一個客戶端能持有鎖,滿足互斥性。其次,由于我們對鎖設(shè)置了過期時間,即使鎖的持有者后續(xù)發(fā)生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖(即key被刪除),不會發(fā)生死鎖。最后,因為我們將value賦值為requestId,代表加鎖的客戶端請求標識,那么在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端。由于我們只考慮Redis單機部署的場景,所以容錯性我們暫不考慮。

解鎖代碼

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腳本代碼。第二行代碼,我們將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。

那么這段Lua代碼的功能是什么呢?其實很簡單,首先獲取鎖對應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那么為什么要使用Lua語言來實現(xiàn)呢?因為要確保上述操作是原子性的。

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

相關(guān)閱讀更多精彩內(nèi)容

  • 我們的涂鴉都是提前安排好的 今日兩次是臨時安排的 ^0^ 中午: 老板的廚藝讓我非常滿意 哈哈 吃完肥鍋肉 “向弟...
    669f9b0cf9d2閱讀 216評論 0 0
  • 天氣雷陣雨 今天的心情就好像今天的天氣一樣,時好時壞。今天看了電視劇《我的前半生》就好像廣播說的一樣這部劇確實不...
    soffel閱讀 215評論 0 0
  • 境界是情操的驛站,行為是情操的具象。由此,我不禁想起濟寧一中(原濟寧二中)的莊啟文老師。如今,莊老師已是耄耋之年。
    無忌西東7閱讀 484評論 0 10
  • 最近看知乎,有很多關(guān)于程序員35歲之后的討論,其中有一個回答是大齡程序員大部分人遇到的情況: 1、明顯感覺腦力跟不...
    全棧媛閱讀 11,473評論 0 0

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