深夜,小艾接到了一通突如其來的電話,是物流系統(tǒng)的負(fù)責(zé)人曹工焦急的聲音。他火急火燎地反饋了一個嚴(yán)重的問題——大批用戶投訴物流信息異常,訂單狀態(tài)與實際情況不符,用戶已完成支付,但物流單還是待支付狀態(tài)。
小艾立刻警覺起來,意識到這個問題可能對公司的業(yè)務(wù)以及用戶體驗造成重大影響。她一邊安撫曹工的情緒,一邊迅速啟動緊急響應(yīng)機(jī)制,通知QA對線上變更進(jìn)行回滾。
隨著回滾進(jìn)程的推進(jìn),系統(tǒng)逐步恢復(fù)正常。緊接著,他手工導(dǎo)出上線以來的全部訂單,并與曹工一起進(jìn)行數(shù)據(jù)核對,對問題數(shù)據(jù)進(jìn)行修復(fù)。終于忙完了,天空已經(jīng)微微發(fā)亮……
上午稍微補(bǔ)了個覺,小艾洗漱完畢后對這件事進(jìn)行分析:訂單已支付,物流單待支付。
現(xiàn)在訂單和物流的系統(tǒng)交互如下:
圖片
在正常的業(yè)務(wù)流程中,訂單發(fā)布事件和物流監(jiān)聽事件緊密相連。
在正常情況下,沒有出現(xiàn)不一致的情況。小艾想到了最近的系統(tǒng)變更:
最近上線的一項新功能——禮品贈送。為了降低對下游系統(tǒng)的影響,小艾通過在應(yīng)用層對流程進(jìn)行編排的方式實現(xiàn)該功能,簡單來說,就是系統(tǒng)先創(chuàng)建訂單,然后模擬支付成功,這樣既能滿足禮品贈送的需求,又能保障下游契約消息沒有變化。新流程如下所示:
圖片
整個流程與原來的方案沒有差別,問題究竟出現(xiàn)在哪呢?無奈的小艾只好打開 idea 查看源碼,終于發(fā)現(xiàn)問題所在:
@Servicepublic class RocketMQProducer { @Autowired private RocketMQTemplate rocketMQTemplate; @TransactionalEventListener public void handle(OrderCreatedEvent event){ rocketMQTemplate.convertAndSend("order_created_event", event); } @TransactionalEventListener public void handle(OrderPaidEvent event){ rocketMQTemplate.convertAndSend("order_paid_event", event); }}下單和支付成功使用兩個不同的 topic,兩個 topic 相互獨立,根本就無法保障投遞順序。在手動支付場景下,由于用戶從訂單創(chuàng)建到支付完成通常會有 5 秒以上的延遲,在這種情況下該實現(xiàn)可以保障邏輯的執(zhí)行順序。然而在禮品贈送這個場景,系統(tǒng)先創(chuàng)建訂單,然后模擬支付成功,導(dǎo)致“訂單已創(chuàng)建”和“訂單已支付”兩個事件幾乎同時發(fā)出,在接收端就有可能先收到支付成功事件,再收到訂單已創(chuàng)建事件,從而導(dǎo)致訂單狀態(tài)和物流單狀態(tài)不一致,具體流程如下:
圖片
如果順序錯了,就會導(dǎo)致業(yè)務(wù)狀態(tài)不一致:
問題終于找到了!!!
既然是順序問題,那最簡的方法就是對支付成功消息進(jìn)行延時發(fā)送。
方案如下:
圖片
中間增加一個延時組件便能解決這個問題,但不同的方案影響巨大:
定時器方案,核心代碼如下:
@TransactionalEventListenerpublic void handle(OrderPaidEvent event){ // 創(chuàng)建Runnable任務(wù) Runnable task = () -> { rocketMQTemplate.convertAndSend("order_paid_event", event); }; // 使用ScheduledExecutorService schedule方法在5秒后執(zhí)行任務(wù) executor.schedule(task, 5, TimeUnit.SECONDS);}該方案存在幾個比較嚴(yán)重的問題:
現(xiàn)在不少 MQ 提供順序消息的支持,比如常見的 RocketMQ 提供了兩種類型的順序消息:全局順序消息和分區(qū)順序消息。
分區(qū)順序消息整體設(shè)計如下:
圖片
核心代碼如下:
@TransactionalEventListenerpublic void handle(OrderCreatedEvent event){ Long orderId = event.getOrderId(); Message<OrderCreatedEvent> message = MessageBuilder.withPayload(event) .setHeader(RocketMQHeaders.KEYS, orderId) // 設(shè)置 Sharding Key,即訂單ID .setHeader(RocketMQHeaders.TAGS, "OrderCreatedEvent") // 設(shè)置 Tag .build(); // 發(fā)送至統(tǒng)一的 order_event_topic rocketMQTemplate.send("order_event_topic", message);}@TransactionalEventListenerpublic void handle(OrderPaidEvent event){ Long orderId = event.getOrderId(); Message<OrderPaidEvent> message = MessageBuilder.withPayload(event) .setHeader(RocketMQHeaders.KEYS, orderId) // 設(shè)置 Sharding Key,即訂單ID .setHeader(RocketMQHeaders.TAGS, "OrderCreatedEvent") // 設(shè)置 Tag .build(); // 發(fā)送至統(tǒng)一的 order_event_topic rocketMQTemplate.send("order_event_topic", message);}代碼倉庫:https://gitee.com/litao851025/learnFromBug
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/mq/disorder
本文鏈接:http://m.www897cc.com/showinfo-26-80867-0.html故障現(xiàn)場 | MQ消息亂序造成的業(yè)務(wù)事故
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 三分鐘學(xué)會消息隊列實踐
下一篇: 你最擅長使用哪個異步編程模式?