在日常工作中,你是否也遇到過下面幾種情況:
使用一個已有接口進行業(yè)務(wù)開發(fā),上線后出現(xiàn)嚴重的性能問題,被老板當眾質(zhì)疑:“你為什么不使用緩存接口,這個接口全部走數(shù)據(jù)庫,這怎么能抗?。 ?/p>
開發(fā)一個后臺管理功能,業(yè)務(wù)反饋說數(shù)據(jù)一直不對,對比后發(fā)現(xiàn)緩存與數(shù)據(jù)庫不一致,為什么要使用緩存接口呢,你陷入沉思?
產(chǎn)品要求在 xxx 上增加新功能,編碼、測試、上線一氣呵成,最后發(fā)現(xiàn)另外一個流程被躺槍,出現(xiàn)異常不得不進行回滾!
在一個高并發(fā)的場景,DB 成為了系統(tǒng)瓶頸,不加索引查詢扛不住,加索引更新扛不住,又該如何處理?
隨著數(shù)據(jù)量的激增,系統(tǒng)變得越來越慢,特別是后臺管理復雜的查詢場景下,復雜的 Join 讓 DB 不堪重負
……
為什么會出現(xiàn)這種現(xiàn)象?其本質(zhì)仍舊是代碼組織結(jié)構(gòu)不合理,我們將不同的復雜性揉在一起,從而造成了更大的復雜性,然后如此往復,不知不覺中陷入巨大的復雜性旋渦不可自拔。
1. CQRS 是什么?
CQRS 是 Command Query Responsibility Segregation 得簡稱,簡單理解就是對 “寫”(Command) 和 “讀” (Query)操作進行分離。反應快的同學會說:“也不是什么高深技術(shù)嗎,不就是數(shù)據(jù)庫的讀寫分離嗎?”
是的,數(shù)據(jù)庫的讀寫分離也算是一種 CQRS,但 CQRS 的含義要比這復雜的多。
?
CRQS 既是一種流行的業(yè)務(wù)架構(gòu),又是一種設(shè)計思維。
?
CQRS 的核心是“拆分”,將復雜系統(tǒng)拆分為 Command 和 Query 兩個部分,針對不同的場景使用不同的模式,選擇最合適的技術(shù)落地最佳解決方案,避免兩者相互掣肘相互影響。
CQRS的目的是降低整個系統(tǒng)的復雜性,那它背后的邏輯是什么?
假設(shè),在一個系統(tǒng)中:
Command 的復雜性為 M
Query 的復雜性為 N
如果使用同一套模型來處理 Command 和 Query,那在極端情況下,系統(tǒng)的復雜性為 M * N,因為兩者相互影響,調(diào)整一方的同時要時刻關(guān)注對另一方的影響。
這種“你中有我,我中有你”的設(shè)計方式,“兩者的相互影響”成為系統(tǒng)最為復雜之處,大量精力消耗在“排查影響”,而非最有價值的設(shè)計和編碼。
如果,將 Command 和 Query 徹底分離,系統(tǒng)的復雜性變成 M + N。Command 的變更不會影響 Query,而 Query 的修改也不會影響 Command。
當然,以上兩個極端在實際工作中也很少見,通常系統(tǒng)的復雜性介于兩者之間。
這只是從理論進行推導,在實際工作中隨處可見的“沖突”也是對“拆分”的一種暗示。
2. 分層架構(gòu)中的沖突
以最常見的分層架構(gòu)進行介紹,具體如下:
如圖所示,將系統(tǒng)分成5層,每層的含義如下:
「Web 接入層?!?/strong> 主要用于處理系統(tǒng)輸入,對輸入信息進行驗證,調(diào)用應用服務(wù)完成業(yè)務(wù)操作,對結(jié)果進行轉(zhuǎn)換,最終返回給調(diào)用方;
「應用服務(wù)層?!?/strong> 主要處理業(yè)務(wù)流程編排,從倉庫中獲取領(lǐng)域?qū)ο?,?zhí)行領(lǐng)域模型的業(yè)務(wù)操作,將最新的對象狀態(tài)通過倉庫同步到數(shù)據(jù)存儲引擎,并對外發(fā)布領(lǐng)域事件;
「領(lǐng)域?qū)??!?/strong> 業(yè)務(wù)邏輯的承載點,是業(yè)務(wù)價值的集中體現(xiàn),通常構(gòu)建于面向?qū)ο笤O(shè)計之上,基于封裝、繼承、多態(tài)等特性保障業(yè)務(wù)邏輯的復用性和擴展性;
「倉庫層?!?/strong> 主要用于數(shù)據(jù)訪問,向上為應用服務(wù)提供數(shù)據(jù)操作服務(wù),向下屏蔽各類存儲引擎的差異;
「數(shù)據(jù)層?!?/strong> 主要用于數(shù)據(jù)保存和檢索,常見的數(shù)據(jù)存儲引擎全部屬于這一層,比如 MySQL、Redis、ES 等;
其實,分層架構(gòu)本身也是一種“拆分”,將不同的關(guān)注點封裝在不同的層次。但除了橫向分層,還可以基于 CQRS 對其進行縱向拆分,也就是將每個層的組件拆分為 Command 和 Query 兩部分。
?
由于接入層沖突較小,本身拆分的意義不大,在此不做要求,但從嚴格意義上講,仍舊建議進行拆分。
?
3. 應用服務(wù)層沖突與拆分
應用服務(wù)層拆分就是將一個應用服務(wù)拆分為 CommandService 和 QueryService 兩組。
這樣做可以避免很多不必要的麻煩,Command 和 Query 存在較大的區(qū)別,具體如下:
回想開篇時提到的場景,完成應用層拆分,就不在為使用錯組件而煩惱:
CommandService 的 Repository 不使用緩存,僅操作數(shù)據(jù)庫
QueryService 的 Repository 可以使用緩存,以提升訪問性能
除此之外,針對統(tǒng)一的操作流程,還可以進一步抽象來消除重復的“模板代碼”,比如:
1.引入“模板方法設(shè)計模式” 以達到核心邏輯的復用
抽象出 BaseCommandService 和 BaseQueryService 兩個父類用于統(tǒng)一核心流程
子類實現(xiàn) BaseCommandService 和 BaseQueryService 的抽象方法完成功能擴展
2.基于“約定優(yōu)于配置” 使用 Proxy 模型,只定義接口不寫實現(xiàn)代碼
按規(guī)范定義 CommandService 和 QueryService 接口,通過注解完成相關(guān)配置
自動生成 Proxy 實現(xiàn)類,完成流程編排
4. 模型層沖突與拆分
模型層是系統(tǒng)的核心,它的設(shè)計直接影響整個系統(tǒng)的質(zhì)量。作為承接業(yè)務(wù)邏輯的核心,比較流程的實現(xiàn)策略包括:
DDD 領(lǐng)域驅(qū)動設(shè)計,其核心是使用面向?qū)ο蟮母呒壧匦裕ǚ庋b、繼承、多態(tài)、組合等)來進行設(shè)計,非常適合復雜的業(yè)務(wù)場景。其體現(xiàn)就是存在很多高內(nèi)聚低耦合的對象組(聚合根),業(yè)務(wù)邏輯由這些小對象相互協(xié)作共同完成;
事務(wù)腳本,使用過程式思維,將數(shù)據(jù)操作編織到流程中,比較適合并不復雜的業(yè)務(wù)場景。其體現(xiàn)就是存在很多“上帝 Service”,Service 中存在很多非常長的方法,業(yè)務(wù)邏輯由這些方法完成;
關(guān)于哪個才是最優(yōu)解,網(wǎng)上已經(jīng)爭論多年,最終也沒有結(jié)論。但我始終認為“沒有業(yè)務(wù)場景就討論方案,就是在耍流氓”。
從不同應用場景出發(fā)便可得到如下結(jié)論:
Command 場景需要保障嚴謹?shù)臉I(yè)務(wù)邏輯,通常復雜性偏高,所以DDD 是最優(yōu)解
Query 場景需要更靈活的數(shù)據(jù)組裝能力作為支持,通常比較簡單,所以 事務(wù)腳本 是最優(yōu)解
我經(jīng)常說:“最簡單的“寫”也是復雜,最復雜的“讀”也是簡單”,其背后邏輯是基于對 Command 和 Query 的場景判斷。
將模型拆分為 Command 和 Query,具體如下:
完成模型拆分后,新模型具有以下特征:
Agg 也就是 DDD 中聚合根,主要用于處理復雜的 Command 邏輯,由具有大量業(yè)務(wù)操作的"富對象"構(gòu)成;
View 是標準的 POJO,主要充當 Query 結(jié)果對象,典型的“貧血對象”,僅作為數(shù)據(jù)的載體,根據(jù)展示需求對數(shù)據(jù)進行組裝;
View 沒有自己的 Repository,只能依賴 CommandRepository 獲取數(shù)據(jù),Converter 組件負責將 Agg 模型轉(zhuǎn)換為 View 模型;
這塊是拆分的重點,為了方便理解,簡單舉個例子:
比如在電商的訂單模塊:
生單流程,由 Order 作為聚合根對內(nèi)部 OrderItem 和 PayInfo 進行統(tǒng)一協(xié)調(diào)
訂單列表頁,只需展示 Order 和 User 信息
訂單詳情,需要展示Order、User、Address、OrderItem、PayInfo、Product等信息
如果讓一個模型同時支持著三個場景,那模型自己就變的非常復雜,很難判斷某個方法、某個字段究竟屬于哪個場景。
此時,應該根據(jù)場景對模型進行拆分:
OrderBO 以 DDD 方式進行建模,對外提供統(tǒng)一的業(yè)務(wù)操作,對內(nèi)協(xié)調(diào) OrderItem 和 PayInfo 等多個實體對象;
OrderListVO 以 POJO 方式進行建模,屬性中包含 Order 和 User 信息;
OrderDetailVO 以 POJO 方式進行建模,屬性中包括 Order、User、Address、OrderItem、PayInfo、Product 等信息;
三個模型相互獨立,互不影響。
當然,由于使用統(tǒng)一的 Repository 還需提供對應 VO 的 Converter:
OrderListVOConverter 將 OrderBO 轉(zhuǎn)換為 OrderListVO 對象
OrderDetailVOConverter 將 OrderBO 轉(zhuǎn)化為 OrderDetailVO 對象
5. 倉庫層沖突與拆分
倉庫層拆分也是非常有必要的,在這一層主要有幾項沖突:
倉庫拆分后整體架構(gòu)如下:
倉庫拆分具有以下特點:
View 不在需要 Converter 組件完成數(shù)據(jù)轉(zhuǎn)換
View 的數(shù)據(jù)來自于自己的 Repository,可以根據(jù)展示需求進行靈活定制
Command 和 Query 仍舊使用同一套數(shù)據(jù)庫、同一套數(shù)據(jù)表
6. 數(shù)據(jù)層沖突與拆分
數(shù)據(jù)層拆分是最重要的拆分,提到分離第一反應也是“數(shù)據(jù)庫主從分離”。
數(shù)據(jù)層拆分的本質(zhì)是:各種數(shù)據(jù)存儲引擎的最佳應用場景相差巨大,讀 和 寫 優(yōu)化往往存在矛盾。
仍舊以最常見的數(shù)據(jù)庫為例:
提升查詢性能,建議為各種查詢維度建立索引
提升寫入性能,需要讓表上的索引越來越少
為了加速更新性能,建議使用三范式設(shè)計表結(jié)構(gòu),減少冗余信息
為了加速查詢性能,建議使用反范式設(shè)計,盡量冗余數(shù)據(jù),避免數(shù)據(jù)表間的 Join 操作
魚和熊掌不可兼得,在數(shù)據(jù)庫層展示的淋漓盡致!
數(shù)據(jù)層拆分后架構(gòu)如下:
該模型具有以下特點:
數(shù)據(jù)存儲進行了徹底拆分;Command 和 Query 都可以靈活的選擇最合適的存儲引擎;
Command 與 Query 需要引入一套同步機制以完成兩者的數(shù)據(jù)同步,常見的同步機制有:
工作在應用層基于領(lǐng)域事件的數(shù)據(jù)同步,如圖所示
工作在數(shù)據(jù)層基于log的數(shù)據(jù)同步,如 MySQL 的主從同步、Canal2XX 等
數(shù)據(jù)層拆分是大型系統(tǒng)最終的歸宿,仍舊以訂單系統(tǒng)為例:
訂單作為一致性要求極高的系統(tǒng),Command 側(cè)首選仍舊為具有 ACID 的關(guān)系型數(shù)據(jù)庫,哪怕是分庫分表底層存儲仍舊不變;
為了滿足高性能查詢需求,需要在 Query 側(cè)引入 Redis 作為分布式緩存對訪問進行加速;
為了滿足后臺復雜且多維度的業(yè)務(wù)查詢,需要在 Query 側(cè)引入 ES 為全文檢索進行加速;
為了滿足各種實時報表需求,需要在 Query 側(cè)引入 TiDB 以滿足海量數(shù)據(jù)的實時檢索;
?
這就是我們面臨的現(xiàn)狀:“數(shù)據(jù)密集型系統(tǒng)”越來越多的應用程序有著各種嚴格而廣泛的要求,單個工具不足以滿足所有的數(shù)據(jù)處理和存儲需求。取而代之的是,總體工作被拆分成一系列能被單個工具高效完成的任務(wù),并通過應用代碼將它們縫合起來,通過 API 的方式,對外提供服務(wù),屏蔽內(nèi)部的復雜性。
?
7. 小結(jié)
“拆分”是“分離關(guān)注點”的重要手段之一。拆分的目的是將問題進行歸類,然后采取有針對性的手段更好的解決問題。
CQRS 作為一種架構(gòu),將業(yè)務(wù)系統(tǒng)不同部分進行歸類,接下來需要為 Command 和 Query 尋找最優(yōu)解決方案:
「1、Command,以 DDD 作為理論基礎(chǔ)將戰(zhàn)術(shù)模型中最佳實戰(zhàn)進行落地,包括」
聚合設(shè)計
倉庫設(shè)計
LazyLoad + Context 模式
業(yè)務(wù)驗證
領(lǐng)域事件
…
「2、Query,以數(shù)據(jù)檢索和組裝作為核心能力,設(shè)計留給開發(fā)人員,實現(xiàn)留給框架,包括」
QueryObject 查詢對象模式
內(nèi)存 Join 模式
寬表&冗余表模式
編輯:黃飛
評論
查看更多