一般電商應用的訂單佇列架構思想
作者:林冠巨集 / 指尖下的幽靈
部落格:http://www.cnblogs.com/linguanh/
GitHub : https://github.com/af913337456/
目錄
- 前序
- 一般的訂單流程
- 思考瓶頸點
- 訂單佇列
- 第一種訂單佇列
- 第二種訂單佇列
- 總結
- 第一種訂單佇列
- 實現佇列的選擇
- 解答
- 實現佇列的選擇
- 第二種佇列的 Go 版本例子程式碼
前序
目前的開發工作主要是將
傳統電商應用
和區塊鏈技術
相結合,區塊鏈平臺依然是以太坊
,此外地,這幾天由我編寫,經清華大學出版社出版的書籍,歷經八月,終於出版上架了,名稱是:《區塊鏈以太坊DApp開發實戰》
,現已可以網購。
本文所要分享的思路就是電商應用中常用的
訂單佇列
。
一般的訂單流程
電商應用中,簡單直觀的使用者從下單到付款,最終完成整個流程的步驟可以用下圖表示:
其中,訂單資訊持久化
,就是儲存資料到資料庫中。而最終客戶端完成支付後的更新訂單狀態
的操作是由第三方支付平臺進行回撥設定好的回撥連結 NotifyUrl
,來進行的。
補全訂單狀態的更新流程,如下圖表示:
思考瓶頸點
服務端的直接瓶頸點
,首先要考慮 TPS
。去除細分點,我們主要看訂單資訊持久化
瓶頸點。
在高併發業務場景中,例如 秒殺
、優惠價搶購
等。短時間內的下單請求數會很多,如果訂單資訊持久化
部分,不做優化,而是直接對資料庫層進行頻繁的
讀寫操作,資料庫會承受不了,容易成為第一個垮掉的服務,比如下圖的所示的常規寫單流程:
可以看到,每
持久化一個訂單資訊,一般要經歷網路連線操作(連結資料庫),以及多個 I/O
操作。
得益於連線池
技術,我們可以在連結資料庫的時候,不用每次都重新發起一次完整的HTTP請求,而可以直接從池中獲取已打開了的連線控制代碼,而直接使用,這點和執行緒池的原理差不多。
此外,我們還可以在上面的流程中加入更多的優化,例如對於一些需要讀取的資訊,可以事先存置到記憶體快取層,並加於更新維護,這樣在使用的時候,可以快速讀取。
即使我們都具備了上述的一些優化手段,但是對於寫操作
的I/O
阻塞耗時,在高併發請求
的時候,依然容易導致資料庫承受不住,容易出現連結多開異常
,操作超時
等問題。
在該層進行優化的操作,除了上面談到的之外,還有下面一些手段:
- 資料庫叢集,採用讀寫分離,減少寫時壓力
- 分庫,不同業務的表放到不同的資料庫,會引入分散式事務問題
- 採用佇列模型削峰
每種方式有各自的特點,因為本文談的是訂單佇列
的架構思想,所以下面我們來看下如何在訂單系統中引入訂單佇列。
訂單佇列
網上有不少文章談到訂單佇列的做法,大部分都漏了說明請求與響應的一致性問題。
第一種訂單佇列
流程圖:
上圖是大多文章提到的佇列模型,有兩個沒有解析的問題:
- 如果訂單存在第三方支付情況,① 和 ② 的一致性如何保證,比如其中一處處理失敗;
- 如果訂單存在第三方支付情況,① 完成了支付,且三方支付平臺回調了
notifyUrl
,而此時 ② 還在排隊等待處理,這種情況又如何處理。
首先,要肯定的是,上面的訂單流程圖是沒有問題的。它有下面的優缺點,所提到的兩個問題也是有解決方案的。
優點:
- 使用者無需等待訂單持久化處理,而能直接獲得響應,實現快速下單
- 持久化處理,採用排隊的先來先處理,不會像上面談到的高併發請求一起衝擊資料庫層面的情況。
- 可變性強,
搭配中介軟體
的組合性強。
缺點:
- 多訂單入隊時,② 步驟的處理速度跟不上。從而導致第二點問題。
- 實現較複雜
上面談及的問題點,我後面都會給出解決方案。下面我們來看下另外一種訂單佇列流程圖。
第二種訂單佇列
流程圖:
第二種訂單佇列的設計模型,注意它的同步等待
持久化處理的結果,解決了持久化與響應的一致性問題,但是有個嚴重的耗時等待問題,它的優缺點如下:
優點:
- 持久化與響應的強一致性。
- 持久化處理,採用排隊的先來先處理,不會像上面談到的高併發請求一起衝擊資料庫層面的情況。
- 實現簡單
缺點:
- 多訂單入隊時,持久化單元處理速度跟不上,造成客戶端同步等待響應。
這類訂單佇列,我下面會放出 Golang
實現的版本程式碼。
總結
對比上面兩種常見的訂單模型,如果從使用者體驗的角度
去優先考慮,第一種不需要使用者等待持久化處理
結果的是明顯優於第二種的。如果技術團隊完善,且技術過硬,也應該考慮第一種的實現方式。
如果僅僅想要達到寧願使用者等待到超時
也不願意儲存層服務被沖垮,那麼有限考慮第二種。
實現佇列的選擇
在這裡,我們進一步細分一下,實現佇列模組的功能有哪些選擇。
相信很多後端開發經驗比較老道的同志已經想到了,使用現有的中介軟體,比如知名的 Redis
、RocketMQ
,以及 Kafka
等,它們都是一種選擇。
此外地,我們還可以直接編寫程式碼,在當前的服務系統中實現一個訊息佇列來達到目的,下面我用圖來分類下佇列型別。
不同的佇列實現方式,能直接導致不同的功能,也有不同的優缺點:
一級快取優點:
- 一級快取,最快。無需連結,直接從記憶體層獲取;
- 如果不考慮持久化和叢集,那麼它實現簡單。
一級快取缺點:
- 如果考慮持久化和叢集,那麼它實現比較複雜。
- 不考慮持久化情況下,如果伺服器斷電或其它原因導致服務中斷,那麼排隊中的訂單資訊將丟失
中介軟體的優點:
- 軟體成熟,一般出名的訊息中介軟體都是經過實踐使用的,文件豐富;
- 支援多種持久化的策略,比如 Redis 有
增量
持久化,能最大程度減少因不可預料的崩潰導致訂單資訊丟失; - 支援叢集,主從同步,這對於分散式系統來說,是必不可少的要求。
中介軟體的缺點:
- 分散式部署時,需要建立連結通訊,導致讀寫操作需要走網路通訊。
解答
回到第一種訂單模型中:
問題1:
如果訂單存在第三方支付情況,① 和 ② 的一致性如何保證?
首先我們看下,不一致性的時候,會產生什麼結果:
- ① 失敗,使用者因為網路原因或返回其它頁面,不能獲取結果。而 ② 成功,那麼最終該訂單的狀態是待支付。使用者進入到個人訂單中心完成訂單支付即可;
- ① 和 ② 都失敗,那麼下單失敗;
- ① 成功,② 失敗,此時使用者在
響應頁面
完成了支付動作,使用者檢視訂單資訊為空白。
上述的情況,明顯地,只有 3 是需要恢復訂單資訊的,應對的方案有:
- 當服務端支付回撥介面被第三方支付平臺訪問時,無法找到對應的訂單資訊。那麼先將這類支付了卻沒訂單資訊的資料儲存起來先,比如儲存到
表A
。同時啟動一個定時任務B
專門遍歷表A,然後去訂單列表尋找是否已經有了對應的訂單資訊,有則更新,沒則繼續,或跟隨制定的檢測策略走。 - 當 ② 是由於服務端的
非崩潰性原因
而導致失敗時:- 失敗的時候同時將原始訂單資料重新插入到
佇列頭部
,等待下一次的重新持久化處理。
- 失敗的時候同時將原始訂單資料重新插入到
- 當 ② 因服務端的
崩潰性
原因而導致失敗時:定時任務B
在進行了多次檢測無果後,那麼根據第三方支付平臺在回撥時候傳遞過來的訂單附屬資訊
對訂單進行恢復。
- 整個過程訂單恢復的過程,使用者檢視訂單資訊為空白。
定時任務B
所在服務最好
和回撥連結notifyUrl
所在的介面服務一致,這樣能保證當 B 掛掉的時候,回撥服務也跟隨掛掉,然後第三方支付平臺在呼叫回撥失敗的情況下,他們會有重試邏輯
,依賴這個,在回撥服務重啟時,可以完成訂單資訊恢復。
問題2:
如果訂單存在第三方支付情況,① 完成了支付,且三方支付平臺回調了 notifyUrl,而此時 ② 還在排隊等待處理,這種情況又如何處理?
應對的方案參考 問題1
的 定時任務B
檢測修改機制。
第二種佇列的 Go 版本例子程式碼
定義一些常量
const (
QueueOrderKey = "order_queue"
QueueBufferSize = 1024 // 請求佇列大小
QueueHandleTime = time.Second * 7 // 單個 mission 超時時間
)
定義出入隊介面,方便多種實現
// 定義出入隊介面,方便多種實現
type IQueue interface {
Push(key string,data []byte) error
Pop(key string) ([]byte,error)
}
定義請求與響應實體
// 定義請求與響應實體
type QueueTimeoutResp struct {
Timeout bool // 超時標誌位
Response chan interface{}
}
type QueueRequest struct {
ReqId string `json:"req_id"` // 單次請求 id
Order *model.OrderCombine `json:"order"` // 訂單資訊 bean
AccessTime int64 `json:"access_time"` // 請求時間
ResponseChan *QueueTimeoutResp `json:"-"`
}
定義佇列實體
// 定義佇列實體
type Queue struct {
mapLock sync.Mutex
RequestChan chan *QueueRequest // 快取管道,裝載請求
RequestMap map[string]*QueueTimeoutResp
Queue IQueue
}
例項化佇列,接收介面引數
// 例項化佇列,接收介面引數
func NewQueue(queue IQueue) *Queue {
return &Queue{
mapLock: sync.Mutex{},
RequestChan: make(chan *QueueRequest, QueueBufferSize),
RequestMap: make(map[string]*QueueTimeoutResp, QueueBufferSize),
Queue: queue,
}
}
接收請求
// 接收請求
func (q *Queue) AcceptRequest(req *QueueRequest) interface{} {
if req.ResponseChan == nil {
req.ResponseChan = &QueueTimeoutResp{
Timeout: false,
Response: make(chan interface{},1),
}
}
userKey := key(req) // 唯一 key 生成函式
req.ReqId = userKey
q.mapLock.Lock()
q.RequestMap[userKey] = req.ResponseChan // 記憶體層儲存對應的 req 的 resp 管道指標
q.mapLock.Unlock()
q.RequestChan <- req // 接收請求
log("userKey : ", userKey)
ticker := time.NewTicker(QueueHandleTime) // 以超時時間 QueueHandleTime 啟動一個定時器
defer func() {
ticker.Stop() // 釋放定時器
q.mapLock.Lock()
delete(q.RequestMap,userKey) // 當處理完一個 req,從 map 中移出
q.mapLock.Unlock()
}()
select {
case <-ticker.C: // 超時
req.ResponseChan.Timeout = true
Queue_TimeoutCounter++ // 輔助計數,int 型別
log("timeout: ",userKey)
return lghError.HandleTimeOut // 返回超時錯誤的資訊
case result := <-req.ResponseChan.Response: // req 被完整處理
return result
}
}
從請求管道中取出 req 放入到佇列容器中,該函式在 gorutine
中執行
// 從請求管道中取出 req 放入到佇列容器中,該函式在 gorutine 中執行
func (q *Queue) addToQueue() {
for {
req := <-q.RequestChan // 取出一個 req
data, err := json.Marshal(req)
if err != nil {
log("redis queue parse req failed : ", err.Error())
continue
}
if err = q.Queue.Push(QueueOrderKey, data);err != nil { // push 入隊,這裡有時間消耗
log("lpush req failed. Error : ", err.Error())
continue
}
log("lpush req success. req time: ", req.AccessTime)
}
}
取出 req 處理,該函式在 gorutine
中執行
// 取出 req 處理,該函式在 gorutine 中執行
func (q *Queue) readFromQueue() {
for {
data, err := q.Queue.Pop(QueueOrderKey) // pop 出隊,這裡也有時間消耗
if err != nil {
log("lpop failed. Error : ", err.Error())
continue
}
if data == nil || len(data) == 0 {
time.Sleep(time.Millisecond * 100) // 空資料的 req,停頓下再取
continue
}
req := &QueueRequest{}
if err = json.Unmarshal(data, req);err != nil {
log("Lpop: json.Unmarshal failed. Error : ", err.Error())
continue
}
userKey := req.ReqId
q.mapLock.Lock()
resultChan, ok := q.RequestMap[userKey] // 取出對應的 resp 管道指標
q.mapLock.Unlock()
if !ok {
// 中介軟體重啟時,比如 redis 重啟而讀取舊 key,會進入這裡
Queue_KeyNotFound ++ // 計數 int 型別
log("key not found, rollback: ", userKey)
continue
}
simulationTimeOutReq4(req) // 模擬出來任務的函式,入參為 req
if resultChan.Timeout {
// 處理期間,已經超時,這裡做可以拓展回滾操作
Queue_MissionTimeout ++
log("handle mission timeout: ", userKey)
continue
}
log("request result send to chan succeee, userKey : ", userKey)
ret := util.GetCommonSuccess(req.AccessTime)
resultChan.Response <- &ret // 輸入處理成功
}
}
啟動
func (q *Queue) Start() {
go q.addToQueue()
go q.readFromQueue()
}
執行例子
func test(){
...
runtime.GOMAXPROCS(4)
redisQueue := NewQueue(NewFastCacheQueue())
redisQueue.Start()
reqNumber := testReqNumber
wg := sync.WaitGroup{}
wg.Add(reqNumber)
for i :=0;i<reqNumber;i++ {
go func(index int) {
combine := model.OrderCombine{}
ret := AcceptRequest(&QueueRequest{
UserId: int64(index),
Order: &combine,
AccessTime: time.Now().Unix(),
ResponseChan: nil,
})
fmt.Println("ret: ------------- ",ret.String())
wg.Done()
}(i)
}
wg.Wait()
time.Sleep(3*time.Second)
fmt.Println("TimeoutCounter: ",Queue_TimeoutCounter,"KeyNotFound: ",Queue_KeyNotFound,"MissionTimeout: ",Queue_MissionTimeout)
}
最後上傳一張書籍圖片
相關推薦
一般電商應用的訂單佇列架構思想
作者:林冠巨集 / 指尖下的幽靈 部落格:http://www.cnblogs.com/linguanh/ GitHub : https://github.com/af913337456/ 目錄 前序 一般的訂單流程 思考瓶頸點 訂單佇列 第一種訂單佇列 第二種訂單佇列 總結 實現佇列的
作為大眾熟知的電商應用,京東如何構建風控體系架構?
作為大眾熟知的電商應用,京東是如何構建堅挺的風控體系架構?如何優化資料的計算和儲存?如何基於裝置做智慧識別的?本文由京東技術專家王美青對以上問題進行解讀。 風控技術體系介紹 風控技術架構 上圖是風控技術架構圖,包括安全模組、風險決策平臺、風險資料洞察模組、風險運營平臺。其中
Java開源生鮮電商平臺-訂單表的設計(源碼可下載)
支付 bsp 後退 們的 ava time 狀態 表的設計 str Java開源生鮮電商平臺-訂單表的設計(源碼可下載) 場景分析說明: 買家(餐館)用戶,通過APP進行選菜,放入購物車,然後下單,最終支付的流程,我們稱為下單過程。 買家可以在張三家買茄子,李四家買蘿蔔,王
6、生鮮電商平臺-訂單表的設計
場景分析說明: 買家(餐館)使用者,通過APP進行選菜,放入購物車,然後下單,最終支付的流程,我們稱為下單過程。 買家可以在張三家買茄子,李四家買蘿蔔,王五家買白菜,趙六家買豬肉等 那麼買家就應該有個訂單主表,我們稱為訂單表,同時還有 上面所說的具體的訂單明細表,清楚的檢視自己買了什麼菜,多少元一斤,買
Flutter新手入門:從零構建電商應用
在這個系列中,我們將學習如何使用google的移動開發框架flutter建立一個電商應用。 本文是flutter框架系列教程的第一部分,將學習如何安裝Flutter開發環境並建立第一個 Flutter應用,並學習Flutter應用開發中的核心概念,例如widget、狀態等。 本系列教程包含如下四
Flutter入門教程:從零構建電商應用(一)
在這個系列中,我們將學習如何使用google的移動開發框架flutter建立一個電商應用。 本文是flutter框架系列教程的第一部分,將學習如何安裝Flutter開發環境並建立第一個 Flutter應用,並學習Flutter應用開發中的核心概念,例如widget、狀態等。 本系列教
【Flutter教程】從零構建電商應用(一)
在這個系列中,我們將學習如何使用google的移動開發框架flutter建立一個電商應用。本文是flutter框架系列教程的第一部分,將學習如何安裝Flutter開發環境並建立第一個Flutter應用,並學習Flutter應用開發中的核心概念,例如widget、狀態等。 本系列教程包含如下四個部分,敬請期待:
【Flutter入門教程】從零構建電商應用(一)
在這個系列中,我們將學習如何使用google的移動開發框架flutter建立一個電商應用。本文是flutter框架系列教程的第一部分,將學習如何安裝Flutter開發環境並建立第一個Flutter應用,並學習Flutter應用開發中的核心概念,例如widget、狀態等。 本系列教程包含如
淘寶雙十一電商秒殺系統架構設計
前言 最近在部門內部分享了原來在電商業務做秒殺活動的整體思路,大家對這次分享反饋還不錯,所以我就簡單整理了一下,分享給大家參考參考 業務介紹 什麼是秒殺?通俗一點講就是網路商家為促銷等目的組織的網上限時搶購活動 比如說京東秒殺,就是一種定時定量秒殺,在規定的時間內
電商中訂單的狀態有哪幾種,請依次說明各個狀態的生命週期
當用戶點選“一鍵購買”或者是從購物車裡點選 “去結算” ,會跳轉到 “核實訂單資訊” 頁面,當全部核實以後點選“提交訂單按鈕”,此時會跳轉到支付頁面,並且訂單提交成功, 此時此刻才算剛剛開始:整個流程如圖(生命週期): 1、訂單提交成功
電商系統訂單分表方案怎麼設計更好
題目背景: 之前做電商運營,打算轉行做開發,參加了幾個面試,幾乎每家都會問到大資料量時的解決方案。暫時不討論問這個題目的合理性,既然有需求,那自己就加強吧。所以基於目前個人做的一個系統,計劃向大資料量做擴充套件設計。 涉及的業務場景: (1)市場中有多個賣家(seller)
蘑菇街電商交易平臺服務架構及改造優化歷程
蘑菇街導購時期 業務結構 蘑菇街是做導購起家的,當時所有的業務都是基於使用者和內容這兩大核心展開。那個時候前臺業務主要做的是社交導購,後臺業務主要做的是內容管理。一句話總結就是小而美的狀態,業務相對來也不是很複雜。 當時的技術架構是典型的創業型公司技術架構
品優購電商系統02------系統架構與使用技術
1.品優購系統架構2. 資料庫表結構3. 框架組合品優購採用當前流行的前後端分離程式設計架構。後端框架採用:Spring +SpringMVC+ MyBatis +Dubbo前端採用:AngularJS
【首度披露】樂視電商雲的整體架構與技術實現
本文根據〖高效運維社群講壇〗線上活動的分享整理而成。 歡迎關注“高效運維(微信ID:greatops)”公眾號,以搶先賞閱乾貨滿滿的各種原創文章。 嘉賓介紹 主題簡介 本次分享將帶大家瞭解電商系統的發展過程,並分析在高速發展期的電商面臨的問題,同時跟大家分享樂視電商雲的架構和實踐方案。
跨境電商系統的一個架構演進
在商城內偶有心得體會,晒出來給後人作為一個可以參考的物件 把以前設計做過得都發出來,給大家參考參考 先上邏輯圖 風.fox 1.0 版 2.0 版 3.0 版 圖片比較大,最好放大了看 4.0 版 邏輯上面沒什麼
java架構師課程、性能調優、高並發、tomcat負載均衡、大型電商項目實戰、高可用、高可擴展、數據庫架構設計、Solr集群與應用、分布式實戰、主從復制、高可用集群、大數據
慢查詢 主從復制 難題 jms 整合 大數 數據庫設計 企業級 nginx網站 15套Java架構師詳情 * { font-family: "Microsoft YaHei" !important } h1 { background-color: #006; color:
JA17-大型電商分布式系統應用實踐+性能優化+分布式應用架構+負載均衡+高並發設計+持久化存儲視頻教程
war height imageview clas 圖片 進步 pac 點滴 blank JA17-大型電商分布式系統應用實踐+性能優化+分布式應用架構+負載均衡+高並發設計+持久化存儲視頻教程 新年伊始,學習要趁早,點滴記錄,學習就是進步! 不要到處找了,抓緊提升自
大資料------電商類網站的大資料應用之使用者畫像的簡單架構搭建
1.大資料時代已經到來,企業希望從使用者行為資料中分析出有價值的東西,利用大資料來分析使用者的行為與消費習慣,可以預測商品的發展的趨勢,提高產品質量,同時提高使用者滿意度。 2.什麼是使用者畫像: 通過不同的維度,去描述一個人,認識一個人,瞭解一個人。使用者畫像也叫使用者
【SSM分散式架構電商專案-27】RabbitMQ的5種佇列
5種佇列 匯入itcast-rabbitmq 簡單佇列 P:訊息的生產者 C:訊息的消費者 紅色:佇列 生產者將訊息傳送到佇列,消費者從佇列中獲取訊息。 匯入RabbitMQ的客戶端依賴 獲取MQ的連線
【.net core】電商平臺升級之微服務架構應用實戰(core-grpc)
## 一、前言 這篇文章本來是繼續分享`IdentityServer4` 的相關文章,由於之前有博友問我關於`微服務`相關的問題,我就先跳過`IdentityServer4`的分享,進行`微服務`相關的技術學習和分享。`微服務`在我的分享目錄裡面是放到四月份開始系列文章分享的,這裡就先穿越下,提前安排`微服務