天天看點

分布式鎖之redis分布式鎖的實作

分布式鎖之redis分布式鎖的實作

一.前言

目前分布式鎖一般有三種實作方式:

  • 資料庫樂觀鎖;
  • 基于Redis的分布式鎖;
  • 基于ZooKeeper的分布式鎖。

本篇部落格将介紹第二種方式,基于Redis實作分布式鎖。雖然網上已經有各種介紹Redis分布式鎖實作的部落格,然而他們的實作卻有着各種各樣的問題,為了避免誤人子弟,本篇部落格将詳細介紹如何正确地實作Redis分布式鎖。

二.可靠性

首先,為了確定分布式鎖可用,我們至少要確定鎖的實作同時滿足以下四個條件:

  • 互斥性。在任意時刻,隻有一個用戶端能持有鎖。
  • 不會發生死鎖。即使有一個用戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他用戶端能加鎖。
  • 具有容錯性。隻要大部分的Redis節點正常運作,用戶端就可以加鎖和解鎖。
  • 解鈴還須系鈴人。加鎖和解鎖必須是同一個用戶端,用戶端自己不能把别人加的鎖給解了。

三.代碼實作

3.1元件依賴

首先我們要通過Maven引入Jedis開源元件,在pom.xml檔案加入下面的代碼:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
           

3.2加鎖代碼

正确姿勢

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的過期時間。

總的來說,執行上面的set()方法就隻會導緻兩種結果:

  • 目前沒有鎖(key不存在),那麼就進行加鎖操作,并對鎖設定個有效期,同時value表示加鎖的用戶端。
  • 已有鎖存在,不做任何操作。

心細的童鞋就會發現了,我們的加鎖代碼滿足我們可靠性裡描述的三個條件:

  • 首先,set()加入了NX參數,可以保證如果已有key存在,則函數不會調用成功,也就是隻有一個用戶端能持有鎖,滿足互斥性。
  • 其次,由于我們對鎖設定了過期時間,即使鎖的持有者後續發生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖(即key被删除),不會發生死鎖。
  • 最後,因為我們将value指派為requestId,代表加鎖的用戶端請求辨別,那麼在用戶端在解鎖的時候就可以進行校驗是否是同一個用戶端。
  • 由于我們隻考慮Redis單機部署的場景,是以容錯性我們暫不考慮。

3.3 解鎖代碼

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代碼傳到jedis.eval()方法裡,并使參數KEYS[1]指派為lockKey,ARGV[1]指派為requestId。eval()方法是将Lua代碼交給Redis服務端執行。

那麼這段Lua代碼的功能是什麼呢?其實很簡單,首先擷取鎖對應的value值,檢查是否與requestId相等,如果相等則删除鎖(解鎖)。那麼為什麼要使用Lua語言來實作呢?因為要確定上述操作是原子性的。

四.總結

繼續閱讀