如何設計訂單超時自動取消
我們在美團 APP 下單,假如沒有立即支付,進入訂單詳情會顯示倒計時,如果超過支付時間,訂單就會被自動取消。
網上有很多八股文,很多方案其實并不太適合真實的業務場景。所以這篇文章,筆者想深入剖析如何設計訂單超時自動取消的功能,希望能帶給大家一些啟發。
圖片
1 定時任務方案
首先,我們非常自然的想到定時任務的方案。
方案流程:
- 每隔 30 秒查詢數據庫,取出最近的 N 條未支付的訂單。
- 遍歷查詢出來的訂單列表,判斷當前時間減去訂單的創建時間是否超過了支付超時時間,如果超時則對該訂單執行取消操作。
定時任務方案工程實現相對簡單,但這種方案會間隔對數據庫造成一定的 IO 壓力。特別是當訂單量數據量非常高時,高頻次的查詢對數據庫的性能是個不小的考驗。
定時任務方案從功能模塊角度來講,包含調度層和業務邏輯層兩部分。
圖片
網上有很多的定時任務實現策略,我們可以簡單劃分為單機版和集群版。
2 定時任務方案:單機版
我們可以使用 Timer 、ScheduledEexcutorService、Quartz 非常容易的實現定時任務。
圖片
但筆者并不推薦使用單機版的方案,舉個簡單的例子:
圖片
假設我們應用 A 通過 Quartz 調度三個定時任務 A、B、C ,當集群部署時,可能出現多臺不同機器實例同時執行任務的風險。
此時,我們可以通過加鎖的方式適當規避,見下圖:
圖片
但這種方式并不優雅,同時定時任務應用內調度層會經常空跑,我們預期是希望三個定時任務 A、B、C 能均勻分布應用 A的不同實例內。
好,接下來,筆者會介紹親身經歷的三種集群定時任務。
3 定時任務方案:集群版
圖片
3.1 Quartz + JDBCJobStore
Quartz 可以支持集群模式,集群模式需要在數據庫中添加11張表,對業務系統有一定的侵入性。
圖片
筆者曾經服務的一家彩票公司,訂單調度中心就是使用 Quartz 的集群模式,實現日均百萬訂單的調度處理。
需要特別注意的是:
基于底層數據庫悲觀鎖的機制,Quartz 的集群模式性能并不高,假如執行頻率高的任務數超過達到一定數量,存在性能問題。
3.1 Elastic-Job
ElasticJob 定位為輕量級無中心化解決方案,使用 jar 的形式提供分布式任務的協調服務。
ElasticJob 從本質上來講 ,底層任務調度還是通過 Quartz ,它的優勢在于可以依賴 Zookeeper 這個大殺器 ,將任務通過負載均衡算法分配給應用內的 Quartz Scheduler 容器,
舉例:應用A有五個任務需要執行,分別是 A,B,C,D,E。任務E需要分成四個子任務,應用部署在兩臺機器上。
圖片
圖中,應用 A 在啟動后, 5個任務通過 Zookeeper 協調后被分配到兩臺機器上,通過 Quartz Scheduler 分開執行不同的任務。
相比 Quartz 集群模式,ElasticJob 的可擴展性更高,同時因為是本地內存存儲 JOB,性能非常好。
但是 ElasticJob 的控制臺非常粗糙,主要原因還是基于它的實現機制 (Quartz + zookeeper)。
通過控制 zookeeper 節點來間接操作應用內任務執行情況,但這樣非常不靈活,所以筆者認為 ElasticJob 更多的還是定位于框架,而不是一個調度平臺。
3.3 任務調度平臺
筆者非常認可任務調度平臺這種模式。XXL-JOB 是一個使用最廣泛的分布式任務調度平臺。
圖片
業務系統和調度平臺分開部署,我們在調度中心上配置應用以及其定時任務,當任務需要執行時,調度平臺會觸發業務系統的任務,業務系統執行完任務之后,反饋給調度平臺任務執行的結果。
業務系統和調度平臺都可以水平擴展實現高可用,同時在調度平臺可以配置靈活的調度策略(比如重試機制、廣播模式等)。
XXL-JOB 并不完美,因為底層依然是基于數據庫悲觀鎖的機制,雖然通過時間輪的方式做了一定程度的優化,但依然會有性能瓶頸。
很多公司比如神州專車、美團都有自己自研的任務調度平臺。這種模式非常適合多團隊協作,便于大規模調度任務的統一管理。
4 延時消息方案
延時消息是一種非常優雅的模式。訂單服務生成訂單后,發送一條延時消息到消息隊列。消息隊列在消息到達支付過期時間時,將消息投遞給消費者,消費者收到消息之后,判斷訂單狀態是否為已支付,假如未支付,則執行取消訂單的邏輯。
圖片
4.1 消息隊列 RocketMQ
RocketMQ 4.X 生產者發送延遲消息代碼如下:
Message msg = new Message();
msg.setTopic("TopicA");
msg.setTags("Tag");
msg.setBody("this is a delay message".getBytes());
//設置延遲level為5,對應延遲1分鐘
msg.setDelayTimeLevel(5);
producer.send(msg);
RocketMQ 4.X 版本默認支持 18 個 level 的延遲消息, 通過 broker 端的 messageDelayLevel 配置項確定的。
圖片
RocketMQ 5.X 版本支持任意時刻延遲消息,客戶端在構造消息時提供了 3 個 API 來指定延遲時間或定時時間。
圖片
假如技術團隊基礎架構能力很強,筆者非常推薦使用 RocketMQ 5.X 的延遲消息功能。
4.2 自研延遲服務
基于 RocketMQ 4 內置的延遲消息只能支持幾個固定的延遲級別,快手、滴滴開發了單獨的 Delay Server 來調度延遲消息。
圖片
上圖這個結構沒有直接將延遲消息發到 Delay Server,而是更換 Topic 以后存入 RocketMQ。這樣的好處是可以復用現有的消息發送接口(以及上面的所有擴展能力)。對業務來說,只需要在構造消息的時候額外指定一個延遲時間字段即可,其它用法都不變。
自研單獨的 Delay Server 不僅可以適配 RocketMQ 4.X , 也可以適配 Kafka ,同時,也可以具有非常高的性能,說實話,這個是一個非常實用且靈活的方案。
4.3 Redis 延遲隊列
Redis 延遲隊列是一個輕量級的解決方案,開源成熟的實現是 Redission 。
圖片
圖中,我們定義兩個集合:
1、zset 集合
生產者將任務信息發送到 zset 集合,value 是任務編號,score 是任務執行時間戳。
2、list 集合
守護線程檢測 zset 集合中到期的任務,若任務到期,將任務編號轉移到 list 集合 , 消費者從 list 集合彈出任務,并執行任務邏輯。
筆者需要強調的是:
Redis 雖然可以實現延遲消息的功能,但 Redis 并不是真正意義上的消息隊列,在使用過程中還是有小概率會丟失消息。
5 最佳實踐
5.1 并發口訣:一鎖二判三更新
不管我們使用定時任務還是延遲消息時,不可避免的會遇到并發執行任務的情況 (比如重復消費、調度重試等)。
當我們執行任務時,我們可以按照一鎖二判三更新這個口訣來處理。
- 鎖定當前需要處理的訂單。
- 判斷訂單是否已經更新過對應狀態了
- 如果訂單之前沒有更新過狀態了,可以更新并完成相關業務邏輯,否則本次不能更新,也不能完成業務邏輯。
- 釋放當前訂單的鎖。
圖片
偽代碼
5.2 兜底意識 + 配置監控
雖然我們提到了很多的實現策略,現實實戰時依然容易出現問題,比如不合理的操作導致消息丟失。
因此,我們應該具備兜底意識。
假如少量消息丟失,我們可以通過每天凌晨跑一次任務,批量將這些未處理的訂單批量取消。這種兜底行為工程實現簡單,同時對系統影響很小。
還有一點,就是配置監控。
筆者曾經自研過任務調度系統,應用 A 接入后,從控制臺發現每隔 2 個小時調度應用 A 的任務時,經常發生超時,通過分析,發現應用 A 線程出現了死鎖。
這種問題出現的幾率非常高,因此配置監控特別要必要。
對業務系統來講,監控分為兩個層面:系統監控和業務監控。
- 系統監控
在條件允許的情況下,建議關注性能監控,方法可用性監控,方法調用次數監控這三大類。
性能監控
上圖是性能監控的示例圖,性能監控不同時間段性能分布,實時統計 TP99、TP999 、AVG 、MAX 等維度指標,這也是性能調優的重點關注對象。
- 業務監控
業務監控功能是從業務角度出發,各個應用系統需要從業務層面進行哪些監控,以及提供怎樣的業務層面的監控功能支持業務相關的應用系統。
具體就是對業務數據,業務功能進行監控,實時收集業務流程的數據,并根據設置的策略對業務流程中不符合預期的部分進行預警和報警,并對收集到業務監控數據進行集中統一的存儲和各種方式進行展示。
比如訂單系統中有一個定時結算的服務,每兩分鐘執行一次。我們可以在定時任務 JOB 中添加埋點,并配置業務監控,假如十分鐘該定時任務沒有執行,則發送郵件,短信給相關負責人。
6 總結
這篇文章,筆者總結了訂單超時自動取消方案的兩種流派:定時任務和延遲消息。
1、定時任務
定時任務實現策略,我們可以簡單劃分為單機版和集群版。
筆者并不認可單機版,背八股文當然可以,訂單自動取消這個業務場景,生產環境還是要慎重。
集群版有三種方式:Quartz + JDBCJobStore、ElasticJob 、XXL-JOB 。
每種方式各有優缺點,筆者更傾向于任務調度平臺 XXL-JOB 這種方式。
2、延遲消息
延時消息是一種非常優雅的模式。本文介紹了三種方式:消息隊列 RocketMQ、自研延遲服務、Redis 延遲隊列。
假如技術團隊基礎架構能力很強,筆者推薦使用 RocketMQ 或者自研延遲服務。
假如技術團隊僅僅想用輕量級的實現,可以選擇 Redis 延遲隊列。
不管是使用定時任務還是延遲消息,架構的穩定性還需要注意如下兩點:
1、并發口訣:一鎖二判三更新 ;
2、兜底意識 + 配置監控。