訂單系統(tǒng)設(shè)計(jì)
我們每天都在使用網(wǎng)上進(jìn)行下單,購買各種各樣的商品,作為一名后端服務(wù)的程序員,不知道你有沒有好奇地想過,在網(wǎng)上下單后,后臺(tái)流程應(yīng)當(dāng)是如何進(jìn)行訂單處理的,這是訂單是又是如何生成的,又是如何推送到下游的各個(gè)系統(tǒng)的,以及在這個(gè)過程中,訂單系統(tǒng)是如何保證系統(tǒng)低延遲、高性能、高可用的,尤其是不出現(xiàn)丟單、錯(cuò)單的問題。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

當(dāng)然,在設(shè)計(jì)日萬級和日千萬級訂單系統(tǒng)的架構(gòu)時(shí),肯定是不一樣的,我們針對這兩種不同量級的架構(gòu)設(shè)計(jì)分開來講解。我們討論的思路是這樣的:首先設(shè)計(jì)一個(gè)簡單的訂單系統(tǒng),然后分析哪幾個(gè)環(huán)節(jié)會(huì)丟單,以及面對日萬級的訂單量如何重構(gòu)優(yōu)化,最后分析下,面對日千萬級的訂單量時(shí),系統(tǒng)應(yīng)該如何升級改造。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
如何設(shè)計(jì)一個(gè)簡單的訂單系統(tǒng)
這個(gè)訂單系統(tǒng)很簡單,只有一個(gè)前臺(tái)和后臺(tái),前臺(tái)有結(jié)算頁提供用戶去結(jié)算,當(dāng)后臺(tái)收到前臺(tái)用戶點(diǎn)擊去結(jié)算的操作時(shí),就會(huì)開始處理下單服務(wù)。
起初,訂單被寫入到后臺(tái)的數(shù)據(jù)庫中,然后異構(gòu)數(shù)據(jù)到緩存中,以此提供用戶在“我的訂單”中進(jìn)行訂單查詢,當(dāng)用戶支付完成后,收銀臺(tái)發(fā)送消息給下單的服務(wù),進(jìn)行數(shù)據(jù)庫和緩存中的訂單狀態(tài)的修改。這樣,一個(gè)簡單的訂單系統(tǒng)就完成了。

不過,真實(shí)的訂單系統(tǒng)還會(huì)有更多的業(yè)務(wù),使得系統(tǒng)更加的復(fù)雜,前面只是一個(gè)示例。
接下來我們再看看這個(gè)系統(tǒng)中,哪些環(huán)節(jié)可能會(huì)出現(xiàn)丟單的問題。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
其實(shí),我們現(xiàn)在的后臺(tái)系統(tǒng)研發(fā)技術(shù)和架構(gòu)設(shè)計(jì)已經(jīng)很成熟了,但如果是系統(tǒng)掛了,就是掛了,那就不是丟單的問題。所以,要考慮丟單的問題,我認(rèn)為,關(guān)鍵點(diǎn)應(yīng)該聚焦在寫數(shù)據(jù)庫、接收和發(fā)送訂單消息的這些環(huán)節(jié)上。
不過,如果是代碼問題,把訂單寫丟了,那估計(jì)誰也沒辦法了,只能好好地去檢查代碼。這里,我總結(jié)幾個(gè)在設(shè)計(jì)訂單系統(tǒng)時(shí)避免丟單的要點(diǎn):
一個(gè)是:關(guān)鍵邏輯不要使用讀寫分離的查詢方式,避免從庫同步延遲造成訂單查詢異常,比如創(chuàng)建訂單之后要?jiǎng)?chuàng)建支付單。但是,在反查訂單的時(shí)候由于主從延遲,沒查詢到訂單信息,這就可能會(huì)造成創(chuàng)建支付單失敗。

另外,關(guān)鍵邏輯也不要使用緩存來進(jìn)行訂單的查詢,這樣做,同時(shí)是為了避免因?yàn)榫彺嫜舆t造成訂單反查的失敗。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

還有一個(gè)點(diǎn)是,訂單補(bǔ)償不要粗暴地使用消息隊(duì)列的方式,避免中間件引發(fā)的訂單丟失。比如在進(jìn)行訂單狀態(tài)的修改時(shí),如果處理失敗,就將這個(gè)訂單信息插入到消息隊(duì)列中,重新消費(fèi),以此完成訂單的補(bǔ)償,這種方式在發(fā)送消息和接收消息時(shí)有可能存在丟消息的情況。

最后一個(gè)點(diǎn)是,接收消息處理失敗時(shí)一定要讓消息重試,避免丟失,尤其注意 return、continue 等關(guān)鍵字。比如,一次消費(fèi)多條訂單記錄,一條條地進(jìn)行處理,如果修改訂單成功,就繼續(xù)處理下一條;如果修改失敗,可能會(huì)因?yàn)?retrun 或 cotinue 關(guān)鍵字將其余的消息都丟失掉了。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

如何設(shè)計(jì)一個(gè)支持日萬級的訂單系統(tǒng)呢?
考慮到前面可能丟單的問題,以及系統(tǒng)的穩(wěn)定性和可用性,我們要如何進(jìn)行系統(tǒng)的重構(gòu)優(yōu)化?注意這里的用詞是“重構(gòu)優(yōu)化”,說明我們之前的設(shè)計(jì)其實(shí)是很容易支撐日萬級的訂單系統(tǒng)的。所以,你只要注意幾個(gè)關(guān)鍵點(diǎn)就可以了:
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
一個(gè)是:注意寫數(shù)據(jù)庫時(shí),數(shù)據(jù)庫事務(wù)的粒度,不要太大,避免鎖表,關(guān)注慢 SQL。比如,最不要犯的錯(cuò)誤,就是在數(shù)據(jù)庫事務(wù)里同時(shí)去更新其他數(shù)據(jù)源,或發(fā)送 MQ 消息等,這不僅不能保證數(shù)據(jù)一致性,還會(huì)把數(shù)據(jù)庫的連接耗盡。

另外需要注意的是:關(guān)注數(shù)據(jù)異構(gòu)的性能和穩(wěn)定性,尤其在網(wǎng)絡(luò)抖動(dòng)的情況下,可能會(huì)影響用戶體驗(yàn)。最后,要關(guān)注訂單系統(tǒng)的冪等性,避免出現(xiàn)計(jì)費(fèi)等錯(cuò)誤,影響后續(xù)操作等流程。
做好這幾個(gè)點(diǎn),前面的架構(gòu)方案,基本就可以滿足并支撐一個(gè)日萬級訂單量的訂單系統(tǒng)了。
現(xiàn)在我們來看看如何設(shè)計(jì)一個(gè)支持日千萬級的訂單系統(tǒng),日千萬級和日萬級的訂單系統(tǒng)關(guān)鍵點(diǎn)的差別主要在于一個(gè)量,由于量的增大,造成系統(tǒng)負(fù)載過重,導(dǎo)致服務(wù)最終宕機(jī)。那我們先來具體分析一下,前面的架構(gòu)中,哪些是系統(tǒng)的瓶頸點(diǎn)呢?
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
首先,前面的架構(gòu)設(shè)計(jì)過重依賴于數(shù)據(jù)庫,而且這個(gè)數(shù)據(jù)庫還是訂單庫,持續(xù)讀寫請求會(huì)給數(shù)據(jù)庫造成很大的壓力,比如,修改訂單狀態(tài)時(shí)就需要反查數(shù)據(jù)庫,并進(jìn)行訂單狀態(tài)的更新,這些操作在高并發(fā)寫請求下,會(huì)造成數(shù)據(jù)庫資源的搶占,從而影響系統(tǒng)的穩(wěn)定性。

其次,為了避免數(shù)據(jù)不一致,請求訪問主要集中在主庫,這樣主庫壓力就會(huì)很大,因此,就需要實(shí)現(xiàn)分庫分表的部署架構(gòu),下單服務(wù)為此也必須改造支持分庫分表的架構(gòu)設(shè)計(jì),但由于熱點(diǎn)數(shù)據(jù)的存在,可能導(dǎo)致數(shù)據(jù)庫出現(xiàn)數(shù)據(jù)傾斜問題,引發(fā)提早的數(shù)據(jù)庫擴(kuò)容。

還有,由于下單服務(wù)耦合業(yè)務(wù)過重,使得即使是多集群的部署架構(gòu),也很難實(shí)現(xiàn)快速的處理響應(yīng),更何況不同業(yè)務(wù)的訂單處理流程還不一樣,使得系統(tǒng)維護(hù)性也會(huì)越來愈差。比如,創(chuàng)建訂單時(shí)由于業(yè)務(wù)不同,數(shù)碼、3C、圖書等訂單包含的信息是不一樣的,這就需要特殊處理,這種特殊處理邏輯與創(chuàng)建訂單耦合在一起,系統(tǒng)自然就會(huì)變得越來越慢。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

最后,由于數(shù)據(jù)庫存儲(chǔ)量的增大,還會(huì)導(dǎo)致數(shù)據(jù)異構(gòu)性能的直線下降,以及緩存存儲(chǔ)容量的不斷擴(kuò)大,這都會(huì)極大的影響查詢的性能,而且,還可能出現(xiàn)業(yè)務(wù)間的相互影響等問題。
總地來說,前面架構(gòu)存在著這樣幾個(gè)問題:下單服務(wù)處理接單慢;數(shù)據(jù)庫壓力大;數(shù)據(jù)異構(gòu)延遲高、緩存數(shù)據(jù)質(zhì)量差等,那我們要如何解決呢?
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
為了應(yīng)對日千萬級的訂單量,我們將下單服務(wù)進(jìn)行了服務(wù)拆分,使用單獨(dú)的接單服務(wù)處理接單,使用訂單引擎和訂單管道處理訂單業(yè)務(wù)邏輯,改用雙寫和數(shù)據(jù)補(bǔ)償?shù)姆绞教幚砭彺?,使用緩存過期的方式控制數(shù)據(jù)量。接下來我們就具體來分析一下實(shí)現(xiàn)方案。

當(dāng)用戶在結(jié)算頁點(diǎn)擊提交訂單之后,接單服務(wù)會(huì)在同一個(gè)事務(wù)里,將訂單插入接單庫,將首任務(wù)插入任務(wù)庫,再交由訂單引擎進(jìn)行任務(wù)調(diào)度。什么是任務(wù)?任務(wù)就是執(zhí)行訂單操作的步驟,比如寫訂單緩存、減庫存、發(fā)送訂單通知等,以及前面提到不同的特殊業(yè)務(wù)流程,這些都是一個(gè)個(gè)的任務(wù)。我們通過將整個(gè)訂單處理流程分解成一個(gè)一個(gè)的任務(wù),逐個(gè)單獨(dú)處理,來應(yīng)對日千萬級的訂單處理壓力。

其中,接單庫為多臺(tái)數(shù)據(jù)庫,通過隨機(jī)的方式寫入,之所以沒有采用哈希等算法,其原因在于擴(kuò)展能力更具靈活性,當(dāng)遇到流量洪峰來臨,新增數(shù)臺(tái)數(shù)據(jù)庫,對寫入邏輯是無感的。接單庫采用一主多從的部署架構(gòu),當(dāng)某一臺(tái)機(jī)器故障,可以通過快速切換主從,或者摘除故障機(jī)等手段進(jìn)行修復(fù)。

而其中的任務(wù)庫由訂單引擎驅(qū)動(dòng)執(zhí)行,任務(wù)通過訂單引擎的服務(wù)編排能力生成任務(wù)隊(duì)列,首任務(wù)執(zhí)行成功之后,會(huì)插入第二個(gè)任務(wù),或者,會(huì)同時(shí)插入第二個(gè)和第三個(gè)任務(wù)。如果插入任務(wù)失敗,訂單引擎會(huì)重新執(zhí)行當(dāng)前任務(wù),執(zhí)行成功之后,繼續(xù)執(zhí)行插入操作,這里就需要每個(gè)任務(wù)的業(yè)務(wù)處理都需要保證冪等性。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

剛才說的是任務(wù)的創(chuàng)建方式,接下來說說任務(wù)的線程調(diào)度方式。任務(wù)使用多線程的異步方式進(jìn)行調(diào)度,并根據(jù)配置選擇是串行執(zhí)行還是并發(fā)執(zhí)行。

有一個(gè)點(diǎn)不知道你注意到?jīng)]有,前面說了任務(wù)由線程調(diào)度執(zhí)行,那么,如果任務(wù)執(zhí)行失敗,訂單引擎是如何重新執(zhí)行失敗任務(wù)的呢?這就是通過任務(wù)狀態(tài)機(jī)來實(shí)現(xiàn)的,任務(wù)狀態(tài)機(jī)就如同一個(gè)系統(tǒng)的守護(hù)線程,任務(wù)狀態(tài)機(jī)通過識(shí)別任務(wù)的狀態(tài),來判斷每個(gè)任務(wù)是執(zhí)行完成,還是執(zhí)行失敗的,并根據(jù)狀態(tài)來進(jìn)行任務(wù)調(diào)度,并且,會(huì)多次執(zhí)行失敗的任務(wù),重試調(diào)度的頻次也會(huì)逐漸遞減,當(dāng)超過一定的重試次數(shù)之后,就會(huì)告警通知人工干預(yù)。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

其實(shí),訂單引擎真正執(zhí)行調(diào)度遠(yuǎn)程服務(wù)的并非是由訂單引擎來調(diào)度的,而是由訂單引擎調(diào)度訂單管道,訂單管道去調(diào)度遠(yuǎn)程真實(shí)的服務(wù)來執(zhí)行的,其原因在于任務(wù)引擎本身就是多線程的設(shè)計(jì)架構(gòu),對線程占用就比較高,而遠(yuǎn)程調(diào)度會(huì)注冊很多的服務(wù),服務(wù)調(diào)度也會(huì)啟動(dòng)多線程去執(zhí)行,如果共同部署在同一系統(tǒng),就會(huì)出現(xiàn)線程數(shù)過多,造成 CPU 飆升的情況。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641

接下來,我們再來說下訂單緩存的實(shí)現(xiàn)策略。接單服務(wù)在處理完一些業(yè)務(wù)邏輯之后,最后調(diào)用下單服務(wù)提交訂單到訂單中心,而在此之前,為了保證訂單的及時(shí)性,在插入訂單和任務(wù)之后,接單服務(wù)會(huì)先將訂單通過接口寫入到訂單中心的緩存中,以支持用戶在支付之后,在“我的訂單”列表中能立即查詢到我的訂單。
總地來說,訂單中心接到下單服務(wù)之后,會(huì)將訂單落庫,再同步更新緩存,在后續(xù)訂單中心接收到臺(tái)賬的消息后,也會(huì)同時(shí)更新數(shù)據(jù)庫和緩存,將訂單狀態(tài)更新為“訂單完成”。
接下來,我對日千萬級的訂單系統(tǒng)架構(gòu)再概括講一下,用戶在結(jié)算頁點(diǎn)擊結(jié)算,結(jié)算頁調(diào)用后臺(tái)的接單服務(wù);接單服務(wù)接收到下單請求之后,會(huì)負(fù)責(zé)接單,并將訂單插入到接單庫,同時(shí)在一個(gè)事務(wù)里將首任務(wù)插入任務(wù)庫,并通知調(diào)度起訂單引擎開始執(zhí)行任務(wù)。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
訂單引擎,根據(jù)任務(wù)編號依次進(jìn)行任務(wù)調(diào)度,更新任務(wù)狀態(tài),并由任務(wù)狀態(tài)機(jī)進(jìn)行任務(wù)校驗(yàn)補(bǔ)償處理。訂單引擎通過調(diào)度訂單管道,實(shí)現(xiàn)真實(shí)服務(wù)的遠(yuǎn)程調(diào)度,訂單管道請求服務(wù)之后,將處理結(jié)果返回給任務(wù)引擎。最后,訂單中心會(huì)在接單服務(wù)創(chuàng)建訂單時(shí),異步寫一份訂單緩存在訂單中心,然后再通過數(shù)據(jù)異構(gòu)的方式,再次寫一份數(shù)據(jù)在訂單緩存中。

了解完訂單的處理流程之后,我們再來分析一下整個(gè)流程是如何保證下單的高性能和高可用的。
整個(gè)訂單系統(tǒng)接單的核心流程,幾乎全是同步執(zhí)行,只有少數(shù)任務(wù),比如發(fā)送訂單通知給下游系統(tǒng)是采用消息異步的方式執(zhí)行,以此來保證訂單流程的高性能。而整個(gè)處理過程,基于訂單引擎的調(diào)度,通過服務(wù)流程編排確定一個(gè)訂單的執(zhí)行步驟,并有效地保證每個(gè)環(huán)節(jié)的正確執(zhí)行,避免訂單丟單、卡單等異常問題的出現(xiàn),進(jìn)而保證訂單流程的高可用。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
總結(jié)
交易平臺(tái)一直是各個(gè)公司的核心系統(tǒng)之一,涉及到數(shù)據(jù)流與資金流的流轉(zhuǎn),通俗說,就是錢從你這里走。那如何做到交易平臺(tái)的高可用,是著實(shí)需要在細(xì)節(jié)點(diǎn)上下足功夫的。 我們知道,秒殺系統(tǒng)的場景會(huì)在某一時(shí)間點(diǎn)或某一時(shí)間段,產(chǎn)生大量的訂單,那我們今天講到的系統(tǒng)架構(gòu)是否可以支撐呢?實(shí)際情況中,秒殺系統(tǒng)和正常的下單流程,在細(xì)節(jié)點(diǎn)上還是有很多不同的,就以減庫存的設(shè)計(jì)為例,比如日萬級的秒殺系統(tǒng),采用數(shù)據(jù)庫減庫存的方式就可以,如果日千萬級的秒殺系統(tǒng)實(shí)現(xiàn)自然要復(fù)雜了,那么要如何實(shí)現(xiàn)呢?你可以再仔細(xì)想一想。
學(xué)習(xí)更多,請點(diǎn)擊:https://space.bilibili.com/701029654
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?https://space.bilibili.com/216317641
作者:傾城之夏
鏈接:https://juejin.cn/post/6945372268029542437
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。