物流訂單能力做爲基礎能力,須要設計一套穩定的訂單模型,以及一套可以在高併發環境下持續可用的接口。這些接口做爲原子接口,供上層業務複用。上層業務不管多麼複雜,經過這些原子接口,最終都會收斂到穩定的訂單模型中來,這也是區分基礎能力和產品服務的一個重要的邊界。html
本文經過如下5點來介紹如何構建一套物流訂單能力:算法
一、模型設計spring
二、狀態機設計sql
三、高併發建立接口數據庫
四、高併發更新接口express
五、高併發查詢接口架構
首先來看ER模型併發
一共四張表,主模型是logistics_order、logistics_order_package和logistics_order_item表,logistics_order_unique是去重表。異步
描述:物流訂單主單表,整張表大概分爲如下幾部分信息高併發
表結構設計
字段名稱
|
字段類型
|
是否必填
|
描述
|
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
描述:物流子單表,主要存儲要發貨的商品信息,整張表大概分爲如下幾部分信息
表設計
字段名稱
|
字段類型
|
是否必填
|
描述
|
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
描述:物流包裹,是對物流商品的包裝。這張表主要是爲了拆單場景使用。拆單場景有不少種,好比同一個訂單下的不一樣商品發往不一樣地址,你們電商品拆分發貨,商品分倉發貨等等。總之,每個包裹都對應一個運單號,都有對應的發貨地和收貨地以及物流詳情。
表設計
字段名稱
|
字段類型
|
是否必填
|
描述
|
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
描述:物流去重表,用於建立的時候去重,具體做用會在第四節介紹。
字段名稱
|
字段類型
|
是否必填
|
描述
|
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
正向物流包含了三條主要流程:
a、建立->發貨->簽收/拒籤
這種是最簡單的流程,也是用戶最關心的流程,若是公司使用的是第三方物流系統,那麼只要這條狀態流就足夠了。
b、建立->發貨->配送接單->配送攬收->配送派送->簽收/拒籤
這條狀態流對接了配送的物流流轉狀態,通常對接第三方物流詳情後,會獲得物流配送的信息。
c、建立->發貨->倉庫接單->倉庫出庫->配送攬收->配送派送->簽收/拒籤
這條狀態流是最複雜的,包含了倉庫和配送,通常只有大公司纔會考慮這麼細緻的狀態流轉。
由上面的狀態機能夠看出來,取消物流的時機有4種:
一、建立後取消
二、發貨後取消
三、倉庫接單後出庫前取消
四、配送接單後簽收前取消
上面第三種和第四種情況也叫倉截單和配截單,須要配合WMS系統和TMS系統進行特別開發。
在整個交易物流業務流程中,物流訂單的建立是銜接交易和物流的關鍵環節。從系統架構上來講,首先交易和物流必須經過消息解耦,這樣能夠對交易中心的高流量進行削峯,減小物流訂單中心的壓力,其次,物流訂單中心必須提供高併發下穩定的建立接口,並且須要支持冪等。
爲此,咱們設計了以下的高併發建立流程:
這個ID必須提早生成,不能使用數據庫自增ID,緣由一個是後面訂單中心數據庫不可避免的會進行分庫分表,提早經過全局生成能夠規避後面遷移數據的風險,第二是提早生成ID能夠將ID存入去重表,這樣高併發下,多餘的建立請求能夠直接從去重表拿到訂單ID,而不須要走後面的流程。
惟一去重碼必須惟一識別一次請求,咱們經過業務單號+業務類型做爲去重碼,並構建惟一索引,保證高併發下不會重複建立。
因爲建立訂單流量很是大,因此除了必要的插入數據操做,其餘業務操做必須經過消息異步化。爲了保證消息必定可以發出去,咱們會使用MQ的消息事務保證。消息事務的原理能夠參考這篇文章:www.codeceo.com/article/dis…
數據庫事務就不用說了,可使用spring的事務模板。
這裏經過惟一去重碼的惟一索引保證建立的惟一性,若是插入失敗而且是數據庫惟一索引異常,則經過惟一去重碼去查去重表的數據,把裏面的物流訂單ID拿出來直接返回,若是是其餘異常,則直接拋異常回滾事務,不然插入去重表。
基本的數據庫操做,這一步若是出錯,會回滾整個事務。
經過mq發送訂單建立消息,這一步出錯,按照上面的文章中的介紹,MQ會主動回調系統,驗證是不是數據庫插入成功消息沒發,若是是則會把該條消息設置爲已提交,從而保證消息發送成功。
經過上面的流程,咱們能夠保證物流訂單的高併發冪等建立。
物流訂單中心承載了整個物流域的狀態流轉,對於物流訂單中心的更新也會比較多。平均來講,一筆物流訂單在整個生命週期中,會有10到20次更新,當物流訂單很是多的時候,更新的量是很是可觀的,所以,咱們須要設計出一套高併發的更新接口。
咱們在設計數據庫表的時候,每每會加上version字段,這個字段就是用來作版本鎖的,版本鎖的流程以下:
對於更新來講,有些字段會頻繁更新,好比狀態,有些字段則較少更新。對於頻繁更新的字段,若是使用版本鎖,就會致使大量版本衝突,從而會影響其餘字段的更新。所以,咱們能夠對狀態更新單獨設計一個status_version字段,更新狀態只會使用這個字段,即便狀態更新衝突,也不影響其餘字段的更新,從而提升更新效率。
爲了使鎖分離,咱們須要在接口層面設計兩套接口,一套是通用的更新接口,用於全量更新字段,一套是相似狀態這樣的特殊字段的更新接口。
在實踐中,咱們發現更新接口被誤用的狀況,好比數據徹底一致,也進行更新接口的調用,這些調用到數據庫層面僅僅是改了下gmt_modified字段,沒有任何其餘做用。對於這些誤調用,咱們經過更新字段的比對,將它們擋掉,這樣就減小了一部分數據庫的壓力。
物流訂單中心做爲物流領域的核心,其餘業務系統幾乎所有會依賴到物流訂單,物流訂單的查詢接口調用量每每會很是大,物流訂單能夠說是整個業務的單點,一旦物流訂單中心掛了,影響會很是大。所以,咱們必須設計高併發下的訂單查詢接口。
首先是數據庫層面的優化,具體能夠參考這篇文章:www.jianshu.com/p/cd033668f…
物流訂單庫不可避免的會涉及到分庫分表,在進行分庫分表的時候須要注意三點:
a、物流訂單ID全局生成
物流訂單ID全局生成能夠參考雪花算法或者阿里TDDL的方法
b、選擇合適的分表字段
分表字段是用來作路由的,所以必須選擇必定會有的字段,好比買家ID。
c、sql語句儘可能不要跨表
一旦分庫分表,對於一些複雜的sql查詢必須進行拆分,不然會影響性能。若是沒法拆分,則須要遷移到搜索引擎中。
物流訂單數據通常會分紅熱點數據和冷數據,熱點數據是最近生成的訂單,這些訂單還處於業務流轉中,冷數據是那些歷史數據,通常查詢量很是小。咱們能夠按照必定規則,把歷史數據遷移到Hbase保存,數據庫只留下熱點數據,從而減小數據庫的數據量。對於歷史數據,咱們須要提供歷史數據的查詢接口。
咱們在設計查詢接口的時候,設計一個LogisticsOrderQuery對象,其中包含查詢條件,以及一些開關:
isIncludePackage:這個開關告訴接口是否把包裹信息查出來
isIncludeItems:這個開關告訴接口是否把物流商品查出來
經過這些開關,能夠減小數據的查詢量,減輕數據庫壓力。
當上面的策略都沒法增長併發量的時候,咱們還剩最後一招,那就是加機器。可是,加機器也不是隨便加的,爲了更科學的利用自有,咱們把集羣分爲讀集羣和寫集羣,經過dubbo的接口路由規則,把讀流量分配到讀集羣,寫流量分配到寫集羣,咱們根據讀寫請求的峯值進行集羣的容量規劃,動態擴容。
經過上面的介紹,咱們基本介紹完了一個物流訂單系統涉及到的技術要點,咱們能夠看出來,對於基礎能力相關的系統,每每對技術要求比較高,它們聚焦的是高併發下穩定、可靠的系統表現,而不是業務需求,這也是爲何中臺思想中要把系統分爲基礎能力系統和業務產品系統。接下來的一系列文章,我會逐一介紹其餘基礎能力系統,以及產品服務系統的設計要點,最後會把這兩種系統串起來,再次講一下基於中臺思想的系統設計。
更多文章歡迎訪問 http://www.apexyun.com/
聯繫郵箱:public@space-explore.com
(未經贊成,請勿轉載)