使用事件驅動代替定時任務

山不在高水不在深發表於2021-01-13

本文的目的是探討一種通過事件觸發來命中資料以便在未來進行處理的方法論。通常這種問題是使用定時任務完成的。所以本文旨在能夠在系統中消除所有定時任務。

一、定時任務的使用

目前的系統設計中,定時任務是被做為很重要的元件存在的。下面我舉兩個場景,做為貫穿本文的例子。

1. 單據明細的彙總

比如某家電商超市的銷售明細,需要及時根據某些條件(地點,渠道,甚至供應商、商品等)對前一日的資料進行分類彙總出總的銷售額。

對於這種場景,使用定時任務幾乎是一種思維定式:選一個凌晨系統比較空閒的時間,通過定時任務排程拉取全部需要彙總的資料進行分類彙總。

2. 合同狀態的自動變更

比如超市和供應商簽署返利合同,合同上面又生效時間欄位和當前狀態欄位。預設狀態是“待生效”,一旦當前時間達到生效時間後,合同狀態變更為“已生效”。

這種場景也具有明顯的按照時間一刀切的特點,所以使用定時任務幾乎也是唯一的選擇。

二、事件驅動的使用

使用定時任務有什麼不好嗎?為什麼這裡我要推薦事件機制呢?

下面我將用更加符合我們思維方式的基於事件觸發的邏輯來重新設計前面例子中的功能。通過不同方式的對比,你應該能體會到他們直接明顯的差別。

1. 單據明細的彙總

前面說過,彙總就是拿到需要處理的資料,根據特定的彙總條件,將資料分組然後壓縮。

這裡面有幾個關鍵資訊:①需要處理的資料,②根據條件分組壓縮。
所以我們先來思考第一個角度:哪些資料是需要處理的。不用想都知道,前一天還沒有彙總的資料就是需要彙總的!其實我想問的不是這個,而是如何拿到這些資料。能拿到才能處理。

很簡單,使用定時任務的話,我們可以通過時間段和是否被處理過的狀態過濾出這批資料。但是如何資料量很大,比如一天有幾千萬,甚至幾十億,想要一次性撈出來然後在記憶體裡面分類彙總怕是有困難。
有人會說了:不怕,處理海量資料我們有經驗:分片處理。使用分片處理的話,我們可以通過一個基礎資料的介面獲取全部正在營業的門店,比如有10萬家,然後將它們按照某種規則分成幾組,各組分別在不同的程式例項中處理(或者同一個程式裡順序處理)。如果是關係型資料庫的話,給門店加一個索引基本就能解決。
然後還有一個問題需要考慮:門店每天的銷售量並不固定,甚至有些門店是一天都沒有銷售量的;而給門店分片的策略是固定的,每天哪個分片是哪些門店都是固定的。這樣導致的問題是不同分片內的資料量差異可能很大,造成的分片計算壓力也不同。

如果不使用定時任務,我們還有以下方案可以選擇,而且我覺得比定時任務要好。

a. 延時彙總

每當一個門店向系統中寫入資料的時候,系統需要判斷當前資料是否是當天的第一次寫入。如果是第一次寫入,則建立一個延時彙總事件(具體的實現可以是定時mq或者定時執行緒池,但是最好不要使用資料庫,否則我們又需要一個定時任務去掃庫);這樣的話,沒有業務量的門店就不會被彙總。
為了降低系統壓力,我們具體的延時時間也可以分列開來,不用所有門店都聚集在一起。

但是使用這種方案,相應的一定要做補償。比如使用Java執行緒池或者akka acotr,如果系統重啟資訊就都丟失了;如果使用mq,訊息也可能消費失敗。所以我們需要監控和告警機制來輔助。

b. 實時彙總

每當資料寫入的時候,都判斷一下該資料低彙總維度,然後寫入彙總記錄。如果彙總記錄已經存在了,就直接更新。

這個過程主要要考慮的問題是併發安全,我們可以使用分散式鎖或者資料庫樂觀鎖,甚至mysql的on duplicate key update(參考[https://www.manxi.info/mysqlDuplicate])。其他問題,比如消峰,可以結合場景判斷。

2. 合同狀態的變更

這種場景使用定時任務的標誌是按某個時間點所有命中某規則的記錄都要被處理。所以我們可以建立一個每天凌晨的定時任務,判斷一下是否有合同在今天需要變成有效(或者標記過期)。但是實際上一年365天可能只有一天這個任務能真正發揮作用,因為其他日期完全命不中記錄 —— 這不是很浪費資源嘛!

針對這種場景,我們可以學習redis對過期鍵的清理策略之懶惰清理。我們不需要一種機制在精準(準精準)的時機變更合同狀態,而是如果這個合同不需要被命中,那麼它的狀態在資料庫中一直是之前的狀態:哪怕過了生效期,我們連到資料庫中檢視依然是沒有生效。只有當業務邏輯命中這條合同的時候,返回的資料中才需要找到這種合同進行修改。

這種方法可以有效降低系統邏輯中的輪空,但是查詢記錄的時候需要把前一種狀態的記錄也查詢到。因為本來我們需要命中的合同是已經生效的,使用懶惰策略就需要把待生效的合同也一起找到,然後從已經生效的合同中剔除已經過期的合同,從待生效的合同中找到已生效的合同;這時候,就需要把已過期和已生效的合同狀態都改掉。當然我們可以使用非同步邏輯去處理狀態變更,因為即使處理失敗依然不影響下一次的查詢結果。

事件驅動的好處

從上面的例子中我們可以感受到使用事件觸發機制代替定時任務實際上有利有弊。正向點是事件機制是按需的,有需求就來,沒需要就別來。弊端是為了維護事件機制的正常執行一般需要一套輔助邏輯:但是又有哪種邏輯不需要補償機制呢?只不過有些是成熟的、現成的,有些是需要我們重新開發的。

從本質上講,定時任務是一種無狀態的行為,也就不符合物件導向的思想。而事件觸發是模擬了人去處理一件事的流程。
比如記錄的彙總,在有管信之前運維人員是怎麼處理的呢?如果等到第二天把前一天擠壓的單子一起處理,這就是定時任務。如果單子量很大,人工一下子處理不完,就需要多個人一起處理,這就是分散式任務。如果多個人一時半會也處理不好,需要比較長的時間(人工的壓力上來了),他們可能會變通為來一單處理一單,不然一天中大部分的時間也是閒著,這樣壓力就降下來了。對於系統也一樣,實時處理可以有效降低系統壓力驟升。

實際上人工處理積壓單據依然不是定時任務的原型,這樣處理僅僅是因為人工能夠處理得過來,是人性的表現而非模型的轉換。定時任務難以在人身上找到原型,雖然人類現在也在使用鬧鐘,但鬧鐘的任務是提醒而重複任務

再模擬一下合同的處理。
假設沒有管信,合同查詢員估計會有幾個格子,一個放待生效的,一個放生效中的,一個放已過期的。當需要找合同的時候,他就去前兩個格子中尋找並把已生效的從第一個格子放到第二個,把已過期的從第二個挪到第三個。
那他可不可能提前挪這些合同呢?可能的,但完全沒必要:這依然是人性 —— 他太懶了。而且他今天挪過以後,可能今天一整天都沒有出現過需要使用合同的時候,那他為什麼不在明天挪呢?所以沒有一個合適的時間去挪,乾脆用的時候挪。

結語

所以結論是什麼?

  • 對於流式資料,我推薦使用實時處理,需要解決的問題是併發安全
  • 對於批資料,我推薦懶惰策略,需要解決的問題是冪等

寫在最後

如果你喜歡這個思想,但是自己遇到了難以摒棄定時任務的場景,歡迎你留言,我們一起來討論。

相關文章