分佈式鎖主動續期的入門級實現-自省 | 簡約而不簡單

一、背景

如果某個客戶端獲得鎖之後處理時間超過最大約定時間,或者持鎖期間內發生了故障導致無法主動釋放鎖,其持有的鎖也能夠被其他機制正確釋放,並保證後續其它客戶端也能加鎖,整個處理流程繼續正常執行。

簡單解釋一下:

  1. 客戶端搶到分佈式鎖之後開始執行任務,執行完畢後再釋放分佈式鎖。
  2. 持鎖後因客戶端異常未能把鎖釋放,會導致鎖成爲永恆鎖。
  3. 爲了避免這種情況,在創建鎖的時候給鎖指定一個過期時間。
  4. 到期之後鎖會被自動刪除掉,這個角度看是對鎖資源的一種保護。

二、理還亂?

邏輯看很簡單,也很清晰,但任何事情都有兩面性,自動刪除自然有理,但肯定也有弊端。如果要把鎖的功能做的健壯,總要從不斷地自我質疑、自我反思中,理順思路,尋找答案,我認爲這屬於自省式學習,以後也想嘗試這種模式,一起來試試吧:

  • 問題:鎖過期了會被刪掉,可是任務沒結束怎麼辦?

    如果鎖被釋放的時候,任務尚未執行完畢,那就可能導致其它客戶端又搶到鎖,任務被重複執行。

  • 問題:把鎖的過期時間定的長一點?

    邏輯聽起來沒錯,如果你能確定任務的最大耗時,那沒問題;大部分情況都很難確定任務的最大耗時該是多少。

  • 問題:鎖的過期時間定多長合適?

    反正會被釋放,過期時間定的足夠長吧;如果鎖使用的頻率很高,加了鎖程序有bug釋放不掉,服務端豈不是要出現大量的垃圾數據?思來想去,對一個健壯的分佈式鎖來說,過期時間設置太長了不合適,設置太短了也不合適。

  • 問題:怎麼平衡?

    不長不短,主動延期!持鎖期間,酌情推後鎖的過期時間,以基於Redis的分佈式鎖來說,就需要調用 API 重置鎖 key 的過期時間。當前線程持鎖後在執行任務期間不能再調用 API 重試鎖 key 的過期時間。

  • 問題:誰來調用API呢?

    需要使用其他的線程來執行續期。

  • 問題:給每個鎖配一個線程?

    可以,如果使用分佈式鎖的場景中沒有什麼併發,一個客戶端也就那麼三兩個鎖同時存在,那就沒問題。每個鎖搶鎖成功後,開啓一個線程,在線程中通過循環給鎖續期。

    public void run() {
        while (true) {
            // 續租
            action.run();
        }
    }
    
  • 問題:多久執行一次續期?

    有一些常規處理是續租間隔默認採用過期時間的1/3。若把鎖的過期時間設定爲與實際耗時相差不大,這樣通過一兩次續租基本就滿足了大部分的情況。

  • 問題:爲什麼要觸發一次續期操作呢,這不浪費資源嗎?

    採用過期時間1/3間隔,若用戶定義鎖3秒過期,那每秒鐘都有一個續期指令,有沒有覺得也不太合適。

  • 問題:要不要避免續期指令太頻繁?

    避免續期指令太頻繁調用是有必要的,也可以增加一個續期的最小間隔時間,比如最少是5秒。可由用戶自己控制續期週期,沒必要一定要發起續期調用。比如任務執行大多在5秒鐘,那麼就把鎖定爲7秒,續期時間定在6秒,那麼6秒內任務結束了就不用續期,即不必把過期時間定的太長,也不必執行一兩次續期操作。

  • 問題:續租的間隔怎麼實現?

    線程內間隔控制通常是通過 sleep() 方法,稍微精準一點的話,單位使用毫秒。

    public void run() {
        while (true) {
            // 1、間隔
            TimeUnit.MILLISECONDS.sleep(sleepTime);
            // 2、續租
            action.run();
        }
    }
    
  • 問題:線程要關閉吧?

    釋放鎖的時候要主動關閉負責續期的線程,所以線程的循環裏要有一個變量來控制退出 while 循環

    public void run() {
        while (isRunning) {
            // 1、間隔
            TimeUnit.MILLISECONDS.sleep(sleepTime);
            // 2、續租
            action.run();
        }
    }
    
  • 問題:變量是跨線程訪問,如何保證跨線程的可見性呢?

    在變量上增加 volatile 關鍵字。

    private volatile boolean isRunning = true;
    
    void cancel(){
        //控制線程退出
        this.isRunning = true;
    }
    
  • 問題:如果續期線程裏在 sleep(),那就一直等 sleep() 結束?

    如果等到 sleep() 結束,就挺浪費資源的

  • 問題:能不能快速結束 sleep() 狀態?

    可以,通過 interrupt(),需留意,被打斷的時候會拋異常 InterruptedException

    void cancel(){
        //控制線程退出
        this.isRunning = true;
        //中斷線程
        this.interrupt();
    }
    

到這裏,似乎都理順了。

三、新的思考

  • 問題:如果同時有成百上千個鎖呢?

    同時有成百上千個線程在工作,你若認爲沒問題,不存在,那ok,不用繼續看下一篇。

  • 那怎麼辦呢?

    可以用 Executors.newScheduledThreadPool ,裏邊有 scheduleAtFixedRate

  • 阿里 Java 代碼規範不允許用Execurots嘛?

  • 不能用?風險是什麼?你沒看累嘛?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章