Job輪詢總結與思考

 

背景

在業務數據沒達到一定量又不想引入分佈式事務框架增加複雜性,基於Job框架實現的補償方案不失爲一種簡單優雅的方案。

微服務環境下雖然使用了retry框架,對一些冪等的接口一次失敗多次嘗試,但有些場景比如下單後無庫存,要保證庫存在一定時間內一定能扣成功,也只能使用Job框架以一定的頻率發起補償。

業界常用的分佈式Job框架有 Saturn與 xxl-job

 

Job併發

像Saturn跟xxl-job都有控制檯頁面把Job配置爲單機執行,單機發生故障進行故障轉移。

數據量少的場景只需要單機執行,不需要考慮併發。

但單機執行場景下仍然要注意併發問題。

一般來說Job框架會保證下一個Job等待上一個Job執行完畢纔開始。

如果業務邏輯使用了線程異步執行,如下

 void executeJob() {
      new Thread(()->{
         //耗時的業務處理
      }).start();
}

由於是在線程裏面異步執行,Job就當做上一個Job已經處理完畢了,毫不猶豫地就開啓下一輪輪詢,就有可能發生兩個線程併發處理同一段數據。

爲了避免耗時業務處理長時間阻塞Job,啓用線程異步處理也是必要的,同時要控制兩個Job的輪詢間隔,避免當前Job輪詢啓動了,上一個Job異步線程裏面還有未完成業務處理邏輯。

 

Job查詢必須有結束條件

使用Job自然是要查詢那些需要補償的任務。

通常使用分頁查詢,把待補償的業務數據查詢出來處理,直到處理完畢結束循環。

查詢分頁避免了一次性查詢出過量數據造成OutOfMemoryError。

//第一種場景:分頁查詢不知道數據總數
public void executeJob() {
    int beginPage = 0;
    int pageSize = 500;
    while (true) {
        //根據beginPage進行分頁查詢
        List<Object> orders = getOrders(beginPage, pageSize);
        //處理業務數據 orders
        if (orders.size() < pageSize) {
            break;
        }
        beginPage = beginPage + 1;
    }
}

//第二種場景:分頁查詢知道數據總數
public void executeJob() {
        int beginPage = 0;
        int pageSize = 10;
        int total = 0;
        int allTotal = 0;
        while (total<allTotal) {
            //根據beginPage進行分頁查詢
            PageInfo<Object> orders = getOrders(beginPage, pageSize);
            //處理業務數據 orders
            allTotal = orders.getTotal();
            total = total + orders.size();
            if (orders.size() < pageSize) {
                break;
            }
            beginPage = beginPage + 1;
        }
    }

while要有一種業務無關的結束條件

 

雖然分頁查詢有結束條件,但查詢邏輯是會隨着需求更新的,當查詢條件不再滿足結束條件就會陷入死循環。

由於Job是純後臺服務,測試人員一時也不易發現Job邏輯出了問題。

例如 getOrders(beginPage, pageSize) 隨着分頁插件的升級或Sql的更新每次總是查詢出相同的500條數據。

500條相同的數據不多也不少,也不易發現查詢結果出錯,如下判斷條件永遠不滿足,第一種場景:分頁查詢不知道數據總數就沒有結束條件。

       if (orders.size() < pageSize) {
            break;
        }

來看第二種場景:分頁查詢知道數據總數 ,多了一個結束條件 total<allTotal ,那多了一個結束條件就靠譜嗎?

雖然把每次查詢出的總數累加 了total = total + orders.size() ,但分頁插件或Sql通過 select count(*) from table where object = xxxx  統計出的 allTotal  也可能在變化,每次都變大,正所謂你長我也長。可能半天過去了 total<allTotal 條件仍然沒有滿足,while也陷入死循環。

由於根據sql查詢條件去結束while有一定的不確定性,通過限制最大循環是一種簡單有效的方法。

例如根據業務量,while循環不應該超過1千次,數據總量不應該超過50萬。

那麼保留根據分頁查詢結果判斷結束條件的同時,應該增加最大循環次數、最大業務量控制,避免由於查詢失效造成的死循環。

public void executeJob() {
        int beginPage = 0;
        while (true) {
            //分頁查詢
            if (orders.size() < pageSize) {
                break;
            }
            beginPage = beginPage + 1;
            if(beginPage>1000){
                break;
            }
        }
    }

    public void executeJob() {
        int beginPage = 0;
        while (total<500000) {
            //分頁查詢
            total = total + orders.size();
            if (orders.size() < pageSize) {
                break;
            }
            beginPage = beginPage + 1;
            if(beginPage>1000){
                break;
            }
        }
    }

Job也應該像微服務一樣優雅關閉

在微服務發佈過程中,通常把舊服務從Eureka等註冊中心以及負載平衡裏面摘掉,不再接受新的請求,舊服務把已有請求處理完畢下線。由於Job是純後臺服務,有可能被忽略掉。例如舊服務已經不接受新的Http請求,但後臺的Job輪詢還沒處理完畢,Job裏面同時開啓了數據庫事務,這時候強殺進程會造成數據回滾。

Job也應該同理,像微服務一樣,通過Http接口向分佈式Job註冊中心發送優雅下線的命令,待當前微服務已經沒有未完成的Job任務再停機。

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