首先,為了確保分布式鎖可用,至少要確保鎖的實現同時滿足以下四個條件:
1.互斥性。在任意時刻,只有一個客戶端能持有鎖。
2.不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。
3.具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
4.解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
====加鎖代碼====
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()方法就只會導致兩種結果:1. 當前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設置個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。
我們的加鎖代碼滿足我們可靠性里描述的三個條件。首先,set()加入了NX參數,可以保證如果已有key存在,則函數不會調用成功,也就是只有一個客戶端能持有鎖,滿足互斥性。其次,由于我們對鎖設置了過期時間,即使鎖的持有者后續發生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖(即key被刪除),不會發生死鎖。最后,因為我們將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代碼傳到jedis.eval()方法里,并使參數KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務端執行。
那么這段Lua代碼的功能是什么呢?其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那么為什么要使用Lua語言來實現呢?因為要確保上述操作是原子性的。那么為什么執行eval()方法可以確保原子性,源于Redis的特性,簡單來說,就是在eval命令執行Lua代碼的時候,Lua代碼將被當成一個命令去執行,并且直到eval命令執行完成,Redis才會執行其他命令。
問題:怎么保證鎖時間大于業務執行時間?
redisson里面有一個看門狗的機制,其實可以根據這個思路來,加鎖成功后,啟動一條守護線程,守護線程給鎖進行無限續期!當鎖不存在的時候就跳過,存在就續期,可以保證鎖的時間大于業務時間!線程為守護線程的原因是,守護線程依賴于主線程,當主線程掛了之后,守護線程也會掛掉!這樣能避免程序宕機之后,續期的線程依舊續期,造成死鎖!
如果你的項目中Redis是多機部署的,那么可以嘗試使用Redisson實現分布式鎖,這是Redis官方提供的Java組件。