電商平臺(tái)中訂單未支付過(guò)期如何實(shí)現(xiàn)自動(dòng)關(guān)單?
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
日常開(kāi)發(fā)中,我們經(jīng)常遇到這種業(yè)務(wù)場(chǎng)景,如:外賣訂單超 30 分鐘未支付,則自動(dòng)取訂單;用戶注冊(cè)成功 15 分鐘后,發(fā)短信息通知用戶等等。這就是延時(shí)任務(wù)處理場(chǎng)景。 在電商,支付等系統(tǒng)中,一設(shè)都是先創(chuàng)建訂單(支付單),再給用戶一定的時(shí)間進(jìn)行支付,如果沒(méi)有按時(shí)支付的話,就需要把之前的訂單(支付單)取消掉。這種類以的場(chǎng)景有很多,還有比如到期自動(dòng)收貨,超時(shí)自動(dòng)退款,下單后自動(dòng)發(fā)送短信等等都是類似的業(yè)務(wù)問(wèn)題。 定時(shí)任務(wù)(數(shù)據(jù)庫(kù)輪詢)通過(guò)定時(shí)任務(wù)關(guān)閉訂單,是一種成本很低,實(shí)現(xiàn)也很容易的方案。通過(guò)簡(jiǎn)單的幾行代碼,寫一個(gè)定時(shí)任務(wù),定期掃描數(shù)據(jù)庫(kù)中的訂單,如果時(shí)間過(guò)期,就將其狀態(tài)更新為關(guān)閉即可。
優(yōu)點(diǎn):實(shí)現(xiàn)容易,成本低,基本不依賴其他組件。 缺點(diǎn):
總結(jié):采用定時(shí)任務(wù)的方案比較適合對(duì)時(shí)間要求不是很敏感,并且數(shù)據(jù)量不太多的業(yè)務(wù)場(chǎng)景。 JDK 延遲隊(duì)列 DelayQueueDelayQueue是JDK提供的一個(gè)無(wú)界隊(duì)列,DelayQueue隊(duì)列中的元素需要實(shí)現(xiàn)Delayed,它只提供了一個(gè)方法,就是獲取過(guò)期時(shí)間。 用戶的訂單生成以后,設(shè)置過(guò)期時(shí)間比如30分鐘,放入定義好的DelayQueue,然后創(chuàng)建一個(gè)線程,在線程中通過(guò)while(true)不斷的從DelayQueue中獲取過(guò)期的數(shù)據(jù)。 優(yōu)點(diǎn):不依賴任何第三方組件,連數(shù)據(jù)庫(kù)也不需要了,實(shí)現(xiàn)起來(lái)也方便。 缺點(diǎn):
總結(jié):DelayQueue適用于數(shù)據(jù)量較小,且丟失也不影響主業(yè)務(wù)的場(chǎng)景,比如內(nèi)部系統(tǒng)的一些非重要通知,就算丟失,也不會(huì)有太大影響。 redis過(guò)期監(jiān)聽(tīng)redis 是一個(gè)高性能的KV 數(shù)據(jù)庫(kù),除了用作緩存以外,其實(shí)還提供了過(guò)期監(jiān)聽(tīng)的功能。在redis.conf 中,配置notify-keyspace-events Ex 即可開(kāi)啟此功能。然后在代碼中繼承KeyspaceEventMessageListener,實(shí)現(xiàn)onMessage 就可以監(jiān)聽(tīng)過(guò)期的數(shù)據(jù)量。
通過(guò)以上源碼,我們可以發(fā)現(xiàn),其本質(zhì)也是注冊(cè)一個(gè)listener,利用redis 的發(fā)布訂閱,當(dāng)key 過(guò)期時(shí),發(fā)布過(guò)期消息(key)到Channel : 在實(shí)際的業(yè)務(wù)中,我們可以將訂單的過(guò)期時(shí)間設(shè)置比如30 分鐘,然后放入到redis。30 分鐘之后,就可以消費(fèi)這個(gè)key,然后做一些業(yè)務(wù)上的后置動(dòng)作,比如檢查用戶是否支付。 優(yōu)點(diǎn): 由于redis 的高性能,所以我們?cè)谠O(shè)置key,或者消費(fèi)key 時(shí),速度上是可以保證的。 缺點(diǎn):致命缺陷,不宜使用 在 Redis 官方手冊(cè)的keyspace-notifications: timing-of-expired-events中明確指出:
redis 自動(dòng)過(guò)期的實(shí)現(xiàn)方式是:定時(shí)任務(wù)離線掃描并刪除部分過(guò)期鍵;在訪問(wèn)鍵時(shí)惰性檢查是否過(guò)期并刪除過(guò)期鍵。也就是說(shuō),由于redis 的key 過(guò)期策略原因,當(dāng)一個(gè)key 過(guò)期時(shí),redis 從未保證會(huì)在設(shè)定的過(guò)期時(shí)間立即刪除并發(fā)送過(guò)期通知,自然我們的監(jiān)聽(tīng)事件也無(wú)法第一時(shí)間消費(fèi)到這個(gè)key,所以會(huì)存在一定的延遲。實(shí)際上,過(guò)期通知晚于設(shè)定的過(guò)期時(shí)間數(shù)分鐘的情況也比較常見(jiàn)。 另外,在redis5.0 之前,訂閱發(fā)布中的消息并沒(méi)有被持久化,自然也沒(méi)有所謂的確認(rèn)機(jī)制。所以一旦消費(fèi)消息的過(guò)程中我們的客戶端發(fā)生了宕機(jī),這條消息就徹底丟失了。 總結(jié):redis 的過(guò)期訂閱相比于其他方案沒(méi)有太大的優(yōu)勢(shì),在實(shí)際生產(chǎn)環(huán)境中,用得相對(duì)較少。 Redisson分布式延遲隊(duì)列Redisson是一個(gè)基于redis實(shí)現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格,它不僅提供了一系列的分布式的Java常用對(duì)象,還提供了許多分布式服務(wù)。 Redisson除了提供我們常用的分布式鎖外,還提供了一個(gè)分布式延遲隊(duì)列RDelayedQueue,他是一種基于zset結(jié)構(gòu)實(shí)現(xiàn)的延遲隊(duì)列,其實(shí)現(xiàn)類是RedissonDelayedQueue。delayqueue 中有一個(gè)名為 timeoutSetName 的有序集合,其中元素的 score 為投遞時(shí)間戳。delayqueue 會(huì)定時(shí)使用 zrangebyscore 掃描已到投遞時(shí)間的消息,然后把它們移動(dòng)到就緒消息列表中。 delayqueue 保證 redis 不崩潰的情況下不會(huì)丟失消息,在沒(méi)有更好的解決方案時(shí)不妨一試。 優(yōu)點(diǎn):使用簡(jiǎn)單,并且其實(shí)現(xiàn)類中大量使用lua 腳本保證其原子性,不會(huì)有并發(fā)重復(fù)問(wèn)題。 缺點(diǎn):需要依賴redis(如果這算一種缺點(diǎn)的話)。 總結(jié):Redisson 是redis 官方推薦的JAVA 客戶端,提供了很多常用的功能,使用簡(jiǎn)單、高效,推薦使用。 RocketMQ 延遲消息延遲消息:當(dāng)消息寫入到Broker 后,不會(huì)立刻被消費(fèi)者消費(fèi),需要等待指定的時(shí)長(zhǎng)后才可被消費(fèi)處理的消息,稱為延時(shí)消息。 在訂單創(chuàng)建之后,我們就可以把訂單作為一條消息投遞到rocketmq,并將延遲時(shí)間設(shè)置為30 分鐘,這樣,30 分鐘后我們定義的consumer 就可以消費(fèi)到這條消息,然后檢查用戶是否支付了這個(gè)訂單。 通過(guò)延遲消息,我們就可以將業(yè)務(wù)解耦,極大地簡(jiǎn)化我們的代碼邏輯。 優(yōu)點(diǎn):可以使代碼邏輯清晰,系統(tǒng)之間完全解耦,只需關(guān)注生產(chǎn)及消費(fèi)消息即可。另外其吞吐量極高,最多可以支撐萬(wàn)億級(jí)的數(shù)據(jù)量。 缺點(diǎn):相對(duì)來(lái)說(shuō),mq 是重量級(jí)的組件,引入mq 之后,隨之而來(lái)的消息丟失、冪等性問(wèn)題等都加深了系統(tǒng)的復(fù)雜度。 總結(jié):通過(guò)mq 進(jìn)行系統(tǒng)業(yè)務(wù)解耦,以及對(duì)系統(tǒng)性能削峰填谷已經(jīng)是當(dāng)前高性能系統(tǒng)的標(biāo)配。 RabbitMQ 死信隊(duì)列除了RocketMQ 的延遲隊(duì)列,RabbitMQ 的死信隊(duì)列也可以實(shí)現(xiàn)消息延遲功能。 死信(Dead Letter) 是 rabbitmq 提供的一種機(jī)制。當(dāng)一條消息滿足下列條件之一那么它會(huì)成為死信:
基于這樣的機(jī)制,我們可以給消息設(shè)置一個(gè)ttl,然后故意不消費(fèi)消息,等消息過(guò)期就會(huì)進(jìn)入死信隊(duì)列,我們?cè)傧M(fèi)死信隊(duì)列即可。通過(guò)這樣的方式,就可以達(dá)到同RocketMQ 延遲消息一樣的效果。 在 rabbitmq 中創(chuàng)建死信隊(duì)列的操作流程大概是:
死信隊(duì)列的設(shè)計(jì)目的是為了存儲(chǔ)沒(méi)有被正常消費(fèi)的消息,便于排查和重新投遞。死信隊(duì)列同樣也沒(méi)有對(duì)投遞時(shí)間做出保證,在第一條消息成為死信之前,后面的消息即使過(guò)期也不會(huì)投遞為死信。 為了解決這個(gè)問(wèn)題,rabbit 官方推出了延遲投遞插件 rabbitmq-delayed-message-exchange ,推薦使用官方插件來(lái)做延時(shí)消息。 優(yōu)點(diǎn):同RocketMQ 一樣,RabbitMQ 同樣可以使業(yè)務(wù)解耦,基于其集群的擴(kuò)展性,也可以實(shí)現(xiàn)高可用、高性能的目標(biāo)。 缺點(diǎn):死信隊(duì)列本質(zhì)還是一個(gè)隊(duì)列,隊(duì)列都是先進(jìn)先出,如果隊(duì)頭的消息過(guò)期時(shí)間比較長(zhǎng),就會(huì)導(dǎo)致后面過(guò)期的消息無(wú)法得到及時(shí)消費(fèi),造成消息阻塞。 總結(jié):除了增加系統(tǒng)復(fù)雜度之外,死信隊(duì)列的阻塞問(wèn)題也是需要我們重點(diǎn)關(guān)注的。
最佳實(shí)踐實(shí)際上,在數(shù)據(jù)庫(kù)索引設(shè)計(jì)良好的情況下,定時(shí)掃描數(shù)據(jù)庫(kù)中未完成的訂單產(chǎn)生的開(kāi)銷并沒(méi)有想象中那么大。在使用 redisson delayqueue 等定時(shí)任務(wù)中間件時(shí)可以同時(shí)使用掃描數(shù)據(jù)庫(kù)的方法作為補(bǔ)償機(jī)制,避免中間件故障造成任務(wù)丟失。 為什么不建議用MQ實(shí)現(xiàn)訂單到期關(guān)閉
并發(fā)口訣:一鎖二判三更新不管我們使用定時(shí)任務(wù)還是延遲消息時(shí),不可避免的會(huì)遇到并發(fā)執(zhí)行任務(wù)的情況 (比如重復(fù)消費(fèi)、調(diào)度重試等)。 當(dāng)我們執(zhí)行任務(wù)時(shí),我們可以按照一鎖二判三更新這個(gè)口訣來(lái)處理。
兜底意識(shí) + 配置監(jiān)控雖然我們提到了很多的實(shí)現(xiàn)策略,現(xiàn)實(shí)實(shí)戰(zhàn)時(shí)依然容易出現(xiàn)問(wèn)題,比如不合理的操作導(dǎo)致消息丟失。 因此,我們應(yīng)該具備兜底意識(shí)。 假如少量消息丟失,我們可以通過(guò)每天凌晨跑一次任務(wù),批量將這些未處理的訂單批量取消。這種兜底行為工程實(shí)現(xiàn)簡(jiǎn)單,同時(shí)對(duì)系統(tǒng)影響很小。 還有一點(diǎn),就是配置監(jiān)控。 筆者曾經(jīng)自研過(guò)任務(wù)調(diào)度系統(tǒng),應(yīng)用 A 接入后,從控制臺(tái)發(fā)現(xiàn)每隔 2 個(gè)小時(shí)調(diào)度應(yīng)用 A 的任務(wù)時(shí),經(jīng)常發(fā)生超時(shí),通過(guò)分析,發(fā)現(xiàn)應(yīng)用 A 線程出現(xiàn)了死鎖。 這種問(wèn)題出現(xiàn)的幾率非常高,因此配置監(jiān)控特別要必要。 對(duì)業(yè)務(wù)系統(tǒng)來(lái)講,監(jiān)控分為兩個(gè)層面:系統(tǒng)監(jiān)控和業(yè)務(wù)監(jiān)控。
在條件允許的情況下,建議關(guān)注性能監(jiān)控,方法可用性監(jiān)控,方法調(diào)用次數(shù)監(jiān)控這三大類。
上圖是性能監(jiān)控的示例圖,性能監(jiān)控不同時(shí)間段性能分布,實(shí)時(shí)統(tǒng)計(jì) TP99、TP999 、AVG 、MAX 等維度指標(biāo),這也是性能調(diào)優(yōu)的重點(diǎn)關(guān)注對(duì)象。
業(yè)務(wù)監(jiān)控功能是從業(yè)務(wù)角度出發(fā),各個(gè)應(yīng)用系統(tǒng)需要從業(yè)務(wù)層面進(jìn)行哪些監(jiān)控,以及提供怎樣的業(yè)務(wù)層面的監(jiān)控功能支持業(yè)務(wù)相關(guān)的應(yīng)用系統(tǒng)。 具體就是對(duì)業(yè)務(wù)數(shù)據(jù),業(yè)務(wù)功能進(jìn)行監(jiān)控,實(shí)時(shí)收集業(yè)務(wù)流程的數(shù)據(jù),并根據(jù)設(shè)置的策略對(duì)業(yè)務(wù)流程中不符合預(yù)期的部分進(jìn)行預(yù)警和報(bào)警,并對(duì)收集到業(yè)務(wù)監(jiān)控?cái)?shù)據(jù)進(jìn)行集中統(tǒng)一的存儲(chǔ)和各種方式進(jìn)行展示。 比如訂單系統(tǒng)中有一個(gè)定時(shí)結(jié)算的服務(wù),每?jī)煞昼妶?zhí)行一次。我們可以在定時(shí)任務(wù) JOB 中添加埋點(diǎn),并配置業(yè)務(wù)監(jiān)控,假如十分鐘該定時(shí)任務(wù)沒(méi)有執(zhí)行,則發(fā)送郵件,短信給相關(guān)負(fù)責(zé)人。 擴(kuò)展一筆訂單,在取消的那一刻用戶剛好付款了,怎么辦? 這種情況在正常的業(yè)務(wù)場(chǎng)景中是有可能出現(xiàn)的,因?yàn)橛唵味紩?huì)有定時(shí)取消的邏輯,比如10 分鐘或者 15分鐘,而用戶剛好卡在這個(gè)時(shí)間點(diǎn)進(jìn)行付款,此時(shí)就會(huì)出現(xiàn)兩種情況:
可以看到,不論是哪種情況,其實(shí)都需要做一定的處理,不然用戶肯定會(huì)來(lái)投訴! 這種場(chǎng)景無(wú)非就是支付單支付成功和取消兩種狀態(tài)的“爭(zhēng)奪”,正常情況下,訂單或者支付單都會(huì)有狀態(tài)機(jī)的存在,在當(dāng)前場(chǎng)景簡(jiǎn)單來(lái)說(shuō)有以下兩條路徑:
針對(duì)情況1,如果是支付回調(diào)取勝,此時(shí)的狀態(tài)應(yīng)該已從 支付中->支付成功 針對(duì)情況2,如果是取消支付單取勝,此時(shí)的狀態(tài)應(yīng)該已從 支付中->已取消 所以我們?cè)谛薷闹Ц秵螤顟B(tài)的時(shí)候,基于原始狀態(tài)的判斷,就可以做正常的處理,來(lái)看下 SOL應(yīng)該就很清晰了:
重點(diǎn)就是我們加了 status='paying’這個(gè)條件,這就能保證情況只有一個(gè)能成功,另一個(gè)一定失敗。這種其實(shí)就是樂(lè)觀鎖的方式
業(yè)務(wù)優(yōu)化針對(duì)訂單超時(shí)業(yè)務(wù),這里在業(yè)務(wù)上可以做一個(gè)小優(yōu)化,你想想,用戶付款前可能有點(diǎn)掙扎,然后在最后一刻終于下定決心進(jìn)行付款,這時(shí)候卻告知被退款了,用戶很可能就不會(huì)再下單了。因此我們?cè)陧?yè)面上可以限時(shí)訂單取消設(shè)置計(jì)時(shí)為 10分鐘,但實(shí)際后端是延遲 11 分鐘取消訂單,這樣就能避免這種情況的發(fā)生啦。 Redis 分布式鎖實(shí)現(xiàn)最后除了利用數(shù)據(jù)庫(kù)處理,還可以使用分布式鎖,對(duì)一筆訂單加鎖也能保證這筆訂單正常的業(yè)務(wù)流轉(zhuǎn)。每次進(jìn)行取消訂單或付款操作時(shí),首先嘗試獲取訂單的分布式鎖,確保只有一個(gè)操作能修改訂單狀態(tài)。在分布式系統(tǒng)中,訂單在取消的同時(shí)用戶付款的競(jìng)態(tài)問(wèn)題可以通過(guò)分布式鎖來(lái)解決。以下是一個(gè)具體的、落地的方案,確保訂單狀態(tài)的可靠性,避免因并發(fā)導(dǎo)致?tīng)顟B(tài)沖突 訂單取消流程:
訂單付款流程:
?轉(zhuǎn)自https://www.cnblogs.com/seven97-top/p/18810985 該文章在 2025/4/8 9:10:25 編輯過(guò) |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |