1. 程式人生 > >基於redis的一種分布式鎖

基於redis的一種分布式鎖

thread 語法 客戶端 read not end 過程 == 直接

前言:本文介紹了一種基於redis的分布式鎖,利用jedis實現應用(本文應用於多客戶端+一個redis的架構,並未考慮在redis為主從架構時的情況)

文章理論來源部分引自:https://i.cnblogs.com/EditPosts.aspx?opt=1

一、基本原理

1、用一個狀態值表示鎖,對鎖的占用和釋放通過狀態值來標識。

2、redis采用單進程單線程模式,采用隊列模式將並發訪問變成串行訪問,多客戶端對Redis的連接並不存在競爭關系。

二、基本命令

1、setNX(SET if Not eXists)

語法:

SETNX key value

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

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

SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫

返回值:

  設置成功,返回 1 。
  設置失敗,返回 0

2、getSet

GETSET key value

給定 key 的值設為 value ,並返回 key 的舊值(old value)

  當 key 存在但不是字符串類型時,返回一個錯誤。

返回值:

  返回給定 key 的舊值。
  當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。

3、get

GET key

  當 key 不存在時,返回 nil ,否則,返回 key 的值。
  如果 key 不是字符串類型,那麽返回一個錯誤

三、取鎖、解鎖以及示例代碼:

    /**
     * @Description:分布式鎖,通過控制redis中key的過期時間來控制鎖資源的分配
     * 實現思路: 主要是使用了redis 的setnx命令,緩存了鎖.
     * reids緩存的key是鎖的key,所有的共享, value是鎖的到期時間(註意:這裏把過期時間放在value了,沒有時間上設置其超時時間)
     * 執行過程:
     * 1.通過setnx嘗試設置某個key的值,成功(當前沒有這個鎖)則返回,成功獲得鎖
     * 2.鎖已經存在則獲取鎖的到期時間,和當前時間比較,超時的話,則設置新的值
     * 
@param key * @param expireTime 有效時間段長度 * @return */ public boolean getLockKey(String key, final long expireTime) { // 1.setnx(lockkey, 當前時間+過期超時時間) ,如果返回1,則獲取鎖成功;如果返回0則沒有獲取到鎖,轉向2 if (getJedis().setnx(key, new Date().getTime() + expireTime + "") == 1) return true; String oldExpireTime = getJedis().get(key); // 2.get(lockkey)獲取值oldExpireTime // ,並將這個value值與當前的系統時間進行比較,如果小於當前系統時間,則認為這個鎖已經超時,可以允許別的請求重新獲取,轉向3 if (null != oldExpireTime && "" !=oldExpireTime && Long.parseLong(oldExpireTime) < new Date().getTime()) { // 3計算newExpireTime=當前時間+過期超時時間,然後getset(lockkey, newExpireTime) // 會返回當前lockkey的值currentExpireTime。 Long newExpireTime = new Date().getTime() + expireTime; String currentExpireTime = getJedis().getSet(key, newExpireTime + ""); // 4.判斷currentExpireTime與oldExpireTime // 是否相等,如果相等,說明當前getset設置成功,獲取到了鎖。如果不相等,說明這個鎖又被別的請求獲取走了, //那麽當前請求可以直接返回失敗,或者繼續重試。防止java多個線程進入到該方法造成鎖的獲取混亂。 if (!currentExpireTime.equals(oldExpireTime)) { return false; } else { return true; } } else { // 鎖被占用 return false; } } /** * * @Description: 如果業務處理完,key的時間還未到期,那麽通過刪除該key來釋放鎖 * @param key * @param dealTime 處理業務的消耗時間 * @param expireTime 失效時間 */ public void deleteLockKey(String key,long dealTime, final long expireTime) { if (dealTime < expireTime) { getJedis().del(key); } }

示例:

    // 循環等待獲取鎖
            StringBuilder key = new StringBuilder(KEY_PRE);
            key.append(code).append("_");
            key.append(batchNum);
            long lockTime = 0;
            try {
                while (true) {
                    boolean locked = redisCacheClient.getLockKey(
                            key.toString(), 60000);
                    if (locked) {
                        lockTime = System.currentTimeMillis();
                        break;
                    }
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                
            }
    //業務邏輯...
            
    //業務邏輯進行完,解鎖
            long delLockDateTime =System.currentTimeMillis();
            long dealTime = delLockDateTime - lockTime;
            deleteLockKey(key.toString(), dealTime, 60000);

四、一些問題

1、為什麽不直接使用expire設置超時時間,而將時間的毫秒數其作為value放在redis中?

如下面的方式,把超時的交給redis處理:

lock(key, expireSec){
isSuccess = setnx key
if (isSuccess)
expire key expireSec
}

這種方式貌似沒什麽問題,但是假如在setnx後,redis崩潰了,expire就沒有執行,結果就是死鎖了。鎖永遠不會超時。

2、為什麽前面的鎖已經超時了,還要用getSet去設置新的時間戳的時間獲取舊的值,然後和外面的判斷超時時間的時間戳比較呢?

因為是分布式的環境下,可以在前一個鎖失效的時候,有兩個進程進入到鎖超時的判斷。如:

C0超時了,還持有鎖,C1/C2同時請求進入了方法裏面

C1/C2獲取到了C0的超時時間

C1使用getSet方法

C2也執行了getSet方法

假如我們不加 oldValueStr.equals(currentValueStr) 的判斷,將會C1/C2都將獲得鎖,加了之後,能保證C1和C2只能一個能獲得鎖,一個只能繼續等待。

註意:這裏可能導致超時時間不是其原本的超時時間,C1的超時時間可能被C2覆蓋了,但是他們相差的毫秒及其小,這裏忽略了

五、不完善之處

1、使用時需要預估業務邏輯處理時間,一旦業務邏輯發生錯誤,那麽只能等到超時之後其他線程才能拿到鎖,可能會出現問題

基於redis的一種分布式鎖