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