前端交易型系統設計原則
從畢業到現在已經快7年開發經驗了,做過基礎用戶系統、積分商城、偷菜游戲、論壇、博客等等;也一個人全棧開發在線視頻網站(http://sishuok.com/),也開發過幾萬、幾十萬、幾千萬、幾個億不同量級的系統,踩過不少坑,也學到許多經驗。
設計了一些系統,也有了一些自己的觀點,個人認為設計系統要因場景因時間而異,一個系統不是一下子就設計的非常***,在有限的資源情況下一定是先解決當下最核心的問題,并預測/發現未來可能出現的問題,一步步解決最痛點的問題。也就是說系統設計是不斷迭代的過程,在迭代中發現問題修復問題;即滿足需求的系統是不斷迭代優化出來的,不是一下子就架構的非常***,這是一個持續的過程,個人不相信***架構銀彈。不過如果一開始就有好的基礎系統設計,未來可以更容易達到一個比較滿意的目標。
在設計系統時應該多思考墨菲定律:
1、任何事都沒有表面看起來那么簡單;
2、所有的事都會比你預計的時間長;
3、會出錯的事總會出錯;
4、如果你擔心某種情況發生,那么它就更有可能發生。
但是也要思考80/20法則,在系統設計初期將有限的資源用到刀刃上,我們的目標是系統滿足現有需求并能支持未來需求。
在持續開發系統過程中前輩們也總結了很多設計原則/經驗;而我個人也有幸運用了一些經驗/原則。設計原則是系統發展初期或進化過程中根據自己系統特征匹配使用的,如果剛開始不是核心問題請不要復雜化系統設計。
拆分
在系統設計初期,是做一個大而全的系統還是進行按功能拆分系統這個需要進行權衡;比如做私塾在線時本身用戶量/交易量不會特別大,而且開發就我一個人,資源有限,那就沒必要對系統拆分(比如拆分商品、訂單等等),就是做一個大而全的系統。而比如設計一個京東秒殺系統,預測到一旦上線量會非常大,而且投入的資源還是蠻充足的,這種情況下就要考慮進行按功能拆分系統。
筆者遇到的拆分主要有如下幾種情況:
- 系統維度:按照系統功能/業務拆分,比如商品系統、購物車、結算、訂單系統等等;
- 功能維度:對一個系統進行功能再拆分,比如優惠券系統,可以拆分為后臺券創建系統、領券系統、用券系統等;
- 讀寫維度:根據讀寫比例特征進行拆分,比如商品系統,交易的各個系統都會讀取,讀大于寫,因此就可以進行拆分:商品寫服務、商品讀服務;讀服務可以考慮全量緩存提升性能;比如寫的量太大,需要考慮分庫分表;還有些聚合讀取的場景,如商品詳情頁,請考慮數據異構拆分系統,將分散在多處的數據聚合到一處存儲,提升讀的性能和可靠性;
- AOP維度:根據訪問特征,按照AOP進行拆分,比如商品詳情頁,可以分為CDN、頁面渲染系統;CDN就是一個AOP系統;
- 模塊維度:比如按照基礎或者代碼維護特征進行拆分,如基礎模塊:分庫分表、數據庫連接池等等;還有如代碼維護一般按照三層架構(Web、Service、DAO)進行劃分。
服務化
首先判斷是不是只需要簡單的單點遠程服務調用即可,如果單機扛不住了需要集群,是不是可以在客戶端注冊多臺機器,使用Nginx進行負載均衡即可解決;如果隨著調用方越來越多,就要考慮使用服務自動注冊和發現(如Dubbo使用zookeeper);還要考慮服務的分組/隔離,比如有的系統訪問量太大導致把整個服務打掛,因此需要為不同的調用方提供不同的服務分組,隔離訪問;后期還會隨著調用量的增加還要考慮如服務的限流、黑白名單等等。還有一些細節需要注意,如超時時間、重試機制、服務路由(能動態切換不同的分組)、故障補償等等,這些都會影響到服務的質量。
總結為進程內服務--->單點遠程服務--->集群手動注冊服務--->自動注冊和發現服務---->服務的分組/隔離/路由---->限流/黑白名單。
數據版本化,可回滾
在設計時考慮是否需要進行數據的版本化,數據維護出問題是否需要回滾。比如商品的維護是不是需要版本化。我們目前有一些非常重要的系統需要對數據進行版本化并且支持可回滾。整體設計類似于下圖設計:
流程可定義
如果接觸過保險業務,會發現不同的保險理賠服務是不一樣的,因此我們在系統設計時就設計了一套理賠流程服務。而承保流程和理賠流程是分離,然后進行關聯,從而可以復用一些理賠流程并提供個性化的理賠流程。
狀態與狀態機
在設計交易訂單系統時,會存在正向狀態(待付款、待發貨、已發貨、完成)和逆向狀態(取消、退款)等,正向狀態和逆向狀態應該根據自己系統的特征來決定是不是需要分離存儲。
另外還有訂單狀態的變遷,比如待支付、已支付待發貨、待收貨、完成的遷移;要考慮是不是需要使用狀態機來驅動狀態的變更和后續流程節點操作。
還要考慮并發狀態修改問題,同時對同一個訂單只存在一個修改;狀態變更的有序問題,狀態變更消息的先到后到問題,如支付成功消息和用戶取消消息的時間差。
消息隊列
消息隊列,用來解耦一些不需要同步調用的服務或者訂閱一些自己系統關心的變化;使用消息隊列可以實現服務解耦(一對多消費)、異步、緩沖(削峰)等。比如電商系統中的交易訂單數據,該數據有非常多的系統關心并訂閱,比如訂單生產系統、定期送系統、訂單風控系統等等;如果訂閱者太多,那么訂閱單個消息隊列就會成為瓶頸,此時需要考慮對消息隊列進行多個鏡像復制。
大流量緩沖持久化
在電商搞大促時,此時的系統流量會高于正常流量的幾倍甚至幾十倍,此時就要進行一些特殊的設計來保證系統平穩度過這段時期;而解決的手段很多,一般都是犧牲強一致性,而是保證最終一致性即可。
比如扣減庫存,可以考慮這樣設計:
直接在Redis中扣減,然后記錄下扣減日志,通過Worker去同步到DB。
還有如交易訂單系統,可以考慮這樣設計:
首先結算服務調用訂單接單服務將訂單存儲到:訂單Redis和訂單隊列表,訂單隊列表可以按照需求水平擴展N個表,通過隊列緩沖表提升接單的能力;然后通過同步Worker同步到訂單中心表;假設用戶支付了訂單,訂單狀態機會驅動狀態變更,此時可能訂單隊列表的訂單還沒有同步到訂單中心表,此時狀態機就要根據實際情況進行重試。
如果用戶查看單個訂單詳情可以直接從訂單Redis就能查到;但如果查詢訂單列表需要考慮訂單Redis和列表的合并。
同步Worker在設計時需要考慮并發處理和重復處理的問題,單機串行掃描處理(每臺Worker只掃描其中的一部分表)還是集群處理(Map-Reduce),另外需要考慮是否需要對訂單隊列表添加相關字段:處理人(哪個應用正在處理)和處理狀態(正在處理、已處理、處理失敗)。
數據校對
在使用了消息異步機制的場景下,可能存在消息的丟失,需要考慮進行數據校對和修正來保證數據一致性和完整性。可以通過Worker定期去掃描原始表進行補償,掃描周期根據實際場景進行定義。
數據異構化
訂單分庫分表一般按照訂單ID進行分,那么如果要查詢某個用戶的訂單列表就需要聚合N個表的數據然后返回,這樣會導致訂單表的讀性能很低;此時需要對訂單表進行異構,異構一套用戶訂單表,按照用戶ID進行分庫分表;另外還需要考慮對歷史訂單數據進行歸檔處理。
還一種異構場景,如商品詳情頁,因為數據來源太多,影響服務穩定性的因素就太多了,因此***的辦法是把使用到的數據進行異構存儲,形成數據閉環;提升服務的性能和穩定性。而有些數據異構的意義不大,如庫存價格可以考慮異步加載,或者并發請求合并。
后臺系統操作可反饋
在我接觸過的很多系統,很多場景都需要反饋,比如修改了某些內容想預覽看看最終效果,即想得到一些反饋;還有一些是規則系統,希望看到這些規則在系統數據下的反饋。因此在設計后臺系統請考慮效果的可預覽、可反饋。
后臺系統審批化
對于有些重要的后臺功能需要設計審批流,比如調整價格,并對操作進行日志記錄從而保證操作可追溯、可審計。
防重設計
比如結算頁需要考慮重復提交,還有如下單扣減庫存時需要防止重復扣減庫存。解決方案可以考慮防重KEY、防重表。而有些場景如重復支付,如有的電商網站同時支持微信支付、京東支付,渠道不一樣是無法防止重復支付的,但是系統設計時需要將支付的每筆情況記錄下。比如下圖是我在京東使用京東支付和微信支付模擬的重復支付之后進行退款的支付明細:
冪等設計
在交易系統中經常會用到消息,而現有消息中間件基本不保證不發生重復消息的消費;因此需要業務系統考慮在重復消息消費時進行冪等處理。還有如使用第三方支付時,第三方支付會進行異步回調,因此也要考慮做好回調的冪等處理。
【本文是51CTO專欄作者張開濤的原創文章,作者微信公眾號:開濤的博客( kaitao-1234567)】