基於redis的一種分布式鎖
前言:本文介紹了一種基於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的一種分布式鎖