
背景

-
買家超時未付款:比如超過15分鐘沒有支付,訂單自動取消,
-
商家超時未發貨:比如商家超過1個月沒發貨,訂單自動取消,
-
買家超時未識訓:比如商家發貨后,買家沒有在14天內點擊確認識訓,則系統默認自動識訓,
一、JDK自帶的延時佇列

-
把訂單插入DelayQueue中,以超時時間作為排序條件,將訂單按照超時時間從小到大排序,
-
起一個執行緒不停輪詢佇列的頭部,如果訂單的超時時間到了,就出隊進行超時處理,并更新訂單狀態到資料庫中,
-
為了防止機器重啟導致記憶體中的DelayQueue資料丟失,每次機器啟動的時候,需要從資料庫中初始化未結束的訂單,加入到DelayQueue中,
-
優點:簡單,不需要借助其他第三方組件,成本低,
-
缺點:
-
所有超時處理訂單都要加入到DelayQueue中,占用記憶體大,
-
沒法做到分布式處理,只能在集群中選一臺leader專門處理,效率低,
-
不適合訂單量比較大的場景,
二、RabbitMQ的延時訊息
-
RabbitMQ Delayed Message Plugin
-
訊息的TTL+死信Exchange
-
TTL:即訊息的存活時間,RabbitMQ可以對佇列和訊息分別設定TTL,如果對佇列設定,則佇列中所有的訊息都具有相同的過期時間,超過了這個時間,我們認為這個訊息就死了,稱之為死信,
-
死信Exchange(DLX):一個訊息在滿足以下條件會進入死信交換機
-
一個訊息被Consumer拒收了,并且reject方法的引數里requeue是false,也就是說不會被再次放在佇列里,被其他消費者使用,
-
TTL到期的訊息,
-
佇列滿了被丟棄的訊息,

-
定義一個BizQueue,用來接收死信訊息,并進行業務消費,
-
定義一個死信交換機(DLXExchange),系結BizQueue,接收延時佇列的訊息,并轉發給BizQueue,
-
定義一組延時佇列DelayQueue_xx,分別配置不同的TTL,用來處理固定延時5s、10s、30s等延時等級,并系結到DLXExchange,
-
定義DelayExchange,用來接收業務發過來的延時訊息,并根據延時時間轉發到不同的延時佇列中,
-
優點:可以支持海量延時訊息,支持分布式處理,
-
缺點:
-
不靈活,只能支持固定延時等級,
-
使用復雜,要配置一堆延時佇列,
三、RocketMQ的定時訊息
RocketMQ支持任意秒級的定時訊息,如下圖所示

MessageBuilder messageBuilder = null;
Long deliverTimeStamp = System.currentTimeMillis() + 10L * 60 * 1000; //延遲10分鐘
Message message = messageBuilder.setTopic("topic")
//設定訊息索引鍵,可根據關鍵字精確查找某條訊息,
.setKeys("messageKey")
//設定訊息Tag,用于消費端根據指定Tag過濾訊息,
.setTag("messageTag")
//設定延時時間
.setDeliveryTimestamp(deliverTimeStamp)
//訊息體
.setBody("messageBody".getBytes())
.build();
SendReceipt sendReceipt = producer.send(message);
System.out.println(sendReceipt.getMessageId());


-
優點
-
精度高,支持任意時刻,
-
使用門檻低,和使用普通訊息一樣,
-
缺點
-
使用限制:定時時長最大值24小時,
-
成本高:每個訂單需要新增一個定時訊息,且不會馬上消費,給MQ帶來很大的存盤成本,
-
同一個時刻大量訊息會導致訊息延遲:定時訊息的實作邏輯需要先經過定時存盤等待觸發,定時時間到達后才會被投遞給消費者,因此,如果將大量定時訊息的定時時間設定為同一時刻,則到達該時刻后會有大量訊息同時需要被處理,會造成系統壓力過大,導致訊息分發延遲,影響定時精度,
四、Redis的過期監聽
-
redis組態檔開啟"notify-keyspace-events Ex"

-
監聽key的過期回呼,以java代碼為例
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory){
RedisMessageListenerContainer container=new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}
@Component
public class RedisKeyExpirationListerner extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListerner(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String keyExpira = message.toString();
System.out.println("監聽到key:" + expiredKey + "已過期");
}
}

typedef struct redisDb {
dict *dict; /* 維護所有key-value鍵值對 */
dict *expires; /* 過期字典,維護設定失效時間的鍵 */
....
} redisDb;
過期字典本質上是一個鏈表,每個節點的資料結構結構如下:
-
key是一個指標,指向某個鍵物件,
-
value是一個long long型別的整數,保存了key的過期時間,

-
定期洗掉:每隔一段時間(默認100ms)就隨機抽取一些設定了過期時間的key,檢查其是否過期,如果有過期就洗掉,之所以這么做,是為了通過限制洗掉操作的執行時長和頻率來減少對cpu的影響,不然每隔100ms就要遍歷所有設定過期時間的key,會導致cpu負載太大,
-
惰性洗掉:不主動洗掉過期的key,每次從資料庫訪問key時,都檢測key是否過期,如果過期則洗掉該key,惰性洗掉有一個問題,如果這個key已經過期了,但是一直沒有被訪問,就會一直保存在資料庫中,
五、定時任務分布式批處理
定時任務分布式批處理解決方案,即通過定時任務不停輪詢資料庫的訂單,將已經超時的訂單撈出來,分發給不同的機器分布式處理:

-
穩定性強:基于通知的方案(比如MQ和Redis),比較擔心在各種極端情況下導致通知的事件丟了,使用定時任務跑批,只需要保證業務冪等即可,如果這個批次有些訂單沒有撈出來,或者處理訂單的時候應用重啟了,下一個批次還是可以撈出來處理,穩定性非常高,
-
效率高:基于MQ的方案,需要一個訂單一個定時訊息,consumer處理定時訊息的時候也需要一個訂單一個訂單更新,對資料庫tps很高,使用定時任務跑批方案,一次撈出一批訂單,處理完了,可以批量更新訂單狀態,減少資料庫的tps,在海量訂單處理場景下,批量處理效率最高,
-
可運維:基于資料庫存盤,可以很方便的對訂單進行修改、暫停、取消等操作,所見即所得,如果業務跑失敗了,還可以直接通過sql修改資料庫來進行批量運維,
-
成本低:相對于其他解決方案要借助第三方存盤組件,復用資料庫的成本大大降低,

-
通過實作map函式,通過代碼自行構造分片,SchedulerX會將分片平均分給超時中心的不同節點分布式執行,

-
通過實作reduce函式,可以做聚合,可以判斷這次跑批有哪些分片跑失敗了,從而通知下游處理,

-
免運維、成本低:不需要自建任務調度系統,由云上托管,
-
可觀測:提供任務執行的歷史記錄、查看堆疊、日志服務、鏈路追蹤等能力,
-
高可用:支持同城雙活容災,支持多種渠道的監控報警,
-
混部:可以托管阿里云的機器,也可以托管非阿里云的機器,
總結
[1]https://developer.aliyun.com/article/994932
[2]https://redis.io/docs/manual/keyspace-notifications/
[3]https://www.aliyun.com/aliware/schedulerx
作者|黃曉萌(學仁)
本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/How-to-handle-order-overtime.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/545891.html
標籤:架構設計
上一篇:訪問者模式
