前言:本文介紹了一種基於redis的分佈式鎖,利用jedis實現應用(本文應用於多客戶端+一個redis的架構,並未考慮在redis爲主從架構時的狀況)java
文章理論來源部分引自:https://i.cnblogs.com/EditPosts.aspx?opt=1redis
1、基本原理緩存
一、用一個狀態值表示鎖,對鎖的佔用和釋放經過狀態值來標識。架構
二、redis採用單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,多客戶端對Redis的鏈接並不存在競爭關係。併發
2、基本命令app
一、setNX(SET if Not eXists)分佈式
語法:ui
SETNX key value
將 key 的值設爲 value ,當且僅當 key 不存在。spa
若給定的 key 已經存在,則 SETNX 不作任何動做。線程
SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫
返回值:
設置成功,返回 1 。
設置失敗,返回 0
二、getSet
GETSET key value
將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。
當 key 存在但不是字符串類型時,返回一個錯誤。
返回值:
返回給定 key 的舊值。
當 key 沒有舊值時,也便是, key 不存在時,返回 nil 。
三、get
GET key
當 key 不存在時,返回 nil ,不然,返回 key 的值。
若是 key 不是字符串類型,那麼返回一個錯誤
3、取鎖、解鎖以及示例代碼:
/** * @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);
4、一些問題
一、爲何不直接使用expire設置超時時間,而將時間的毫秒數其做爲value放在redis中?
以下面的方式,把超時的交給redis處理:
lock(key, expireSec){ isSuccess = setnx key if (isSuccess) expire key expireSec }
這種方式貌似沒什麼問題,可是假如在setnx後,redis崩潰了,expire就沒有執行,結果就是死鎖了。鎖永遠不會超時。
二、爲何前面的鎖已經超時了,還要用getSet去設置新的時間戳的時間獲取舊的值,而後和外面的判斷超時時間的時間戳比較呢?
由於是分佈式的環境下,能夠在前一個鎖失效的時候,有兩個進程進入到鎖超時的判斷。如:
C0超時了,還持有鎖,C1/C2同時請求進入了方法裏面
C1/C2獲取到了C0的超時時間
C1使用getSet方法
C2也執行了getSet方法
假如咱們不加 oldValueStr.equals(currentValueStr) 的判斷,將會C1/C2都將得到鎖,加了以後,能保證C1和C2只能一個能得到鎖,一個只能繼續等待。
注意:這裏可能致使超時時間不是其本來的超時時間,C1的超時時間可能被C2覆蓋了,可是他們相差的毫秒及其小,這裏忽略了
5、不完善之處
一、使用時須要預估業務邏輯處理時間,一旦業務邏輯發生錯誤,那麼只能等到超時以後其餘線程才能拿到鎖,可能會出現問題