秒殺場景下訂單中心的架構設計
不管是普通場景的下單,還是秒殺場景的下單,對訂單中心來說,都是下單,關鍵是要能支撐秒殺瞬間大量的下單請求。本文探討一下通用的訂單中心架構,主要從服務劃分、下單請求處理流程、核心表分庫等方面來介紹,不區分普通下單還是秒殺下單,系統架構設計做好了,有秒殺活動時,無非進行一些擴容、限流、降級等手段即可應對。
服務劃分
服務說明
我不希望整個訂單中心就是一個巨大的單體服務,也不希望是太細的微服務,我希望訂單中心是合理的“中服務”的組合。
可針對不同場景對服務進行擴容,比如訂單搜索請求量比較大,就適當增加訂單搜索服務的實例數量;消費速度慢,可針對訂單消費者服務進行優化,調整服務實例數量。
上面劃分的每一個服務都是獨立部署運行的服務。
服務 | 說明 |
order-core(訂單核心服務) | 負責訂單的業務處理,直接和 DB 交互 |
order-search(訂單搜索服務) | 負責訂單索引的維護和搜索,直接和 ES 交互 |
order-job(訂單調度服務) | 訂單超時取消等調度任務 |
order-consumer(訂單消費者服務) | 消費訂單相關消息,如下單消息、訂單索引更新消息 |
order-manage(訂單管理后臺系統) | 訂單管理后臺,數據來源于 ES 和從庫 |
應用架構圖
服務調用關系圖
提交訂單服務調用關系:
- 鏈路 1.1 ~ 1.3 提交訂單
- 鏈路 2.1 ~ 2.4 消費下單消息(下單業務處理)
- 鏈路 3.1 ~ 3.2 查詢下單結果
- BFF(小程序的后端,負責聚合和適配)
后臺服務調用關系:
- 后臺連接獨立的專有讀庫,與前臺隔離,不能因為后臺的查詢影響前臺的操作
- 后臺的訂單查詢可以調用搜索服務和讀庫來完成
- 后臺的增刪改操作調用 order-core(訂單核心服務)進行操作,不能直接操作數據庫
訂單搜索或查看訂單詳情服務調用關系:
- 訂單列表或者搜索訂單可以調 order-search(訂單搜索服務)來完成
- 在訂單列表店家訂單明細,可以根據訂單號由 order-core(訂單核心服務)查詢從庫來完成
訂單調度服務調用關系:
- 訂單調度服務查詢專有的讀庫
- 寫操作調用 order-core(訂單核心服務)查詢來完成
下單流程
下單請求通過 MQ 異步化處理,下單處理結果存入 Redis,前端輪詢下單結果。
步驟說明
步驟 1:提交訂單說明
- order-core(訂單核心服務)提供提交訂單接口(/order/submit)
- 這個接口接收訂單相關的參數,如商品 id、價格、數量等
- 接收到請求后,做好基本參數校驗
- 生成唯一的訂單號,組裝基本的訂單信息
- 將訂單號和訂單創建中狀態存入 Redis
- 發送 MQ,然后將訂單號返回給前端
步驟 2:消費下單請求說明
- order-consumer(訂單消費者服務)獲取到下單請求消息后,調庫存中心接口進行庫存預占
- 如果庫存不足,預占失敗,則將訂單創建失敗狀態和失敗信息更新到 Redis,流程終止
- 如果庫存充足,預占成功,則將訂單創建成功狀態更新到 Redis
- 調用 order-core(訂單核心服務)保存訂單信息到數據庫,調用 order-search(訂單搜索服務)對訂單進行索引
- 發送創建訂單結果消息,庫存中心根據創單結果消息進行庫存扣減或者釋放
步驟 3:根據訂單號查詢輪詢下單結果說明
- order-core(訂單核心服務)提供根據訂單號查詢訂單是否已創建接口(/order/is-created)
- 該接口返回報文應包括,訂單號、訂單創建狀態(創建中,創建成功,創建失敗)、創建失敗原因
- 前端定時輪詢該接口,查詢訂單是否創建成功,輪詢頻率可根據實際情況進行調整,比如 20ms 一次
- 輪詢到訂單創建成功,可直接喚起支付,失敗則直接提示失敗信息
核心表分庫
- 以訂單主表和訂單明細表為例進行分庫設計,假如按 32 個庫進行分庫。
- 訂單主表和訂單明細表通過訂單號進行關聯。
- 分庫要求:
某個用戶的所有訂單在同一個庫,避免跨庫查詢(可根據用戶 id——buyerId 定位到分庫編號)
某個商家的所有訂單在同一個庫,避免跨庫查詢(可根據商家 id——sellerId 定位到分庫編號)
可以根據訂單號查詢(可根據訂單號定位到分庫編號)
- 按照以上分庫要求,做出以下分庫設計
訂單主表進行冗余,訂單主表分成用戶訂單主表(buyer_order)和商家訂單主表(seller_order)
用戶訂單主表(buyer_order)按照 buyerId % 32 進行分庫
商家訂單主表(seller_order)按照 sellerId %32 進行分庫
訂單號末位帶上分庫編號,分庫編號為 buyerId % 32
訂單明細表(order_detail)按照訂單號進行分庫,確保同一個訂單的明細在同一個庫
用戶訂單主表(buyer_order)同步寫入,因為訂單是由用戶發起的,需要保證實時性。
商家訂單主表(seller_order)建議保證最終一致性即可,可根據實際業務選擇同步雙寫或者通過 MQ 異步寫入
- 分庫設計圖:
庫存扣減方案
- 采用預占庫存方案:創建訂單時預占庫存
- 庫存不足,預占失敗,下單失敗
- 庫存足夠,預占成功,創建訂單
- 訂單創建成功,扣減庫存;創建訂單失敗或者取消訂單,釋放庫存
庫存扣減序列圖
庫存設置到 redis 中,已 skuId 為 key,變化的數量為值,如:
- 將 skuId=10086 的庫存值初始化為 100,redis.incrby(10086, 100)
- 庫存初始化后,只能對庫存進行加減操作,不允許做覆蓋操作
Redis 如何與數據庫中的庫存保持一致:
- Redis 和數據庫的庫存保持最終一致性
- 庫存被預占時,生成庫存預占流水,關鍵字段有,訂單號、skuId、預占數量、流水狀態有(預占中、已扣減、已釋放),預占超時時間,同時可以在 Redis 或者數據庫中維護一個 skuId 對應的總預占數量字段,總預占數量 + 預占數量
- 訂單中心發送庫存扣減消息,庫存中心消費消息時更新庫存流水狀態為已扣減,總預占數量 - 預占數量
- 訂單中心發送庫存釋放消息,庫存中心消費消息時更新庫存流水狀態為已釋放,返還庫存到 Redis,總預占數量 - 預占數量
/**
* 預占庫存 偽代碼
* @param orderNo 訂單號
* @param skuId sku 標識
* @param quantity 預占數量
*/
boolean preOccupy(String orderNo, String skuId, int quantity) {
boolean isPreOccupySuccess = false;
int value = redis.decrby(skuId, quantity);
if (value >= 0) {
// 庫存充足
// 生成庫存預占流水記錄
//(關鍵字段:orderNo,skuId,quantity,state(0-預占中;1-已扣減;2-已釋放),timeout(超時時間)
isPreOccupySuccess = true;
} else {
// 庫存不足,返還剛才預占的庫存
redis.incrby(skuId, qunatity);
}
return isPreOccupySuccess;
}
數據庫的庫存數量禁止覆蓋更新!
數據庫的庫存數量禁止覆蓋更新!
扣減庫存偽 SQL:update stock set stock_num = stock_num - 變化的值 where sku_id = 10086
關于釋放庫存
對一些釋放異常的情況,可由庫存中心調度服務,找出庫存預占流水狀態為預占中且預占超時的記錄,根據訂單號向訂單中心確認該訂單號的庫存是已扣減還是已釋放,再進行相應業務處理。
其他
除了以上大的方面設計,分布式事務、冪等、補償、壓測……這些點是大家在設計系統時都需要考慮的,不在本文討論范圍。