1. redis分佈式鎖主要是由以下4個命令實現的:
a. setnx:是set if not exists的縮寫,也就是當該key在redis中不存在的時候才放入redis中,這個步驟分爲兩步:首先判斷該key是否存在,若不存在,則放入;這兩步操作是個原子操作,所以這個命令能夠實現鎖的效果;
b. getset:這個命令先根據key執行get操作,再執行set操作,這個命令的作用是:先獲取原來的舊值,再設置一個新值;這個命令也具有原子性;
c. expire:設置鍵的有效期;
d. del:根據key,刪除對應的value;
2. Redis分佈式鎖流程圖--基礎版(不防死鎖)
這個版本的redis分佈式鎖是有缺陷的,考慮一個這樣的應用場景:
當業務量增加到一定程度後,一臺服務器無法應付這麼大的業務量了,需要再啓動一臺服務器,用兩臺服務器同時運行;假設我們使用tomcat運行項目的war包,用nginx做負載均衡;這兩個war包裏都有定時任務,根據業務需要,只能在一臺服務器上執行定時任務,這時候就用到redis分佈式鎖了。即在同一時刻,哪個tomcat獲取到redis鎖,哪個tomcat就執行定時任務,另一個tomcat則提前執行結束。假設tomcat-A在執行完setnx後,獲取到redis鎖,在即將執行expire(lockkey)時,tomcat-A突然宕機了,那麼後果就是該lockkey在redis中將永久存在;在下一個時間點,要執行該定時任務時,任何一個tomcat都無法執行,因爲這個lockkey在redis中永久存在了。
3 Redis分佈式鎖流程圖--優化版(雙重防死鎖)
上述圖片對應代碼如下:
優化版本的redis分佈式鎖與基礎版的redis分佈式鎖相比較而言,lockKey對應的value是currentTime + timeOut--當前時間戳 + 超時時間(這個超時時間要比定時任務執行的時間略長)
假設我們用5個tomcat才能支撐現在的業務量,每個tomcat上運行的功能代碼都是一樣的,每個tomcat都可以運行這個定時任務,但同一時間點只能有一個tomcat執行該定時任務;
//每分鐘執行一次
@Scheduled(cron="0 */1 * * * ?")
public static void borrowcash() throws InterruptedException {
logger.info("當前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】,執行定時任務");
//lockTimeout:這個時間要比定時任務執行時間要長一些,因爲在setnx命令後,若是沒有獲取到鎖,
//那麼在第一個else分支下,我們會判斷這個lockTimeout是否超時,若是超時了,那麼就認爲這個鎖失效了
long lockTimeout = 50l;
Long setnxResult = RedisShardedPoolUtil.setnx(BORROWCASH_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
if(setnxResult != null && setnxResult.intValue() == 1){
/*同一時間點只有一個tomcat能獲取到鎖,從而進入到這裏---執行業務邏輯,其餘4個tomcat只能走else分支*/
sendMsgToUps(BORROWCASH_LOCK);
}else{
/*其餘未獲取到鎖的4個tomcat都執行這個else分支下的代碼*/
//這4個tomcat先根據key取出舊值,然後根據當前時間戳,看是否可以重置並獲取到鎖
String oldLockValue = RedisShardedPoolUtil.get(BORROWCASH_LOCK);
if(lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)){
//能進入到這裏,說明這把鎖已經失效了,既然失效了,這個鎖爲什麼還沒有被釋放掉:
//a. 其中一個有可能的原因是:獲取鎖的tomcat正在執行該定時任務,實際完成定時任務所用的時間比預設的lockTimeout的值要長;
//這裏使用getset命令重新設置key的時間戳,並獲取到舊值
/*兩個疑問:
* 1. 爲什麼使用getset命令,而不是使用兩個命令get--獲取舊值和set命令--設置新值?
* 因爲getset命令在執行的過程中是個原子操作,而get和set這兩個命令連在一起使用時,不是原子操作。
* 2. 不使用getset命令,而使用get命令,會出現什麼後果?
* 同一時刻,只有一個tomcat獲取到鎖,那麼其餘4個tomcat都會執行get操作,它們
* 獲取到的值是一樣的,接下來如果滿足這個條件
* (newLockValue!= null && StringUtils.equals(oldLockValue ,newLockValue)),
* 那麼這4個tomcat將都會執行該定時任務,這樣一來,就失去了分佈式鎖的意義;如果不
* 滿足這個條件,這4個tomcat都不執行該定時任務,而獲取到鎖的tomcat在執行完setnx
* 後,突然宕機了,那麼此時,該定時任務在這個時間點將不會被執行,即5個tomcat在這
* 一時間點都不會執行該定時任務;在下一時間點,這5個tomcat都會執行該定時任務;
* 爲了防止這種情況的發生,所以使用getset命令設置新值,而不使用get命令只獲取舊值;
*/
String newLockValue= RedisShardedPoolUtil.getSet(BORROWCASH_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
if(newLockValue!= null && StringUtils.equals(oldLockValue ,newLockValue)){
//真正獲取到鎖
/*
* 當這個條件newLockValue!= null && StringUtils.equals(oldLockValue ,newLockValue)滿足時,即新值和舊值相等,
* 則說明這個鎖並沒有被其它的tomcat操作,那麼此時就可以執行該定時任務了;
* 假設獲取到鎖的tomcat剛執行完setnx後,就宕機了,那麼其餘的4個tomcat就算同時
* 執行getset命令,也沒關係,因爲redis是單線程的,這4個tomcat的請求會串行執
* 行;總有一個tomcat獲取到的新值和舊值相等,那麼該tomcat就會執行該定時任務;
*/
sendMsgToUps(BORROWCASH_LOCK);
}else{
System.out.println("當前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】沒有獲取到分佈式鎖 = 【" + BORROWCASH_LOCK + "】");
}
}else{
//a. 當舊值不存在時(要麼是key過期了--redis自動將該key刪除了,要麼是定時任務執行完了,執行定時任務的tomcat使用del命令把該key給刪除了),這時候獲取到鎖的tomcat很可能已經執行完該定時任務了,此時執行到這裏的tomcat就執行結束了;
//b. 當舊值存在,且當前時間戳小於舊值時,說明該鎖還在有效期內,這時候獲取到鎖的tomcat很可能正在執行該定時任務,此時執行到這裏的tomcat也執行結束了;
System.out.println("當前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】沒有獲取到分佈式鎖 = 【" + BORROWCASH_LOCK + "】");
}
}
System.out.println("給用戶打款結束");
}
private static void sendMsgToUps(String lockName) throws InterruptedException {
RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死鎖
System.out.println("當前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】獲取 = 【" + BORROWCASH_LOCK + "】");
//模擬處理業務所花的時間 2019-06-06
Thread.sleep(3000);
RedisShardedPoolUtil.del(BORROWCASH_LOCK);
System.out.println("當前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】釋放 = 【" + BORROWCASH_LOCK + "】");
System.out.println("當前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】===============================");
}