­

從單機定時到多層分發

在工作中基本上都會使用定時任務,常用的有 Spring 定時框架、Quartz、elastic-job、xxl-job 等。這裡說不上框架的好壞,只有適合自己的才是最好的,本文僅從個人角度上談一談對定時任務的看法。

單機定時

單機定時我這裡分為純單機版固定 IP 版分散式鎖版單機調度版,下面從這四個角度來談一談他們的實現方式以及當時所在的背景。

純單機版

顧名思義,就是應用都是單體應用,不存在集群,寫一個定時任務就可以了,可以是執行緒定時調度、也可以是 Spring 定時框架用 @Scheduled註解實現。這種方式在單體應用的極為合適,主要是簡單方便。

當然也存在他的弊端,那就是如果我的應用是多機部署的,那就會導致並發衝突。出現問題,解決問題,所以下面三種方式應運而生。

固定 IP 版

就是如果我知道了機器的 IP 地址,並且基本上 IP 地址也不會變化,我只需要在程式碼中寫一個判斷邏輯,這樣 IP 地址不是當前機器的應用,並不會執行定時任務。

大概邏輯如下:

@Component
public class ScheduledTask {

    @Scheduled(cron="0 0 * * * ? *")
    public void execute() {
        // 獲取當前機器的 IP 地址
        // 比較配置的 IP 地址和當前機器的 IP 地址是否相同
        // 不相同直接返回
        // 相同則繼續執行定時任務
    }

}

這種方式可以很完美的避免多台機器同時執行定時任務,也可以稍微進階一下,就是將指定的 IP 地址用 @Value 註解,然後可以在配置中心比如 Apollo 進行動態修改。

分散式鎖版

這種和上面的方式區別不大,只是在中間嘗試獲取分散式鎖,不過需要對分散式鎖的時間把握好,一般問題不大,如果一天一次的定時任務,在 Redis 鎖它個一天都可以,總不能定時任務也執行一天。當然幾分鐘一次的也一個意思,合理安排鎖的時間就行。在資料庫寫個標識也可以,都是大同小異。

@Component
public class ScheduledTask {

    @Scheduled(cron="0 0 0 * * ? *")
    public void execute() {
        // 嘗試獲取分散式鎖
        // 獲取鎖失敗,說明別的機器在執行定時任務,直接返回
        // 獲取鎖成功,在本機執行定時任務
    }

}

單機調度版

這種方式也很容易理解,定時執行的任務,也是一個介面,我定時去調度一下這個介面就行了。

這種方式是完全可以的,定時系統用 Spring 定時框架定時執行,定時系統是單機的,不存在並發,調度到業務系統,可以使用 Dubbo,這裡只會有一台機器被調度到。

至於說重複調度了這種極端情況那就另說,不過像查單、補單這種基本不會有啥問題,做個冪等就行。

這種情況也存在弊端,就是定時系統是單機的,如果他掛了怎麼辦?不用怕,技術還可以繼續演進!

分散式調度中間件

單機執行版

分散式調度中間件,我相對熟悉一些的就是 xxl-job。圖和上面定是系統調度版本區別不大,一般常用的就是將調度任務發到一台機器來執行。基本上使用的都是這種方式,能解決大部分的場景,但是依然存在問題,畢竟咱們的主題是多層分發。

@Component
@JobHandler("demoJob")
@Slf4j
public class DemoJob extends IJobHandler {


    @Override
    public ReturnT<String> execute(String param) throws Exception {

        log.info("XXJob 收到調度 ...");

        try {
        } catch (Exception e) {
            log.info("XXJob 調度異常:", e);
            return FAIL;
        }
        return SUCCESS;
    }
}

如果我是定時查單,並且 TPS 不是很高的情況下,問題不大,畢竟每分鐘改的單量,定時還是可以查的過來的。但是如果換成基金髮息或者賬務對賬那就大不一樣了。

因為單機執行定時調度,會花費很久,像基金需要知道昨日金額等等,賬務需要對用戶交易計算。結果就是可能一個定時任務執行四五個小時,當然一天也有可能。四五個小時還好,畢竟我今天能出結果,如果一天,我今天的還沒算完,明天的交易又來了,並且這個時效性也太差了。

所以就用到了 xxl-job 的分片廣播 & 動態分片功能。

分片廣播

在分片廣播場景下,xxl-job 會對當前定時中所有註冊的應用發起調度。

按照文檔可以使用下面的方式獲取當前機器的 shardIndex 和 shardTotal。
👉🏻 文檔地址

// 可參考Sample示例執行器中的示例任務"ShardingJobHandler"了解試用 
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();

有人問這種有什麼用呢?可以想一下,本來是由單機執行的定時任務,現在變成集群每台機器來執行一部分,這不是充分利用了集群的特徵了么?

具體一點可以是:

  1. 按照 user_id 取模,然後每台機器只執行某些特定用戶的定時統計
  2. 分庫分表場景下,一台機器執行一個庫的數據統計

多層分發

就像面試題肯定會層層剖析,這時候肯定會問如果發生數據傾斜了怎麼辦?

具體現象就是如果按照用戶來分配機器,取模等於 0 的用戶在 shardIndex0 上執行定時,但是這些用戶的總交易量佔據了 90% 以上,那就會導致另幾台的定時咔咔咔一會執行完了,這太機器還在吭哧吭哧的干。那不就沒啥用了么?

上圖只是分發了三層:

  1. 第一層僅有一台機器收到調度,然後獲取所有任務,可以是多少個庫,也可以是有多少數據,然後發起 RPC 調用本集群的介面,說你們每次執行這些
  2. 第二層收到調度,再按照其他維度再分割一次,比如第一次按照用戶來分的,第二次則查出來訂單,按照訂單再分,然後再發送 RPC 調用集群執行介面
  3. 第三層收到被執行的訂單,開始執行具體的任務

理論上是可以多層分發的,最終結果就是讓每台機器均勻的執行定時任務,這樣可以充分利用每台機器的能力。

總結

其實這個問題是我曾經遇到的面試題,當時還和群友討論了很久。在實際工作中,這幾種也並沒有好壞之差,只要適合自己,就夠了。