做者簡介html
大錘,物流運單與服務負責人,同時也是運單系統最先的主力研發,運單系統這些年經歷了數次優化和重構,支撐起現在巨大的體量,大錘功不可沒redis
運單系統是蜂鳥配送系統核心,支撐着全部配送業務。運單系統須要有很好的擴展性和穩定性,以應對互聯網產品千變化萬的更新迭代和大流量下的系統穩定。這幾年隨着蜂鳥業務的不斷髮展,用戶(消費者、商家、騎手、代理商)在產品功能和體驗上不斷提出新的要求。算法
蜂鳥天天會有上千萬的配送單量,每次上游的呼叫配送請求都會對後臺應用發出一系列調用。蜂鳥有多個上游流量入口,包括餓了麼商家呼叫蜂鳥配送、第三方平臺經過開放平臺(open api)接入方式呼叫蜂鳥配送、蜂鳥配送產品跑腿呼叫蜂鳥配送等。上游商戶有餐飲類外賣商戶,新零售超市、生鮮類商戶,零售類淘寶、天貓商戶等不一樣行業商戶的配送需求;各個行業不一樣類型商戶對配送的要求各有不一樣,餐飲類商戶配送要求較即時,配送範圍通常爲商戶附近3千米範圍內,配送時效要求30分鐘左右;零售類商戶配送要求小時級,配送範圍有超10千米;也有配送要求當天送達,全城配送等。蜂鳥組織代理商、衆包和第三方運力完成配送,會根據商戶的配送要求,運力系統狀況將運單分配給合適運力的騎手配送。且不一樣配送場景,運單履約過程各有不一樣,有普通外賣運單的到店、取餐配送,有零售類的前置倉配送,有取送模式的取分離和送分離配送等。一個好的運單系統須要有很好地擴展性和穩定性,運單系統做爲物流基礎模塊須要提早考慮到系統的擴展,爲上層各產品系統提供強大的支撐。sql
運單系統核心是數據和狀態機,架構上分爲流量接入、核心、運力對接、查詢、管理等功能模塊。運單信息主要包括基礎信息、配送信息、狀態信息、起/終點信息、費用信息、屬性/畫像信息等,運單系統負責抽象和定義運單數據結構,如何定義運單數據結構以支撐不一樣配送業務場景的數據存儲是系統設計上的一大難點。運單包括母單和子單,母單跟子單是一對多關係,運單系統根據上游配送請求和相應的惟一標誌生成母單,運單履約過程當中會根據上游的不一樣配送要求和實際運力狀況動態的生成多段子單以接力模式完成整個配送過程。運單定義標準狀態機,根據業務不一樣,定義不一樣的狀態機跳轉,子母單狀態互相影響,不支持逆向狀態機。數據庫
數據存儲上運單存儲在三種數據介質:Mysql、Redis和ES。Mysql數據分爲運單明細數據和運單查詢數據,兩類數據均以sharding方式存儲。由於描述一張運單的信息屬性很是多,運單明細數據經過多張表存儲,包括運單基本信息表、配送信息表、狀態信息表、起/終點信息表、費用信息表、屬性/畫像信息表等。隨着運單系統支持的業務愈來愈多,業務愈來愈複雜,運單數據字段又會根據數據的使用場合將公共可結構化的數據做爲運單屬性存儲,業務方特有非公共屬性字段數據以KV非結構化形式存儲在運單屬性數據表中。運單明細數據按物流商戶ID做爲分區key分爲512片存儲在32個數據庫集羣,每一個集羣一主一備。運單查詢數據庫存儲運單關鍵ID的mapping關係,用於支持實時的多維查詢。Redis按數據塊緩存運單明細數據,支持對一致性要求不是特別高的明細數據查詢。運單數據還會實時按天索引到ES中,支持複雜的較高,近實時的數據檢索和聚合計算。後端
運單明細數據庫按物流商戶ID分片,物流商戶ID是流量方商戶主體在物流側的映射,爲自增ID,因此數據在不一樣的數據塊集羣上分佈很是均勻。api
由於蜂鳥即時配送模式下,單筆運單流轉有很強的地域特性,從用戶下單、到支付、到商家接單、到騎手到店取餐至送達用戶,整個運單的流轉發生在短短的幾十分鐘,一個運單的全生命週期基本可確保都發生在同一個shard(餓了麼多活地域概念,相似省份)。運單查詢數據是按餓了麼多活shard進行分片,由於運單有明確的shard信息,且履單過程當中涉及到的各個角色都有明確的地域特性,因此不一樣地域的請求操做會對應到不一樣的數據塊分片。但由於各個shard的業務量各有不一樣,咱們會根據各個shard的業務量佔比,自定義shard的分區編碼,經過數據庫中間件DAL將shard流量映射到不一樣的數據庫集羣,儘量的保證各個集羣的數據量均勻。緩存
運單接入模塊負責跟上游系統對接,將上游配送請求轉化成物流運單。運單接入模塊是整個物流系統的入口模塊,接入模塊的穩定關乎整個物流,如何高效穩定地接入流量是該模塊設計關鍵。bash
運單接入經過異步方式跟上游系統進行對接,上游系統經過接口方式將呼單請求提交至接入模塊,接口邏輯只作必要的參數驗證,參數驗證經過後接入模塊會將請求參數記錄到數據庫並返回成功。因處理呼單請求的業務邏輯很是複雜,涉及多個內外部接口調用,接入模塊內部經過線程池異步方式處理呼單請求,經過消息將呼單請求處理結果反饋給上游系統。數據結構
設計關鍵點:
運單中心提供基礎的運單業務操做供各個上層系統調用,上游業務系統功能變幻無窮,運單系統如何作到能快速地支持各業務系統功能快速開發迭代又能保證關鍵鏈路的穩定是運單主流程設計的關鍵。
咱們將主流程對運單的操做分爲三類:狀態類、信息類和屬性類。狀態類操做是基於運單基礎狀態機配置依賴方須要的操做,供依賴方操做運單狀態。信息類是配置化提供依賴方修改運單信息的能力來修改運單基礎屬性信息。屬性類是提供方便的數據接口,供依賴方回傳非公共、非結構化的運單數據。運單經過三種流程抽象,基本能夠涵蓋大部分運單操做需求,避免頻繁定製化需求開發。
運單的業務操做底層是對運單數據進行修改,只是不一樣的業務動做操做的字段和對應的業務校驗不一樣而已。咱們將運單數據修改抽象以下(僞代碼)流程:爲了防止併發問題,運單在修改數據過程當中咱們加了分佈式鎖;在鎖內咱們獲取了運單的最新數據對象,而後copy成old和new兩個新的內存對象並存儲在threadlocal中;不一樣的業務邏輯會經過內存操做修改new對象的屬性值,業務邏輯修改的是內存運單new對象,此時並未將修改提交至數據庫;由於old對象描述的是修改前的運單數據,new對象描述的是業務邏輯修改後的運單數據,咱們只須要在內存中compare出兩個對象的變化,就能提煉出本次業務邏輯對運單數據的修改;咱們基於修改的明細數據以最小事物形式提交至運單基礎和查詢數據庫;同時咱們還會觸發運單redis緩存的刪除,但設置的超時極短,避免影響主流程;最後咱們會觸發標準的運單topic消息發送。
lock(單號) {
...
1. get and copy
2. 業務邏輯
3. compare
4. db
5. redis
6. MQ
...
}
複製代碼
運單基礎狀態機定義運單最細粒度的可跳轉狀態,僅容許正向不可跳躍的狀態流轉。因爲業務的多樣化,運單不只須要提供基礎的運單狀態操做接口,還須要提供同狀態或跨越式運單狀態操做接口,且須要保證操做的原子性。如騎手端須要支持騎手快速取餐和轉單業務,快速取餐業務場景是運單還未分配騎手,騎手直接到店將運單取走進行配送,對於運單須要支持運單狀態從待分配騎手到騎手取餐配送中的狀態跳轉;轉單業務是支持騎手間轉單,對於運單屬於同狀態跳轉,有可能當前運單是待到店、待取餐或配送中。要支持如上兩個業務場景運單基礎操做動做沒法知足,若業務方自行組裝業務邏輯串行調用運單基礎操做接口,邏輯上沒法保證業務動做的原子性。相似業務場景較多且很是雜,如何作到運單即不理解業務又能支持花式的業務邏輯是狀態類運單流程抽象的關鍵。
首先咱們封裝了運單的基礎操做動做,如:accept()、assign()、arrival()等幾個基礎的運單操做,每一個基礎的運單操做都會定義標準的輸入、內部業務校驗、數據影響。將不一樣的業務場景抽象成不一樣的操做code,配置操做code容許的起始狀態和終止狀態,內部執行時咱們會根據基礎狀態機串行執行基礎運單操做,同時咱們會merge基礎運單操做的參數描述對應到操做code。這樣,業務方有不一樣的業務需求時,咱們只須要配置業務操做容許的起終點狀態,生成業務操做code和對應的參數描述,經過公共api調用傳入對應的操做code和參數便可完成業務調用。
基礎運單操做示例:
assign(a, b) {
//業務邏輯
}
arrival(b, c) {
//業務邏輯
}
fetch(b, d) {
//業務邏輯
}
...
複製代碼
快速取餐業務操做配置示例:
業務操做code | 起點狀態列表 | 終點狀態列表 | 參數列表 |
---|---|---|---|
quick_fetch | [assign] | [fetch] | a,b,c,d |
... | [...] | [...] | ... |
代碼邏輯執行示例:
state_api(code, orderid, map{a, b, c, d}) {
...
lock(orderid) {
1. get and copy
2.{
assign(a, b);
arrival(b, c);
fetch(b, d);
}
3. compare
4. db
5. redis
6. MQ
}
...
}
複製代碼
運單信息修改類操做主要應對業務場景須要修改運單基礎屬性信息的需求,如業務場景須要修改用戶電話號碼或商家經緯度等。運單內部定義運單可修改域對應的基礎修改方法和參數描述,業務場景code只須要配置業務操做code跟可修改域之間的關聯關係便可,一個業務場景需修改多個數據域只須要關聯多個可便可。
屬性域基礎操做方法示例:
customer(a, b) {
Order.Customer.class.getMethod("setA", Object.class).invoke(new.getCustomer(), a);
Order.Customer.class.getMethod("setB", Object.class).invoke(new.getCustomer(), b);
}
merchant(c, d) {
Order.Merchant.class.getMethod("setC", Object.class).invoke(new.getMerchant(), c);
Order.Merchant.class.getMethod("setD", Object.class).invoke(new.getMerchant(), d);
}
...
複製代碼
業務修改操做配置示例:
業務操做code | 操做列表 | 參數列表 |
---|---|---|
modify_customer | [customer] | a,b |
modify_merchant | [merchant] | c,d |
modify_customer_and_merchant | [customer, merchant] | a,b,c,d |
... | [...] | ... |
代碼邏輯執行示例:
modify_api(code, orderid, map{a, b, c, d}) {
...
lock(orderid) {
1. get and copy
2.{
customer(a, b);
merchant(c, d);
}
3. compare
4. db
5. redis
6. MQ
}
...
}
複製代碼
運單做爲物流履約的數據基礎,業務方不少場合依賴運單存儲一些個性化數據,咱們將這類數據存儲在運單的一個單獨kv數據塊中,以非結構化方式儲存。爲了防止kv數據種類過多,不被業務方濫用,key由運單側定義,當需求方須要添加新的key時須要申請,運單側確認合理性後方可線上使用。另外,因爲需求方添加屬性場景很是多,咱們要求需求方根據業務場景定義key,同時支持追加方式添加key數據,儘量把一類數據存儲在一塊,避免kv數據氾濫。同時,咱們也會根據數據使用場合在運單查詢時結構化部分數據的返回,避免多方使用公共kv數據時各方都須要理解數據結構而進行解析,數據查詢篇幅會詳細講解kv數據的查詢邏輯。
kv方法示例:
addition(orderid, key, value) {
//add kv
}
append_addition(orderid, key, map\<string, string\>) {
//merge kv
}
...
複製代碼
代碼邏輯執行示例:
append_addition_api(orderid, key, map{a, b, c, d}) {
...
lock(orderid) {
1. get and copy
2.{
append\_additiont(orderid, key, map{a, b, c, d});
}
3. compare
4. db
5. redis
6. MQ
}
...
}
複製代碼
閱讀博客還不過癮?
時間: 2018年12月30日
地點:餓了麼上海總部:普陀區近鐵城市廣場北座5樓榴蓮酥
這次活動邀請到了物流團隊的6位重量級嘉賓。不只會有先後端大佬分享最新的架構、算法在物流團隊的落地實戰經驗,更有 P10 大佬教你如何在業務開發中得到技術成長。固然,也會有各類技術書籍,記念品拿到手軟,最後最重要的一點,徹底免費!還等什麼,趕快點擊 etech.ele.me/salon.html?… 瞭解更多細節並報名吧!
歡迎你們掃二維碼經過添加羣助手,加入交流羣,討論和博客有關的技術問題,還能夠和博主有更多互動
![]()
博客轉載、線下活動及合做等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通