饿了么交易系统设计思路
本文作者:
盛赫,花名白茶,就職于阿里本地生活中臺研發部,多年交易系統建設開發經驗,目前轉入營銷領域繼續探索。
?
叮~,您有新的餓了么訂單,正在阿里云上被接單。
?
這篇文章成型于交易系統重構一期之后,主要是反思其過程中做決策的思路,我沒有使用「架構」這個詞語,是因為它給人的感受充滿權利和神秘感,談論「架構」讓人有一種正在進行責任重大的決策或者深度技術分析的感覺。
?
如畢玄在系統設計的套路這篇文章里所提:
?
回顧了下自己做過的幾個系統的設計,發現現在自己在做系統設計的時候確實是會按照一個套路去做,這個套路就是:系統設計的目的->系統設計的目標->圍繞目標的核心設計->圍繞核心設計形成的設計原則->各子系統,模塊的詳細設計
在進行系統設計時,摸清楚目的,并形成可衡量的目標是第一步。
?
?
"Soft" ware
?
?Software 拆開來分別是 soft ware ,即靈活的產品。? -- 鮑勃大叔
重構前的交易系統第一版的代碼可以追溯到 8 年前,這期間也經歷過拆解重構,17 年我來到時,主要系統是這樣:
?
這套系統馱著業務從百萬級訂單跑到了千萬級訂單,從壓測表現來看,它可以再支撐業務多翻幾倍的量,也就是說如果沒有啥變化,它可以繼續穩定運行著,但如果發生點變化呢,答案可能就不這么肯定了。
?
在我入職的這兩年里,系統承載的業務迭增變化:從單一的餐飲外賣到與新零售及品牌餐飲三方并行,又從到家模式衍生至到店,隨之而來的是業務持續不斷的差異化定制,還有并行上線的要求。另一面,隨著公司組織架構變化,有的項目需要三地協同推進才能完成,溝通協作成本翻倍提升。幾方面結合起來,導致開發沒有精力對大部分系統的演進都進行完善的規劃。
?
幾個月前,業務提了一個簡單的需求:對交易的評價做自動審核并進行相應的處罰。當時評價核心“域模型”是這樣的:
?
?
設計自身的優劣這里暫不進行討論,只是舉例說明為了滿足這個訴求,會涉及多個評價子模塊要改動,開發評估下來的工作量遠遠超出了預期,業務方對此不滿意,類似的沖突在其他系統里也經常出現。但實際上,團隊里沒人偷懶,和之前一樣努力工作,只是不管投入了多少個人時間,救了多少次火,加了多少次班,產出始終上不去,因為開發大部分時間都在系統的修修補補上,而不是真正完成實際的新功能,一直在拆東墻補西墻,周而往復。
?
為什么會導致這樣的結果,我想應該是因為大部分系統已經演變到很難響應需求的變更了,業務認為的小小變更,對開發來說都是系統的一次大手術,但系統本不應該往這個方向發展的,它和 hardware 有著巨大的區別就在于:變更對軟件來說應該是簡單靈活的。
?
所以我們思考設計的核心目標:“采用好的軟件架構來節省項目構建和維護的人力成本,讓每一次變更都短小簡單,易于實施,并且避免缺陷,用最小的成本,最大程度地滿足功能性和靈活性的要求”。
?
?
Source code is the design
?
提到軟件設計,大家腦袋里可能會想到一幅幅結構清晰的架構圖,認為關于軟件架構的所有奧秘都隱藏在圖里了,但經歷過一些項目后發現,這往往是不夠的。Jack ? Reeves 在 1992 年發表了一篇論文《源代碼即設計》,他在文中提出一個觀點:
?
高層結構的設計不是完整的軟件設計,它只是細節設計的一個結構框架。在嚴格地驗證高層設計方面,我們的能力是非常有限的。詳細設計最終會對高層設計造成的影響至少和其他的因素一樣多(或者應該允許這種影響)。對設計的各個方面進行改進,是一個應該貫穿整個設計周期的過程。
?
在踩過一些坑之后,這種強調詳細設計重要性的觀點在我看來很實在接地氣,簡單來說:“自頂向下的設計通常是不靠譜的,編碼即是設計過程的一部分”,個人認為:系統設計應該是從下到上,隨著抽象層次的提升,不斷演化而得到良好的高層設計。
?
?
編程范式
?
從下向上,那就應該從編碼開始審視,餓了么交易系統最開始是由 Python 編寫,? Python 足夠靈活,可以非??焖俚漠a出 mvp 的系統版本,這也和當時的公司發展狀態相關: 產品迭代迅速,新項目的壓力很大。
?
最近這次重構,順應集團趨勢,我們使用 Java 來進行編寫,不過在這之前有一個小插曲:17 年底,因為預估到當前系統框架在單量到達下一個量級時會遇到瓶頸,所以針對一些新業務逐漸開始使用 Go 語言編寫,但在這個過程里,經常會聽到一些言論:用 Go 來寫業務不舒服。為什么會不舒服?大致是因為沒有框架,沒有泛型,沒有 try catch ,確實,在解決業務問題的這個大的上下文中, Go 語言不是最優的選擇,但語法簡單,可以極大程度的避免普通程序員出錯的概率。
?
那么 Python 呢,任何事物都有雙刃劍,雖然 Python 具有強表達力,但是靈活性也把很多人慣壞了,代碼寫的糙,動態語言寫太多坑也多,容易出錯,在大項目上的工程管理和維護上有一定劣勢,所以 rails 作者提到:“靈活性被過分高估——約束才是解放”也有一定道理。
?
為避免引起語言戰,這里不過多討論,只是想引出:我從 C++ 寫到 Go ,又從 Python 寫到 Java ,在這個過程里體會到--編程范式也許是學習任何一門編程語言時要理解的最重要的術語,簡單來說它是程序員看待程序應該具有的觀點,但卻容易被忽視。交易老系統的代碼,不管是針對什么業務邏輯,幾乎都是OPP一桿到底,類似的代碼在系統里隨處可見。
?
我們好像完全遺忘了 OOP ,這項古老的技藝被淡化了,我這里不是說一定要 OOP 就是完美的,準確來說我是“面向問題”范式的擁躉者,比如, Java從骨子里就是要 OOP ,但是業務流程不一定需要 OOP 。一些交易業務就是第一步怎么樣,第二步怎么樣,采取 OPP 的范式就是好的解法。這時,弄很復雜的類設計有時并不必要,反而還會帶來麻煩。
?
此外,同一個問題還可以拆解為不同的層次,不同的層次可以使用各自適合的方式。比如高層的可以 OOP ,具體到某個執行邏輯里可以用 FP ,比如:針對訂單的金額計算,我們用 Go 寫了一版FP的底層計算服務,性能高、語法簡單以及出錯少等是語言附帶的優點,核心還是因為該類問題自身適合。
?
然而,當面向整個交易領域時,針對繁復多樣的業務場景,合理運用 OOP 的設計思想已經被證明確實可以支撐起復雜龐大的軟件設計,所以我們作出第一個決策:采用以 OOP 為主的“混合”范式。
?
?
原則和模式
?
The difference between a bad programmer and a?good one is whether he considers his code or his?
data structures more important. Bad programmers?worry about the code. Good programmers worry about?data structures and their relationships.?--?Linus Torvalds
?
不管是采用哪種編程范式、編程語言,構造出來的基礎模塊就像蓋樓的磚頭,如果磚頭質量不好,最終大樓也不會牢固,引用里的一大段話, relationships 才是我最想強調的:我理解它是指類之間的交互關系,“關系”的好壞通常等價于軟件設計的優劣,設計不好的軟件結構大都有些共同特征:
?
-
僵化性:難以對軟件進行改動,一般會引發連鎖改動,比如下單時增加一個新的營銷類型,訂單中心和相關上下游都要感知到并去做改動。
-
脆弱性:簡單的改動會引發其他意想不到的問題,甚至概念完全不相關。
-
牢固性:設計中有對其他系統有用的部分,但是拆出來的風險和成本很高,比如訂單中心針對外賣場景的支付能力并不能支持會員卡等虛擬商品的支付需求。
-
不必要的復雜性:這個通常是指過度設計。
-
晦澀性:隨時間演化,模塊難以理解,代碼越來越難讀懂,比如購物車階段的核心代碼已經長成了一個近千行的大函數。
-
...
?
采取合適的范式后,我們需要向上抽一個層次,來關注代碼之上的邏輯,多年軟件工程的發展沉淀下來了一些基本原則和模式,并被證明可以指導我們如何把數據和函數封裝起來,然后再把它們組織起來成為程序。
?
SOLID
?
有人將這些原則重新排列下順序,將首字母組成 SOLID ,分別是:SRP、OCP、LSP、ISP、DIP。這里針對其中幾個原則來舉些例子。
?
SRP(單一職責):這個原則很簡單,即任何一個軟件模塊都應該只對一類用戶負責,所以代碼和數據應該因為和某一類用戶關系緊密而被組織到一起。實際上我們大部分的工作就是在發現職責,然后拆開他們。
?
我認為該原則的核心在于用戶的定義,18 年去聽 Qcon 時,聽到俞軍的分享,其中一段正好可以拿來詮釋什么是用戶,俞軍說:“用戶不是人,是需求的集合”。在我們重構的過程中,曾經對交易系統里的交付環節有過爭論,目前餓了么支持商家自配和平臺托管以及選擇配送(比如跑腿),這幾類配送的算價方式,配送邏輯,和使用場景都不一樣,所以我們基于此做了拆解,一開始大家都認同這種分解方式。
?
但后來商戶群體調整了,新零售商戶和餐飲商戶進行分拆,對應著業務方的運營方式也開始出現差異,導致在每個配送方式下也有了不同訴求,伴隨這些變化,最后我們選擇做了第二次拆解。
?
對于單一職責,這里有個小 tips :大家如果實在不好分析的話,可以多觀察那些因為分支合并而產生沖突的代碼,因為這很可能是因為針對不同需求,大家同時改了同一個模塊。
?
DIP(依賴倒置):有人說依賴反轉是 OOP 和 OPP 的分水嶺,因為在過程化設計里所創建的依賴關系,策略是依賴于細節的--也就是高層依賴于底層,但這通常會讓策略因為細節改變而受到影響,舉個例子:在外賣場景下,一旦用戶因為某些原因收不到餐了,商戶會賠代金券安撫用戶,此時 OPP 可以這樣做:
?
?
而過一陣子,因為代金券通常不能跨店使用,平臺想讓用戶繼續復購,就想通過賠付通用紅包來挽留,這個時候就需要改動老的代碼,通過增加對紅包賠付邏輯的依賴,才可以來滿足訴求。
?
但如果換個方式,采用 DIP 的話,問題也許可以被更優雅的解決了:
?
?
當然這個示例是簡化后的版本,實際工作里還有很多更加復雜的場景存在,但本質都是一樣:采用 OOP 倒置了策略對細節的依賴,使細節依賴于抽象,并且常常是客戶擁有服務接口,這個過程中的核心是需要我們做好抽象。
?
OCP(開閉原則):如果仔細分析,會發現這個原則其實是我們一開始定的系統設計的目標,也是其他原則最終想達成的目的,比如:通過 SRP ,把每個業務線的模塊拆解出來,將變動隔離,但是平臺還要做一定的抽象,將核心業務流程沉淀下來,并開放出去每個業務線自己定義,這時候就又會應用到 DIP 。
?
其他的幾個原則就不舉例子了,當然除了 SOLID ,還有其他類型的原則,比如 IoC :用外賣交易平臺舉例子,商戶向用戶賣飯,一手交錢一手交貨,所以,基本上來說用戶和商戶必需強耦合(必需見面)。這個時候,餓了么平臺出來做擔保,用戶把錢先墊到平臺,平臺讓商家接單然后出餐,用戶收到餐后,平臺再把錢打給商家。這就是反轉控制,買賣雙方把對對方的直接依賴和控制,反轉到了讓對方來依賴一個標準的交易模型的接口。
?
可以發現只要總結規律,總會出現這樣或那樣的原則,但每個的原則的使用都不是一勞永逸的--需要不斷根據實際的需求變化做代碼調整,原則也不是萬金油,不能無條件使用,否則會因為過分遵循也會帶來不必要的復雜性,比如經常見到一些使用了工廠模式的代碼,里面一個 new 其實就是違反了 DIP ,所以適度即可。
?
?
演進到模式
?
這里的模式就是我們常說的設計模式,用演進這個詞,是因為我覺得模式不是起點,而是設計的終點?!对O計模式》這本書的內容不是作者的發明創造,而是其從大量實際的系統里提取出來的,它們大都是早已存在并已經廣泛使用的做法,只不過沒有被系統的梳理。換句話說,只要遵循前面敘述的某些原則,這些模式完全可能會自然在系統代碼中體現出來,在《敏捷軟件開發》這本書里,就特意有一個章節,描述了一段代碼隨著調整慢慢演進到了觀察者模式的過程。
?
擁有模式固然是好的,比如搜索系統里,通過 Template Method 模式,定義一套完整的搜索參數解析模版,只需要增加配置就可以定制不同的查詢訴求。這里最想強調的是不要設計模式驅動編程,拿交易系統里的狀態機來舉例子(狀態機簡直太常見了,簡單如家里使用的臺燈,都有一個開和關的狀態,只是交易場景下會更加復雜),在餐飲外賣交易有如下的狀態流轉模型:
?
?
實現這樣的一個有限狀態機,最直接的方式是使用嵌套 switch/case 語句,簡略的代碼比如:
? ?- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
因為是簡寫了流程,所以上面的代碼看起來還是挺能接受的,但是對于訂單狀態這么復雜的狀態機,這個 switch/case 語句會無限膨脹,可讀性很差,另一個問題是狀態的邏輯和動作沒有拆開,《設計模式》提供了一個 State 模式,具體做法是這樣:
這個模式確實分離了狀態機的動作和邏輯,但是隨著狀態的增加,不斷增加 State 的類會讓系統變得異常復雜,而且對 OCP 的支持也不好:對切換狀態這個場景,新增類會引起狀態切換類的修改,最不能忍受的是這個方式會把整個狀態機的邏輯隱藏在零散的代碼里。
?
舊版的交易系統就使用的是解釋遷移表來實現的,簡化版本是這樣的:
?
? ?- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
這個版本非常容易理解,狀態邏輯集中在一起,也沒有和動作耦合起來,擴展性也比較強,唯一缺點的話是遍歷的時間,但也可以通過字典表來優化,但它總體帶來的好處更加明顯。
?
不過隨著業務發展,交易系統需要同時支持多套狀態機,意味著會出現多個遷移表,而且還有根據業務做擴展定制的需求,這套解決方案會導致代碼編寫變得復雜起來,我們在重構時采用了二級編排+流程引擎的方式來優化了這個問題,只是不在我們討論的范圍內,這里只想強調第二個決策:代碼上要靈活通過設計原則分析問題,再通過合適的設計模式解決問題,不能設計模式驅動編程,比如有時候一個全局變量就可以替代所謂的單例模式。
?
?
豐富的領域含義
?
?一旦你想解說美,而不提擁有這種特質的東西,那么就完全無法解釋清楚了。
?
用個不那么貼切的說法,如果前面說的是針對靜態問題的策略,現在我們需要討論面對動態問題的解決辦法:即使沒有風,人們也不會覺得一片樹葉是穩定的,所以人們定義穩定的時候和變更的頻繁度無關,而是和變更需要的成本有關,因為吹一口氣,樹葉就會隨之搖擺了。我們除了要寫好當前代碼,讓其足夠清晰合理,還要能寫好應對需求變化的“樹葉”代碼。
?
面向業務變化的設計首先就是要理解業務的核心問題,進而進行拆解劃分為各個子領域,DDD--也就是領域驅動設計,已經被證明是一個很好的切入點。這里不是把它當作技術來學習,而是作為指導開發的方法論,成為第三個決策,并且我個人仍處在初級階段,所以只說一些理解深刻的點。
?
通用語言
?
設計良好的架構在行為上對系統還有一個最重要的作用:就是明確的顯式的反映系統設計的意圖,簡單來說,在你拉下某些服務的代碼的時候,大概掃一眼就可以覺得:嗯,這個“看起來” 就像一個交易系統的應用。我們不能嘴上在談論業務邏輯,手上卻敲出另一份模樣的代碼,簡單來說,不能見人說人話,見鬼說鬼話。可以對比一下這兩類分包的方式,哪一個更容易理解:
?
?
發現領域通用語言的目的之一是可以通過抓住領域內涵來應該需求變更,這個需要很多客觀條件,比如團隊里有一個領域專家。但沒有的時候,我們也可以向內求解,我有次看到一位在丁香園工作的程序員朋友,購買了一大批醫學的書籍,不用去問,我就猜他一定是成了 DDD 的教徒。
?
針對這個點,我們這次重構時還做了些讓“源代碼即設計”的工作:領域元素可視化,當系統領域內的一些概念已經和產品達成一致之后,便增加約定好的注解,代碼編譯時便可以掃描并收集起來發送給前端,用于畫圖。
?
回到前面提到的評價域模型,后來在和產品多次溝通后意識到,產品沒有希望評價這么多種類,對它來說商品也好、騎手也好,都屬于被評價的對象,從領域模型來看,之前的設計更多是面對場景,而不是面對行為,所以合理的域模型應該是:
?
?
限界上下文
?
這個在我們平時開發過程中會很常見。拿用戶系統舉例:一個 User 的 Object ,如果是從用戶自身的視角來看,就可以登陸、登出,修改昵稱;如果是從其他普通用戶來看,就只能看看昵稱之類的;如果從后臺管理員來看,就可以注銷或者踢出登陸。這時就需要界定一個 Scope ,來說明現在的 User 到底是哪個 Scope ,這其實就是 DDD 中限界上下文的理念。
?
限界上下文可以很好的隔離相同事物的不同內涵,通過嚴格規范可以進入上下文的對象模型,從而保護業務抽象行為的一致性,回到交易領域,餓了么是最開始支持超級會員玩法的,為了支持對應的結算訴求,需要接入交易系統來完成這個業務,我們通過分解問題域來降低復雜度,這個時候就對應切割為會員域和交易域,為了保護超會卡在進入交易領域的時候,不擾亂交易內部的業務邏輯,我們做了一次映射:
?
切分
?
當所有代碼完成之后,隨著程序增長,會有越來越多的人參與進來,為了方便協作,就必須把這些代碼劃分成一些方便個人或者團隊維護的組。根據軟件變更速度不同,可以把上文提到的代碼化為幾個組件:
?
-
Extension :擴展包,這里存放著前面提到的業務定制包,面向對象的思想,最核心的貢獻在于通過多態,允許插件化的切換一段程序的邏輯,其實軟件開發技術發展的歷史就是一個想法設法方便的增加插件,從而創建一個可擴展,可維護的系統架構的過程。
-
Domain : 領域包,存放著具備領域通用語言的核心業務包,它最為穩定。
-
Business :業務包,存放著具體的業務邏輯,它和 Domain 包的區別在于,可能 Domain 包會提供一個 people.run() 的方法,他會用這個方法去跑著送外賣,或者去健身。
-
Infra : 基礎設置包,存放這對數據庫及各種中間件的依賴,他們都屬于業務邏輯之外的細節。
?
然后是分層依賴,Martin Flower 已經提供了一套經典的分層封裝的模式,拿簡化的訂單模塊舉例:
?
?
然而如果有的同學避免做各種類型的轉換,不想嚴格遵守分層依賴,覺得一些查詢(這里指 Query,Query != Read )可以直接繞過領域層,這樣就變成了 CQRS 模式:
?
但是最理想的還是下面這種方式,領域層作為核心業務邏輯,不應該依賴基礎設施的細節,通過這種方式,代碼的可測性也會提升上去。
?
?
單體程序的組件拆分完畢后,再向上一層,我們開始關注四個核心服務:Booking被分拆為 Cart、Buy、Calculate,Eos 被分拆為 Procee、Query、Timeout,Blink 一部分和商戶訂單相關的功能被分拆到 Process、Query,和物流交付的部分單獨成一塊 Delivery ,最后交易的核心服務拆解成下圖:
?
到目前,算上這個切分的方式,加起來一共就四個決策,其實也沒必要分序列,它們核心都是圍繞著軟件靈活性這個目標,從程序范式到組件編寫,最后再到分層,我們主動選擇或避開的一些教條限制,所以業務架構從某種意義上來講,也是在某種領域中限制程序員的一些行為,讓他往我們所希望的規范方向編碼。從而達到整個系統的靈活可靠。
?
?
"No Silver Bullet"
?
“個體和交互勝過過程和工具”,敏捷宣言第一條。
目前系統架構是什么樣子并不重要,因為它可能會隨著時間還會拆解成其他模樣,重要的是,我們要認識到對于如何建造一個靈活的交易系統——沒有銀彈。
?
如果仔細觀察的話,會發現當前系統里仍有很多問題等著被解決。比如一些橫跨型變更:系統鏈路里會因為某個服務的接口增加了字段,而導致上下游跟著一起改。更為尷尬的是,本來我們拆分服務就是為了解耦合,但有時還會出現服務發布依賴的現象。系統演進是一場持久的戰爭,“個體和交互勝過過程和工具”,人才是勝利的核心因素。
?
過去的兩年里,我們沒有停止過思考和實踐,經??梢钥吹浇灰讏F隊內部成員的爭執,小到一個接口字段變更,大到領域之間的邊界,大家為拿到一個合理的技術方案做了很多討論,這讓我想起《禪與摩托車維修藝術》里所提到的良質,有人點評說:關于良質,程序員可能有這樣的經歷——寫出了一段絕妙的代碼,你會覺得“不是你寫出了代碼,這段代碼一直存在,而你,發現了它”。
總結
以上是生活随笔為你收集整理的饿了么交易系统设计思路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OpenEuler安装 20212802
- 下一篇: 莴笋_百度百科