一、引言
物流訂單能力作為基礎能力,需要設計一套穩定的訂單模型,以及一套能夠在高併發環境下持續可用的介面。這些介面作為原子介面,供上層業務複用。上層業務無論多麼複雜,通過這些原子介面,最終都會收斂到穩定的訂單模型中來,這也是區分基礎能力和產品服務的一個重要的邊界。
本文通過以下5點來介紹如何構建一套物流訂單能力:
1、模型設計
2、狀態機設計
3、高併發建立介面
4、高併發更新介面
5、高併發查詢介面
二、物流訂單資料模型設計
首先來看ER模型
一共四張表,主模型是logistics_order、logistics_order_package和logistics_order_item表,logistics_order_unique是去重表。
1、logistics_order
描述:物流訂單主單表,整張表大概分為以下幾部分資訊
表結構設計
欄位名稱
|
欄位型別
|
是否必填
|
描述
|
id
|
bigint
|
必填
|
主鍵
|
lg_order_code
|
varchar(128)
|
必填
|
物流單號
|
trade_order_code
|
varchar(128)
|
非必填
|
交易單號
|
receiver_id
|
bigint
|
非必填
|
收貨人ID
|
receiver_name
|
varchar(64)
|
非必填
|
收貨人姓名
|
receiver_telephone
|
varchar(32)
|
非必填
|
收貨人電話
|
receiver_province
|
varchar(32)
|
非必填
|
收貨人省份
|
receiver_city
|
varchar(64)
|
非必填
|
收貨人城市
|
receiver_area
|
varchar(64)
|
非必填
|
收貨人地區
|
receiver_street
|
varchar(64)
|
非必填
|
收貨人街道
|
receiver_address
|
varchar(1024)
|
非必填
|
收貨人詳細地址
|
receiver_address_code
|
varchar(32)
|
非必填
|
四級地址編碼
|
sender_id
|
bigint
|
非必填
|
發貨人ID
|
sender_name
|
varchar(64)
|
非必填
|
發貨人姓名
|
sender_telephone
|
varchar(32)
|
非必填
|
發貨人電話
|
sender_province
|
varchar(32)
|
非必填
|
發貨人省份
|
sender_city
|
varchar(64)
|
非必填
|
發貨人城市
|
sender_area
|
varchar(64)
|
非必填
|
發貨人地區
|
sender_street
|
varchar(64)
|
非必填
|
發貨人街道
|
sender_address
|
varchar(1024)
|
非必填
|
發貨人詳細地址
|
sender_address_code
|
varchar(32)
|
非必填
|
四級地址編碼
|
buyer_id
|
bigint
|
必填
|
買家ID
|
buyer_name
|
varchar(64)
|
非必填
|
買家暱稱
|
seller_id
|
bigint
|
非必填
|
賣家ID
|
seller_name
|
varchar(64)
|
非必填
|
賣家暱稱
|
parent_lg_order_code
|
varchar(128)
|
非必填
|
父物流單號
|
biz_type
|
varchar(32)
|
必填
|
業務型別
|
order_origin
|
int
|
非必填
|
訂單來源
|
order_type
|
int
|
必填
|
訂單型別
|
status
|
int
|
必填
|
狀態
|
mailno
|
varchar(256)
|
非必填
|
運單號
|
express_code
|
varchar(32)
|
非必填
|
快遞公司編碼
|
express_name
|
varchar(32)
|
非必填
|
快遞公司名稱
|
is_delete
|
int
|
必填
|
是否刪除
|
feature
|
varchar(1024)
|
非必填
|
擴充套件欄位,JSON格式
|
version
|
int
|
非必填
|
版本號,用於樂觀鎖
|
gmt_created
|
datetime
|
必填
|
建立時間
|
gmt_modified
|
datetime
|
必填
|
編輯時間
|
索引設計:
a)、主鍵id
b)、普通索引欄位:lg_order_code、buyer_id
2、logistics_order_item
描述:物流子單表,主要儲存要發貨的商品資訊,整張表大概分為以下幾部分資訊
表設計
欄位名稱
|
欄位型別
|
是否必填
|
描述
|
id
|
bigint
|
必填
|
主鍵
|
lg_order_code
|
varchar(128)
|
必填
|
物流單號
|
trade_order_code
|
varchar(128)
|
非必填
|
交易單號
|
trade_sub_order_code
|
varchar(128)
|
非必填
|
交易子單號
|
package_id
|
bigint
|
非必填
|
包裹ID
|
sku_id
|
bigint
|
非必填
|
skuid
|
sku_name
|
varchar(256)
|
非必填
|
sku名稱
|
buyer_id
|
bigint
|
必填
|
買家ID
|
seller_id
|
bigint
|
非必填
|
賣家ID
|
shop_id
|
bigint
|
非必填
|
店鋪ID
|
item_id
|
bigint
|
必填
|
商品ID
|
item_type
|
int
|
非必填
|
商品型別
|
item_name
|
varchar(256)
|
非必填
|
商品名稱
|
item_num
|
int
|
必填
|
商品數量
|
item_weight
|
decimal
|
非必填
|
商品重量
|
item_volumn
|
decimal
|
非必填
|
商品體積
|
marking
|
varchar(128)
|
非必填
|
商品標籤資訊
|
status
|
int
|
必填
|
狀態
|
feature
|
varchar(1024)
|
非必填
|
擴充套件欄位
|
is_delete
|
int
|
必填
|
是否刪除
|
version
|
int
|
必填
|
版本號
|
gmt_created
|
datetime
|
必填
|
建立時間
|
gmt_modified
|
datetime
|
必填
|
修改時間
|
索引設計:
a)、主鍵id
b)、普通索引欄位:lg_order_code、buyer_id
3、logistics_order_pacakge
描述:物流包裹,是對物流商品的包裝。這張表主要是為了拆單場景使用。拆單場景有很多種,比如同一個訂單下的不同商品發往不同地址,大家電商品拆分發貨,商品分倉發貨等等。總之,每一個包裹都對應一個運單號,都有對應的發貨地和收貨地以及物流詳情。
表設計
欄位名稱
|
欄位型別
|
是否必填
|
描述
|
id
|
bigint
|
必填
|
主鍵
|
lg_order_code
|
varchar(128)
|
必填
|
物流單號
|
trade_order_code
|
varchar(128)
|
非必填
|
交易單號
|
receiver_id
|
bigint
|
非必填
|
收貨人ID
|
receiver_name
|
varchar(64)
|
非必填
|
收貨人姓名
|
receiver_telephone
|
varchar(32)
|
非必填
|
收貨人電話
|
receiver_province
|
varchar(32)
|
非必填
|
收貨人省份
|
receiver_city
|
varchar(64)
|
非必填
|
收貨人城市
|
receiver_area
|
varchar(64)
|
非必填
|
收貨人地區
|
receiver_street
|
varchar(64)
|
非必填
|
收貨人街道
|
receiver_address
|
varchar(1024)
|
非必填
|
收貨人詳細地址
|
receiver_address_code
|
varchar(32)
|
非必填
|
四級地址編碼
|
sender_id
|
bigint
|
非必填
|
發貨人ID
|
sender_name
|
varchar(64)
|
非必填
|
發貨人姓名
|
sender_telephone
|
varchar(32)
|
非必填
|
發貨人電話
|
sender_province
|
varchar(32)
|
非必填
|
發貨人省份
|
sender_city
|
varchar(64)
|
非必填
|
發貨人城市
|
sender_area
|
varchar(64)
|
非必填
|
發貨人地區
|
sender_street
|
varchar(64)
|
非必填
|
發貨人街道
|
sender_address
|
varchar(1024)
|
非必填
|
發貨人詳細地址
|
sender_address_code
|
varchar(32)
|
非必填
|
四級地址編碼
|
buyer_id
|
bigint
|
必填
|
買家ID
|
seller_id
|
bigint
|
非必填
|
賣家ID
|
shop_id
|
bigint
|
非必填
|
店鋪ID
|
mailno
|
varchar(256)
|
非必填
|
運單號
|
express_code
|
varchar(32)
|
非必填
|
快遞公司編碼
|
express_name
|
varchar(32)
|
非必填
|
快遞公司名稱
|
pacakge_type
|
int
|
必填
|
包裹型別
|
status
|
int
|
必填
|
狀態
|
feature
|
varchar(1024)
|
非必填
|
擴充套件欄位
|
is_delete
|
int
|
必填
|
是否刪除
|
version
|
int
|
必填
|
版本號
|
gmt_created
|
datetime
|
必填
|
建立時間
|
gmt_modified
|
datetime
|
必填
|
修改時間
|
索引設計:
a)、主鍵id
b)、普通索引欄位:lg_order_code、buyer_id
4、logistics_order_unique
描述:物流去重表,用於建立的時候去重,具體作用會在第四節介紹。
欄位名稱
|
欄位型別
|
是否必填
|
描述
|
id
|
bigint
|
必填
|
主鍵
|
unique_code
|
varchar(196)
|
必填
|
去重單號
|
trade_code
|
varchar(128)
|
必填
|
業務單號
|
biz_type
|
varchar(32)
|
必填
|
業務型別
|
lg_order_id
|
bigint
|
必填
|
物流單主鍵ID
|
buyer_id
|
bigint
|
必填
|
買家ID
|
gmt_created
|
datetime
|
必填
|
建立時間
|
gmt_modified
|
datetime
|
必填
|
修改時間
|
索引設計
主鍵:id
唯一索引:unique_code
三、狀態機的設計
1、正向物流狀態機設計
正向物流包含了三條主要流程:
a、建立->發貨->簽收/拒籤
這種是最簡單的流程,也是使用者最關心的流程,如果公司使用的是第三方物流系統,那麼只要這條狀態流就足夠了。
b、建立->發貨->配送接單->配送攬收->配送派送->簽收/拒籤
這條狀態流對接了配送的物流流轉狀態,一般對接第三方物流詳情後,會得到物流配送的資訊。
c、建立->發貨->倉庫接單->倉庫出庫->配送攬收->配送派送->簽收/拒籤
這條狀態流是最複雜的,包含了倉庫和配送,一般只有大公司才會考慮這麼細緻的狀態流轉。
2、逆向物流狀態機設計
由上面的狀態機可以看出來,取消物流的時機有4種:
1、建立後取消
2、發貨後取消
3、倉庫接單後出庫前取消
4、配送接單後簽收前取消
上面第三種和第四種狀況也叫倉截單和配截單,需要配合WMS系統和TMS系統進行特別開發。
四、高併發下的訂單建立介面設計
在整個交易物流業務流程中,物流訂單的建立是銜接交易和物流的關鍵環節。從系統架構上來說,首先交易和物流必須通過訊息解耦,這樣可以對交易中心的高流量進行削峰,減少物流訂單中心的壓力,其次,物流訂單中心必須提供高併發下穩定的建立介面,而且需要支援冪等。
為此,我們設計瞭如下的高併發建立流程:
1、生成物流訂單ID
這個ID必須提前生成,不能使用資料庫自增ID,原因一個是後面訂單中心資料庫不可避免的會進行分庫分表,提前通過全域性生成可以規避後面遷移資料的風險,第二是提前生成ID可以將ID存入去重表,這樣高併發下,多餘的建立請求可以直接從去重表拿到訂單ID,而不需要走後面的流程。
2、構建唯一去重碼
唯一去重碼必須唯一識別一次請求,我們通過業務單號+業務型別作為去重碼,並構建唯一索引,保證高併發下不會重複建立。
3、開啟訊息事務
由於建立訂單流量非常大,所以除了必要的插入資料操作,其他業務操作必須通過訊息非同步化。為了保證訊息一定能夠發出去,我們會使用MQ的訊息事務保證。訊息事務的原理可以參考這篇文章:www.codeceo.com/article/dis…
4、開啟資料庫事務
資料庫事務就不用說了,可以使用spring的事務模板。
5、插入去重表
這裡通過唯一去重碼的唯一索引保證建立的唯一性,如果插入失敗並且是資料庫唯一索引異常,則通過唯一去重碼去查去重表的資料,把裡面的物流訂單ID拿出來直接返回,如果是其他異常,則直接拋異常回滾事務,否則插入去重表。
6、插入訂單資料
基本的資料庫操作,這一步如果出錯,會回滾整個事務。
7、傳送訊息
通過mq傳送訂單建立訊息,這一步出錯,按照上面的文章中的介紹,MQ會主動回撥系統,驗證是否是資料庫插入成功訊息沒發,如果是則會把該條訊息設定為已提交,從而保證訊息傳送成功。
通過上面的流程,我們可以保證物流訂單的高併發冪等建立。
五、高併發下的訂單更新介面設計
物流訂單中心承載了整個物流域的狀態流轉,對於物流訂單中心的更新也會比較多。平均來說,一筆物流訂單在整個生命週期中,會有10到20次更新,當物流訂單非常多的時候,更新的量是非常可觀的,因此,我們需要設計出一套高併發的更新介面。
1、使用版本鎖保證資料不被覆蓋
我們在設計資料庫表的時候,往往會加上version欄位,這個欄位就是用來做版本鎖的,版本鎖的流程如下:
2、鎖分離
對於更新來說,有些欄位會頻繁更新,比如狀態,有些欄位則較少更新。對於頻繁更新的欄位,如果使用版本鎖,就會導致大量版本衝突,從而會影響其他欄位的更新。因此,我們可以對狀態更新單獨設計一個status_version欄位,更新狀態只會使用這個欄位,即使狀態更新衝突,也不影響其他欄位的更新,從而提高更新效率。
為了使鎖分離,我們需要在介面層面設計兩套介面,一套是通用的更新介面,用於全量更新欄位,一套是類似狀態這樣的特殊欄位的更新介面。
3、資料比對
在實踐中,我們發現更新介面被誤用的情況,比如資料完全一致,也進行更新介面的呼叫,這些呼叫到資料庫層面僅僅是改了下gmt_modified欄位,沒有任何其他作用。對於這些誤呼叫,我們通過更新欄位的比對,將它們擋掉,這樣就減少了一部分資料庫的壓力。
六、高併發下的訂單查詢介面設計
物流訂單中心作為物流領域的核心,其他業務系統幾乎全部會依賴到物流訂單,物流訂單的查詢介面呼叫量往往會非常大,物流訂單可以說是整個業務的單點,一旦物流訂單中心掛了,影響會非常大。因此,我們必須設計高併發下的訂單查詢介面。
1、資料庫層面優化
首先是資料庫層面的優化,具體可以參考這篇文章:www.jianshu.com/p/cd033668f…
2、分庫分表
物流訂單庫不可避免的會涉及到分庫分表,在進行分庫分表的時候需要注意三點:
a、物流訂單ID全域性生成
物流訂單ID全域性生成可以參考雪花演算法或者阿里TDDL的方法
b、選擇合適的分表欄位
分表欄位是用來做路由的,因此必須選擇一定會有的欄位,比如買家ID。
c、sql語句儘量不要跨表
一旦分庫分表,對於一些複雜的sql查詢必須進行拆分,否則會影響效能。如果無法拆分,則需要遷移到搜尋引擎中。
3、資料分離
物流訂單資料一般會分成熱點資料和冷資料,熱點資料是最近生成的訂單,這些訂單還處於業務流轉中,冷資料是那些歷史資料,一般查詢量非常小。我們可以按照一定規則,把歷史資料遷移到Hbase儲存,資料庫只留下熱點資料,從而減少資料庫的資料量。對於歷史資料,我們需要提供歷史資料的查詢介面。
4、查詢介面優化
我們在設計查詢介面的時候,設計一個LogisticsOrderQuery物件,其中包含查詢條件,以及一些開關:
isIncludePackage:這個開關告訴介面是否把包裹資訊查出來
isIncludeItems:這個開關告訴介面是否把物流商品查出來
通過這些開關,可以減少資料的查詢量,減輕資料庫壓力。
5、叢集讀寫分離
當上面的策略都無法增加併發量的時候,我們還剩最後一招,那就是加機器。但是,加機器也不是隨便加的,為了更科學的利用自有,我們把叢集分為讀叢集和寫叢集,通過dubbo的介面路由規則,把讀流量分配到讀叢集,寫流量分配到寫叢集,我們根據讀寫請求的峰值進行叢集的容量規劃,動態擴容。
七、總結
通過上面的介紹,我們基本介紹完了一個物流訂單系統涉及到的技術要點,我們可以看出來,對於基礎能力相關的系統,往往對技術要求比較高,它們聚焦的是高併發下穩定、可靠的系統表現,而不是業務需求,這也是為什麼中臺思想中要把系統分為基礎能力系統和業務產品系統。接下來的一系列文章,我會逐一介紹其他基礎能力系統,以及產品服務系統的設計要點,最後會把這兩種系統串起來,再次講一下基於中臺思想的系統設計。
更多文章歡迎訪問 http://www.apexyun.com/
聯絡郵箱:public@space-explore.com
(未經同意,請勿轉載)