古鎮(zhèn)中小企業(yè)網(wǎng)站建設(shè)如何找客戶資源
文章目錄
- 1.訂單的過程分析
- 2.JDK自帶的延時隊列 (單機)
- 3.RabbitMQ的延時消息 (消息隊列方案)
- 4.RocketMQ的定時消息 (消息隊列方案)
- 5.Redis過期監(jiān)聽 (Redis方案)
- 6.定時任務(wù)分布式批處理 (掃表輪訓(xùn)方案)
- 7.總結(jié)
1.訂單的過程分析
一個訂單流程中有許多環(huán)節(jié)要用到超時處理
- 買家超時未付款:比如超過15分鐘沒有支付,訂單自動取消。
- 商家超時未發(fā)貨:比如商家超過1個月沒發(fā)貨,訂單自動取消。
- 買家超時未收貨:比如商家發(fā)貨后,買家沒有在14天內(nèi)點擊確認收貨,則系統(tǒng)默認自動收貨。
超時訂單的結(jié)局方式:
- 掃表輪訓(xùn)
- 懶刪除
- 消息隊列實現(xiàn)
- Redis實現(xiàn)
2.JDK自帶的延時隊列 (單機)
JDK中提供了一種延遲隊列數(shù)據(jù)結(jié)構(gòu)DelayQueue,其本質(zhì)是封裝了PriorityQueue,可以把元素進行排序。
-
把訂單插入DelayQueue中,以超時時間作為排序條件,將訂單按照超時時間從小到大排序。
-
起一個線程不停輪詢隊列的頭部,如果訂單的超時時間到了,就出隊進行超時處理,并更新訂單狀態(tài)到數(shù)據(jù)庫中。
-
為了防止機器重啟導(dǎo)致內(nèi)存中的DelayQueue數(shù)據(jù)丟失,每次機器啟動的時候,需要從數(shù)據(jù)庫中初始化未結(jié)束的訂單,加入到DelayQueue中。
優(yōu)點:簡單,不需要借助其他第三方組件,成本低。
缺點:
-
所有超時處理訂單都要加入到DelayQueue中,占用內(nèi)存大。
-
沒法做到分布式處理,只能在集群中選一臺leader專門處理,效率低。
-
不適合訂單量比較大的場景。
3.RabbitMQ的延時消息 (消息隊列方案)
-
RabbitMQ Delayed Message Plugin
-
消息的TTL+死信Exchange
RabbitMQ Delayed Message Plugin是官方提供的延時消息插件,雖然使用起來比較方便,但是不是高可用的,如果節(jié)點掛了會導(dǎo)致消息丟失。
消息的TTL+死信Exchange解決方案:
-
定義一個BizQueue,用來接收死信消息,并進行業(yè)務(wù)消費。
-
定義一個死信交換機(DLXExchange),綁定BizQueue,接收延時隊列的消息,并轉(zhuǎn)發(fā)給BizQueue。
-
定義一組延時隊列DelayQueue_xx,分別配置不同的TTL,用來處理固定延時5s、10s、30s等延時等級,并綁定到DLXExchange。
-
定義DelayExchange,用來接收業(yè)務(wù)發(fā)過來的延時消息,并根據(jù)延時時間轉(zhuǎn)發(fā)到不同的延時隊列中。
優(yōu)點:可以支持海量延時消息,支持分布式處理。
缺點:
-
不靈活,只能支持固定延時等級。
-
使用復(fù)雜,要配置一堆延時隊列。
4.RocketMQ的定時消息 (消息隊列方案)
只需要在發(fā)送消息的時候設(shè)置延時時間即可
MessageBuilder messageBuilder = null;
Long deliverTimeStamp = System.currentTimeMillis() + 10L * 60 * 1000; //延遲10分鐘
Message message = messageBuilder.setTopic("topic")//設(shè)置消息索引鍵,可根據(jù)關(guān)鍵字精確查找某條消息。.setKeys("messageKey")//設(shè)置消息Tag,用于消費端根據(jù)指定Tag過濾消息。.setTag("messageTag")//設(shè)置延時時間.setDeliveryTimestamp(deliverTimeStamp) //消息體.setBody("messageBody".getBytes()).build();
SendReceipt sendReceipt = producer.send(message);
System.out.println(sendReceipt.getMessageId());
RocketMq定時消息的實現(xiàn):
使用了經(jīng)典的時間輪算法, 通過TimerWheel來描述時間輪不同的時刻,通過TimerLog來記錄不同時刻的消息。
TimerWheel中的每一格代表著一個時刻,同時會有一個firstPos指向這個刻度下所有定時消息的首條TimerLog記錄的地址,一個lastPos指向這個刻度下所有定時消息最后一條TimerLog的記錄的地址。并且,對于所處于同一個刻度的的消息,其TimerLog會通過prevPos串聯(lián)成一個鏈表。
當需要新增一條記錄的時候,例如現(xiàn)在我們要新增一個 “1-4”。那么就將新記錄的 prevPos 指向當前的 lastPos,即 “1-3”,然后修改 lastPos 指向 “1-4”。這樣就將同一個刻度上面的 TimerLog 記錄全都串起來了。
優(yōu)點:
-
精度高,支持任意時刻。
-
使用門檻低,和使用普通消息一樣。
缺點
-
使用限制:定時時長最大值24小時。
-
成本高:每個訂單需要新增一個定時消息,且不會馬上消費,給MQ帶來很大的存儲成本。
-
同一個時刻大量消息會導(dǎo)致消息延遲:定時消息的實現(xiàn)邏輯需要先經(jīng)過定時存儲等待觸發(fā),定時時間到達后才會被投遞給消費者。因此,如果將大量定時消息的定時時間設(shè)置為同一時刻,則到達該時刻后會有大量消息同時需要被處理,會造成系統(tǒng)壓力過大,導(dǎo)致消息分發(fā)延遲,影響定時精度。
5.Redis過期監(jiān)聽 (Redis方案)
刪除過期的key的時候, 進行判斷狀態(tài)是否是超時狀態(tài), 然后進行關(guān)閉訂單
Redis支持過期監(jiān)聽,也能達到和RocketMQ定時消息一樣的能力
1.redis配置文件開啟"notify-keyspace-events Ex"
2.監(jiān)聽key的過期回調(diào)
@Configuration
public class RedisListenerConfig {@BeanRedisMessageListenerContainer container(RedisConnectionFactory factory){RedisMessageListenerContainer container=new RedisMessageListenerContainer();container.setConnectionFactory(factory);return container;}
}
@Component
public class RedisKeyExpirationListerner extends KeyExpirationEventMessageListener {public RedisKeyExpirationListerner(RedisMessageListenerContainer listenerContainer) {super(listenerContainer);}@Overridepublic void onMessage(Message message, byte[] pattern) {String keyExpira = message.toString();System.out.println("監(jiān)聽到key:" + expiredKey + "已過期");}
}
在實際生產(chǎn)上不推薦
每當我們對一個key設(shè)置了過期時間,Redis就會把該key帶上過期時間,存到過期字典中,在redisDb中通過expires字段維護:
typedef struct redisDb {dict *dict; /* 維護所有key-value鍵值對 */dict *expires; /* 過期字典,維護設(shè)置失效時間的鍵 */....
} redisDb;
過期字典本質(zhì)上是一個鏈表
-
key是一個指針,指向某個鍵對象。
-
value是一個long long類型的整數(shù),保存了key的過期時間。
Redis主要使用了定期刪除和惰性刪除策略來進行過期key的刪除
Redis過期刪除是不精準的,在訂單超時處理的場景下,惰性刪除基本上也用不到,無法保證key在過期的時候可以立即刪除,更不能保證能立即通知。如果訂單量比較大,那么延遲幾分鐘也是有可能的。
Redis過期通知也是不可靠的,Redis在過期通知的時候,如果應(yīng)用正好重啟了,那么就有可能通知事件就丟了,會導(dǎo)致訂單一直無法關(guān)閉,有穩(wěn)定性問題。如果一定要使用Redis過期監(jiān)聽方案,建議再通過定時任務(wù)做補償機制。
如果無法刪除的話會導(dǎo)致庫存數(shù)據(jù)始終占著, 但是未支付也未取消支付。
6.定時任務(wù)分布式批處理 (掃表輪訓(xùn)方案)
開啟一個定時任務(wù)去掃描訂單表, 獲取待支付狀態(tài)的數(shù)據(jù), 判斷將一些超時狀態(tài)的數(shù)據(jù)進行批量修改狀態(tài)。
通過定時任務(wù)不停輪詢數(shù)據(jù)庫的訂單,將已經(jīng)超時的訂單撈出來,分發(fā)給不同的機器分布式處理:
-
穩(wěn)定性強:基于通知的方案(比如MQ和Redis),比較擔心在各種極端情況下導(dǎo)致通知的事件丟了。使用定時任務(wù)跑批,只需要保證業(yè)務(wù)冪等即可,如果這個批次有些訂單沒有撈出來,或者處理訂單的時候應(yīng)用重啟了,下一個批次還是可以撈出來處理,穩(wěn)定性非常高。
-
效率高:基于MQ的方案,需要一個訂單一個定時消息,consumer處理定時消息的時候也需要一個訂單一個訂單更新,對數(shù)據(jù)庫tps很高。使用定時任務(wù)跑批方案,一次撈出一批訂單,處理完了,可以批量更新訂單狀態(tài),減少數(shù)據(jù)庫的tps。在海量訂單處理場景下,批量處理效率最高。
-
可運維:基于數(shù)據(jù)庫存儲,可以很方便的對訂單進行修改、暫停、取消等操作,所見即所得。如果業(yè)務(wù)跑失敗了,還可以直接通過sql修改數(shù)據(jù)庫來進行批量運維。
-
成本低:相對于其他解決方案要借助第三方存儲組件,復(fù)用數(shù)據(jù)庫的成本大大降低。
缺點:沒法做到精度很高。定時任務(wù)的延遲時間,由定時任務(wù)的調(diào)度周期決定。如果把頻率設(shè)置很小,就會導(dǎo)致數(shù)據(jù)庫的qps比較高,容易造成數(shù)據(jù)庫壓力過大,從而影響線上的正常業(yè)務(wù)。
所以一般需要抽離出超時中心和超時庫來單獨做訂單的超時調(diào)度
如何讓超時中心不同的節(jié)點協(xié)同工作,拉取不同的數(shù)據(jù):
通常的解決方案是借助任務(wù)調(diào)度系統(tǒng),開源任務(wù)調(diào)度系統(tǒng)大多支持分片模型,比較適合做分庫分表的輪詢,比如一個分片代表一張分表。但是如果分表特別多,分片模型配置起來還是比較麻煩的。另外如果只有一張大表,或者超時中心使用其他的存儲,這兩個模型就不太適合。
阿里巴巴分布式任務(wù)調(diào)度系統(tǒng)SchedulerX:
- 通過實現(xiàn)map函數(shù),通過代碼自行構(gòu)造分片,SchedulerX會將分片平均分給超時中心的不同節(jié)點分布式執(zhí)行。
2. 通過實現(xiàn)reduce函數(shù),可以做聚合,可以判斷這次跑批有哪些分片跑失敗了,從而通知下游處理。
使用SchedulerX定時跑批解決方案:
-
免運維、成本低:不需要自建任務(wù)調(diào)度系統(tǒng),由云上托管。
-
可觀測:提供任務(wù)執(zhí)行的歷史記錄、查看堆棧、日志服務(wù)、鏈路追蹤等能力。
-
高可用:支持同城雙活容災(zāi),支持多種渠道的監(jiān)控報警。
-
混部:可以托管阿里云的機器,也可以托管非阿里云的機器。
7.總結(jié)
如果對于超時精度比較高
,超時時間在24小時內(nèi),且不會有峰值壓力的場景,推薦使用RocketMQ的定時消息解決方案
。
在電商業(yè)務(wù)
下,許多訂單超時場景都在24小時以上,對于超時精度沒有那么敏感
,并且有海量訂單需要批處理
,推薦使用基于定時任務(wù)的跑批解決方案
。
- 掃表輪訓(xùn): 定時任務(wù)分布式批處理, 阿里使用SchedulerX
- 懶刪除: 通過設(shè)置一個數(shù)據(jù)庫的狀態(tài), 用戶查詢訂單的時候去判斷狀態(tài)看是否關(guān)閉訂單
- 消息隊列實現(xiàn): RabbitMQ的ttl和延遲隊列, RocketMQ的定時消息
- Redis實現(xiàn): 刪除策略實現(xiàn)不樂觀