前序
一般的訂單流程
思考瓶頸點
訂單隊列
第一種訂單隊列流程圖:
第二種訂單隊列流程圖:
總結(jié)
實現(xiàn)隊列的選擇
解答
第二種隊列的 Go 版本例子代碼
前序
本文所要分享的思路就是電商應(yīng)用中常用的訂單隊列。
一般的訂單流程
電商應(yīng)用中,簡單直觀的用戶從下單到付款,最終完成整個流程的步驟可以用下圖表示:
其中,訂單信息持久化,就是存儲數(shù)據(jù)到數(shù)據(jù)庫中。而最終客戶端完成支付后的更新訂單狀態(tài)的操作是由第三方支付平臺進(jìn)行回調(diào)設(shè)置好的回調(diào)鏈接?NotifyUrl,來進(jìn)行的。
補(bǔ)全訂單狀態(tài)的更新流程,如下圖表示:
思考瓶頸點
服務(wù)端的直接瓶頸點,首先要考慮?TPS。去除細(xì)分點,我們主要看訂單信息持久化瓶頸點。
在高并發(fā)業(yè)務(wù)場景中,例如?秒殺、優(yōu)惠價搶購等。短時間內(nèi)的下單請求數(shù)會很多,如果訂單信息持久化?部分,不做優(yōu)化,而是直接對數(shù)據(jù)庫層進(jìn)行頻繁的讀寫操作,數(shù)據(jù)庫會承受不了,容易成為第一個垮掉的服務(wù),比如下圖的所示的常規(guī)寫單流程:
可以看到,每持久化一個訂單信息,一般要經(jīng)歷網(wǎng)絡(luò)連接操作(鏈接數(shù)據(jù)庫),以及多個?I/O?操作。
得益于連接池技術(shù),我們可以在鏈接數(shù)據(jù)庫的時候,不用每次都重新發(fā)起一次完整的HTTP請求,而可以直接從池中獲取已打開了的連接句柄,而直接使用,這點和線程池的原理差不多。
此外,我們還可以在上面的流程中加入更多的優(yōu)化,例如對于一些需要讀取的信息,可以事先存置到內(nèi)存緩存層,并加于更新維護(hù),這樣在使用的時候,可以快速讀取。
即使我們都具備了上述的一些優(yōu)化手段,但是對于寫操作的I/O阻塞耗時,在高并發(fā)請求的時候,依然容易導(dǎo)致數(shù)據(jù)庫承受不住,容易出現(xiàn)鏈接多開異常,操作超時等問題。
在該層進(jìn)行優(yōu)化的操作,除了上面談到的之外,還有下面一些手段:
數(shù)據(jù)庫集群,采用讀寫分離,減少寫時壓力
分庫,不同業(yè)務(wù)的表放到不同的數(shù)據(jù)庫,會引入分布式事務(wù)問題
采用隊列模型削峰
每種方式有各自的特點,因為本文談的是訂單隊列的架構(gòu)思想,所以下面我們來看下如何在訂單系統(tǒng)中引入訂單隊列。
訂單隊列
網(wǎng)上有不少文章談到訂單隊列的做法,大部分都漏了說明請求與響應(yīng)的一致性問題。
第一種訂單隊列流程圖:
上圖是大多文章提到的隊列模型,有兩個沒有解析的問題:
如果訂單存在第三方支付情況,① 和 ② 的一致性如何保證,比如其中一處處理失??;
如果訂單存在第三方支付情況,① 完成了支付,且三方支付平臺回調(diào)了notifyUrl,而此時 ② 還在排隊等待處理,這種情況又如何處理。
首先,要肯定的是,上面的訂單流程圖是沒有問題的。它有下面的優(yōu)缺點,所提到的兩個問題也是有解決方案的。
優(yōu)點:
用戶無需等待訂單持久化處理,而能直接獲得響應(yīng),實現(xiàn)快速下單
持久化處理,采用排隊的先來先處理,不會像上面談到的高并發(fā)請求一起沖擊數(shù)據(jù)庫層面的情況。
可變性強(qiáng),搭配中間件的組合性強(qiáng)。
缺點:
多訂單入隊時,② 步驟的處理速度跟不上。從而導(dǎo)致第二點問題。
實現(xiàn)較復(fù)雜
上面談及的問題點,我后面都會給出解決方案。下面我們來看下另外一種訂單隊列流程圖。
第二種訂單隊列流程圖:
第二種訂單隊列的設(shè)計模型,注意它的同步等待持久化處理的結(jié)果,解決了持久化與響應(yīng)的一致性問題,但是有個嚴(yán)重的耗時等待問題,它的優(yōu)缺點如下:
優(yōu)點:
持久化與響應(yīng)的強(qiáng)一致性。
持久化處理,采用排隊的先來先處理,不會像上面談到的高并發(fā)請求一起沖擊數(shù)據(jù)庫層面的情況。
實現(xiàn)簡單
缺點:
多訂單入隊時,持久化單元處理速度跟不上,造成客戶端同步等待響應(yīng)。
這類訂單隊列,我下面會放出?Golang?實現(xiàn)的版本代碼。
總結(jié)
對比上面兩種常見的訂單模型,如果從用戶體驗的角度去優(yōu)先考慮,第一種不需要用戶等待持久化處理結(jié)果的是明顯優(yōu)于第二種的。如果技術(shù)團(tuán)隊完善,且技術(shù)過硬,也應(yīng)該考慮第一種的實現(xiàn)方式。
如果僅僅想要達(dá)到寧愿用戶等待到超時也不愿意存儲層服務(wù)被沖垮,那么有限考慮第二種。
實現(xiàn)隊列的選擇
在這里,我們進(jìn)一步細(xì)分一下,實現(xiàn)隊列模塊的功能有哪些選擇。
相信很多后端開發(fā)經(jīng)驗比較老道的同志已經(jīng)想到了,使用現(xiàn)有的中間件,比如知名的?Redis、RocketMQ,以及?Kafka?等,它們都是一種選擇。
此外地,我們還可以直接編寫代碼,在當(dāng)前的服務(wù)系統(tǒng)中實現(xiàn)一個消息隊列來達(dá)到目的,下面我用圖來分類下隊列類型。
不同的隊列實現(xiàn)方式,能直接導(dǎo)致不同的功能,也有不同的優(yōu)缺點:
一級緩存優(yōu)點:
一級緩存,最快。無需鏈接,直接從內(nèi)存層獲??;
如果不考慮持久化和集群,那么它實現(xiàn)簡單。
一級緩存缺點:
如果考慮持久化和集群,那么它實現(xiàn)比較復(fù)雜。
不考慮持久化情況下,如果服務(wù)器斷電或其它原因?qū)е路?wù)中斷,那么排隊中的訂單信息將丟失
中間件的優(yōu)點:
軟件成熟,一般出名的消息中間件都是經(jīng)過實踐使用的,文檔豐富;
支持多種持久化的策略,比如 Redis 有增量持久化,能最大程度減少因不可預(yù)料的崩潰導(dǎo)致訂單信息丟失;
支持集群,主從同步,這對于分布式系統(tǒng)來說,是必不可少的要求。
中間件的缺點:
分布式部署時,需要建立鏈接通訊,導(dǎo)致讀寫操作需要走網(wǎng)絡(luò)通訊。
解答
回到第一種訂單模型中:
問題1:
如果訂單存在第三方支付情況,① 和 ② 的一致性如何保證?
首先我們看下,不一致性的時候,會產(chǎn)生什么結(jié)果:
① 失敗,用戶因為網(wǎng)絡(luò)原因或返回其它頁面,不能獲取結(jié)果。而 ② 成功,那么最終該訂單的狀態(tài)是待支付。用戶進(jìn)入到個人訂單中心完成訂單支付即可;
① 和 ② 都失敗,那么下單失?。?/p>
① 成功,② 失敗,此時用戶在響應(yīng)頁面完成了支付動作,用戶查看訂單信息為空白。
上述的情況,明顯地,只有 3 是需要恢復(fù)訂單信息的,應(yīng)對的方案有:
當(dāng)服務(wù)端支付回調(diào)接口被第三方支付平臺訪問時,無法找到對應(yīng)的訂單信息。那么先將這類支付了卻沒訂單信息的數(shù)據(jù)存儲起來先,比如存儲到表A。同時啟動一個定時任務(wù)B專門遍歷表A,然后去訂單列表尋找是否已經(jīng)有了對應(yīng)的訂單信息,有則更新,沒則繼續(xù),或跟隨制定的檢測策略走。
當(dāng) ② 是由于服務(wù)端的`非崩潰性原因而導(dǎo)致失敗時:
失敗的時候同時將原始訂單數(shù)據(jù)重新插入到隊列頭部,等待下一次的重新持久化處理。
當(dāng) ② 因服務(wù)端的`崩潰性原因而導(dǎo)致失敗時:
定時任務(wù)B在進(jìn)行了多次檢測無果后,那么根據(jù)第三方支付平臺在回調(diào)時候傳遞過來的訂單附屬信息對訂單進(jìn)行恢復(fù)。
整個過程訂單恢復(fù)的過程,用戶查看訂單信息為空白。
定時任務(wù)B?所在服務(wù)最好和回調(diào)鏈接?notifyUrl?所在的接口服務(wù)一致,這樣能保證當(dāng) B 掛掉的時候,回調(diào)服務(wù)也跟隨掛掉,然后第三方支付平臺在調(diào)用回調(diào)失敗的情況下,他們會有重試邏輯,依賴這個,在回調(diào)服務(wù)重啟時,可以完成訂單信息恢復(fù)。
問題2:
如果訂單存在第三方支付情況,① 完成了支付,且三方支付平臺回調(diào)了 notifyUrl,而此時 ② 還在排隊等待處理,這種情況又如何處理?
應(yīng)對的方案參考?問題1?的?定時任務(wù)B?檢測修改機(jī)制,或者是另加一個定時任務(wù)主動發(fā)起查詢的機(jī)制。