vivo全球商城:電商交易平臺設(shè)計
一、背景
vivo官方商城經(jīng)過了七年的迭代,從單體架構(gòu)逐步演進到微服務(wù)架構(gòu),我們的開發(fā)團隊沉淀了許多寶貴的技術(shù)與經(jīng)驗,對電商領(lǐng)域業(yè)務(wù)也有相當深刻的理解。
去年初,團隊承接了O2O商城的建設(shè)任務(wù),還有即將成立的禮品中臺,以及官方商城的線上購買線下門店送貨需求,都需要搭建底層的商品、交易和庫存能力。
為節(jié)約研發(fā)與運維成本,避免重復(fù)造輪子,我們決定采用平臺化的思想來搭建底層系統(tǒng),以通用能力靈活支撐上層業(yè)務(wù)的個性化需求。
包括交易平臺、商品平臺、庫存平臺、營銷平臺在內(nèi)的一整套電商平臺化系統(tǒng)應(yīng)運而生
本文將介紹交易平臺的架構(gòu)設(shè)計理念與實踐,以及上線后持續(xù)迭代過程中的挑戰(zhàn)與思考。
二、整體架構(gòu)
2.1 架構(gòu)目標
除了高并發(fā)、高性能、高可用這三高外,還希望做到:
- 低成本
注重模型與服務(wù)的可重用性,靈活支撐各業(yè)務(wù)的個性化需求,提高開發(fā)效率,降低人力成本。 - 高擴展
系統(tǒng)架構(gòu)簡單清晰,應(yīng)用系統(tǒng)間耦合低,容易水平擴展,業(yè)務(wù)功能增改方便快捷。
2.2 系統(tǒng)架構(gòu)
(1)電商平臺整體架構(gòu)中的交易平臺
(2)交易平臺系統(tǒng)架構(gòu)
2.3 數(shù)據(jù)模型
三、關(guān)鍵方案設(shè)計
3.1 多租戶設(shè)計
(1)背景和目標
- 交易平臺面向多個租戶(業(yè)務(wù)方),需要能夠存儲大量訂單數(shù)據(jù),并提供高可用高性能的服務(wù)。
- 不同租戶的數(shù)據(jù)量和并發(fā)量可能有很大區(qū)別,要能根據(jù)實際情況靈活分配存儲資源。
(2)設(shè)計方案
- 考慮到交易系統(tǒng)OLTP特性和開發(fā)人員熟練程度,采用MySQL作為底層存儲、ShardingSphere作為分庫分表中間件,將用戶標識(userId)作為分片鍵,保證同一個用戶的訂單落在同一個庫中。
- 接入新租戶時約定一個租戶編碼(tenantCode),所有接口都要帶上這個參數(shù);租戶對數(shù)據(jù)量和并發(fā)量進行評估,分配至少滿足未來五年需求的庫表數(shù)量。
- 租戶與庫表的映射關(guān)系:租戶編碼 -> {庫數(shù)量,表數(shù)量,起始庫編號,起始表編號}。
通過上面的映射關(guān)系,可以為每個租戶靈活分配存儲資源,數(shù)據(jù)量很小的租戶還能復(fù)用已有的庫表。
示例一:
新租戶接入前已有4庫*16表,新租戶的訂單量少且并發(fā)低,直接復(fù)用已有的0號庫0號表,映射關(guān)系是:租戶編碼-> 1,1,0,0
示例二:
新租戶接入前已有4庫*16表,新租戶的訂單量多但并發(fā)低,用原有的0號庫中新建8張表來存儲,映射關(guān)系是:租戶編碼-> 1,8,0,16
示例三:
新租戶接入前已有4庫*16表,新租戶的訂單量多且并發(fā)高,用新的4庫*8表來存儲,映射關(guān)系是:租戶編碼-> 4,8,4,0
用戶訂單所屬庫表計算公式
庫序號 = Hash(userId) / 表數(shù)量 % 庫數(shù)量 + 起始庫編號
表序號 = Hash(userId) % 表數(shù)量 + 起始表編號
可能有小伙伴會問:為什么計算庫序號時要先除以表數(shù)量?下面的公式會有什么問題?
庫序號 = Hash(userId) % 庫數(shù)量 + 起始庫編號
表序號 = Hash(userId) % 表數(shù)量 + 起始表編號
答案是,當庫數(shù)量和表數(shù)量存在公因數(shù)時,會存在傾斜問題,先除以表數(shù)量就能剔除公因數(shù)。
以2庫4表為例,對4取模等于1的數(shù),對2取模也一定等于1,因此0號庫的1號表中不會有任何數(shù)據(jù),同理,0號庫的3號表、1號庫的0號表、1號庫的2號表中都不會有數(shù)據(jù)。
路由過程如下圖所示:
(3)局限性和應(yīng)對辦法
- 全局唯一ID
問題:分庫分表后,數(shù)據(jù)庫自增主鍵不再全局唯一,不能作為訂單號來使用。且很多內(nèi)部系統(tǒng)間的交互接口只有訂單號,沒有用戶標識這個分片鍵。
方案:如下圖所示,參考雪花算法來生成全局唯一訂單號,同時將庫表編號隱含在其中(兩個5bit分別存儲庫表編號),這樣就能在沒有用戶標識的場景下,從訂單號中獲取庫表編號。
- 全庫全表搜索
問題:管理后臺需要根據(jù)各種篩選條件,分頁查詢所有滿足條件的訂單。
方案:將訂單數(shù)據(jù)冗余存儲一份到搜索引擎Elasticsearch中,滿足各種場景下的快速靈活查詢需求。
3.2 狀態(tài)機設(shè)計
(1)背景
- 之前做官方商城時,由于是定制化業(yè)務(wù)開發(fā),各類型的訂單和售后單的狀態(tài)流轉(zhuǎn)都是寫死的,比如常規(guī)訂單在下單后是待付款,付款后是待發(fā)貨,發(fā)貨后是待收貨;虛擬商品訂單不需要發(fā)貨,沒有待發(fā)貨狀態(tài)。
- 現(xiàn)在要做的是平臺系統(tǒng),不可能再為每個業(yè)務(wù)方做定制化開發(fā),否則會導(dǎo)致頻繁改動發(fā)版,代碼錯綜冗余。
(2)目標
- 引入訂單狀態(tài)機,能為每個業(yè)務(wù)方配置多套差異化的訂單流程,類似于流程編排。
- 新增訂單流程時,盡可能不改動代碼,實現(xiàn)狀態(tài)和操作的可復(fù)用性。
(3)方案
- 在管理后臺為每個租戶維護一系列訂單類型,數(shù)據(jù)轉(zhuǎn)化為JSON格式存儲在配置中心,或存儲在數(shù)據(jù)庫并同步到本地緩存中。
- 每個訂單類型的配置包括:初始訂單狀態(tài),以及每個狀態(tài)下允許的操作和操作之后的目標狀態(tài)。
- 當訂單在執(zhí)行某個動作時,使用訂單狀態(tài)機來修改訂單狀態(tài)。
訂單狀態(tài)機的公式是:
StateMachine(E,S —> A , S’)
表示訂單在事件E的觸發(fā)下執(zhí)行動作A,并從原狀態(tài)S轉(zhuǎn)化為目標狀態(tài)S’ - 每個訂單類型配置完成后,生成數(shù)據(jù)的結(jié)構(gòu)是
- 訂單商品行狀態(tài)機、售后單狀態(tài)機,也用同樣的方式實現(xiàn)
3.3 通用操作觸發(fā)器
(1)背景
業(yè)務(wù)中通常都會有這樣的延時需求,我們之前往往通過定時任務(wù)來掃描處理。
- 下單后多久未支付,自動關(guān)閉訂單
- 申請退款后商家多久未審核,自動同意申請
- 訂單簽收后多久未確認收貨,自動確認收貨
(2)目標
- 業(yè)務(wù)方有類似的延時需求時,能夠有通用的方式輕松實現(xiàn)
(3)方案
設(shè)計通用操作觸發(fā)器,具體步驟為:
- 配置觸發(fā)器,粒度是狀態(tài)機的流程類型。
- 創(chuàng)建訂單/售后單時或訂單狀態(tài)變化時,如果有滿足條件的觸發(fā)器,發(fā)送延遲消息。
- 收到延遲消息后,再次判斷執(zhí)行條件,執(zhí)行配置的操作。
觸發(fā)器的配置包括:
- 注冊時間:可選訂單創(chuàng)建時,或訂單狀態(tài)變化時
- 執(zhí)行時間:可使用JsonPath表達式選取訂單模型中的時間,并可疊加延遲時間
- 注冊條件:使用QLExpress配置,滿足條件才注冊
- 執(zhí)行條件:使用QLExpress配置,滿足條件才執(zhí)行操作
- 執(zhí)行的操作和參數(shù)
3.4 分布式事務(wù)
對交易平臺而言,分布式事務(wù)是一個經(jīng)典問題,比如:
- 創(chuàng)建訂單時,需要同時扣減庫存、占用優(yōu)惠券,取消訂單時則需要進行回退。
- 用戶支付成功后,需要通知發(fā)貨系統(tǒng)給用戶發(fā)貨。
- 用戶確認收貨后,需要通知積分系統(tǒng)給用戶發(fā)放購物獎勵的積分。
我們是如何保證微服務(wù)架構(gòu)下數(shù)據(jù)一致性的呢?首先要區(qū)分業(yè)務(wù)場景對一致性的要求。
(1)強一致性場景
比如訂單創(chuàng)建和取消時對庫存和優(yōu)惠券系統(tǒng)的調(diào)用,如果不能保證強一致,可能導(dǎo)致庫存超賣或優(yōu)惠券重復(fù)使用。
對于強一致性場景,我們采用Seata的AT模式來處理,下面的示意圖取自seata官方文檔。
(2)最終一致性場景
比如支付成功后通知發(fā)貨系統(tǒng)發(fā)貨,確認收貨后通知積分系統(tǒng)發(fā)放積分,只要保證能夠通知成功即可,不需要同時成功同時失敗。
對于最終一致性場景,我們采用的是本地消息表方案:在本地事務(wù)中將要執(zhí)行的異步操作記錄在消息表中,如果執(zhí)行失敗,可以通過定時任務(wù)來補償。
3.5 高可用與安全設(shè)計
- 熔斷
使用Hystrix組件,對依賴的外部系統(tǒng)添加熔斷保護,防止某個系統(tǒng)故障的影響擴大到整個分布式系統(tǒng)中。
- 限流
通過性能測試找出并解決性能瓶頸,掌握系統(tǒng)的吞吐量數(shù)據(jù),為限流和熔斷的配置提供參考。
- 并發(fā)鎖
任何訂單更新操作之前,會通過數(shù)據(jù)庫行級鎖加以限制,防止出現(xiàn)并發(fā)更新。
- 冪等性
所有接口均具備冪等性,上游調(diào)用我們接口如果出現(xiàn)超時之類的異常,可以放心重試。
- 網(wǎng)絡(luò)隔離
只有極少數(shù)第三方接口可通過外網(wǎng)訪問,且都有白名單、數(shù)據(jù)加密、簽名驗證等保護,內(nèi)部系統(tǒng)交互使用內(nèi)網(wǎng)域名和RPC接口。
- 監(jiān)控和告警
通過配置日志平臺的錯誤日志報警、調(diào)用鏈的服務(wù)分析告警,再加上公司各中間件和基礎(chǔ)組件的監(jiān)控告警功能,讓我們能夠能夠第一時間發(fā)現(xiàn)系統(tǒng)異常。
3.6 其他考慮
- 是否用領(lǐng)域驅(qū)動設(shè)計
考慮到團隊非敏捷型組織架構(gòu),又缺少領(lǐng)域?qū)<?,因此沒有采用
- 高峰期性能瓶頸問題
大促和推廣期間,特別是爆款搶購時的流量可能會觸發(fā)限流,導(dǎo)致部分用戶被拒之門外。因為無法準確預(yù)估流量,難以提前擴容。
可以通過主動降級方案增加并發(fā)量,比如同步入庫切為異步入庫、db查詢轉(zhuǎn)為cache查詢、只能查到最近半年的訂單等。
考慮到業(yè)務(wù)復(fù)雜度和數(shù)據(jù)量級還處在初期,團隊規(guī)模也難以支撐,這些設(shè)計有遠期計劃,但暫時還沒做。(架構(gòu)的合適性原則,殺雞用牛刀,你愿意也行)。
四、總結(jié)與展望
我們在設(shè)計系統(tǒng)時并沒有一味追求前沿技術(shù)和思想,面對問題時也不是直接采用業(yè)界主流的解決方案,而是根據(jù)團隊和系統(tǒng)的實際狀況來選取最合適的辦法。好的系統(tǒng)不是在一開始就被大牛設(shè)計出來的,而是隨著業(yè)務(wù)的發(fā)展和演進逐漸被迭代出來的。
目前交易平臺已上線一年多,接入了三個業(yè)務(wù)方,系統(tǒng)運行平穩(wěn),公司內(nèi)有交易/商品/庫存等需求的新業(yè)務(wù),以及存量業(yè)務(wù)在遇到系統(tǒng)瓶頸需要升級時,都可以復(fù)用這塊能力。
上游業(yè)務(wù)方數(shù)量的增加和版本的迭代,對平臺系統(tǒng)的需求源源不斷,平臺的功能得到逐漸完善,架構(gòu)也在不斷演進,我們正在將履約模塊從交易平臺中剝離出來,進一步解耦,為業(yè)務(wù)持續(xù)發(fā)展做好儲備。