從單機定時到多層分發

程式設計師小航發表於2022-04-09

在工作中基本上都會使用定時任務,常用的有 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. 第三層收到被執行的訂單,開始執行具體的任務

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

總結

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

相關文章