redis實現分佈式鎖的迭代演進

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") + "】===============================");
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章