DDD CQRS架構和傳統架構的優缺點比較

明天就是大年三十了,今天在家有空,想集中整理一下CQRS架構的特色以及相比傳統架構的優缺點分析。先提早祝你們猴年新春快樂、萬事如意、身體健康!前端

最近幾年,在DDD的領域,咱們常常會看到CQRS架構的概念。我我的也寫了一個ENode框架,專門用來實現這個架構。CQRS架構自己的思想其實很是簡單,就是讀寫分離。是一個很好理解的思想。就像咱們用MySQL數據庫的主備,數據寫到主,而後查詢從備來查,主備數據的同步由MySQL數據庫本身負責,這是一種數據庫層面的讀寫分離。關於CQRS架構的介紹其實已經很是多了,你們能夠自行百度或google。我今天主要想總結一下這個架構相對於傳統架構(三層架構、DDD經典四層架構)在數據一致性、擴展性、可用性、伸縮性、性能這幾個方面的異同,但願能夠總結出一些優勢和缺點,爲你們在作架構選型時提供參考。java

前言

CQRS架構因爲自己只是一個讀寫分離的思想,實現方式多種多樣。好比數據存儲不分離,僅僅只是代碼層面讀寫分離,也是CQRS的體現;而後數據存儲的讀寫分離,C端負責數據存儲,Q端負責數據查詢,Q端的數據經過C端產生的Event來同步,這種也是CQRS架構的一種實現。今天我討論的CQRS架構就是指這種實現。另外很重要的一點,C端咱們還會引入Event Sourcing+In Memory這兩種架構思想,我認爲這兩種思想和CQRS架構能夠完美的結合,發揮CQRS這個架構的最大價值。數據庫

數據一致性

傳統架構,數據通常是強一致性的,咱們一般會使用數據庫事務保證一次操做的全部數據修改都在一個數據庫事務裏,從而保證了數據的強一致性。在分佈式的場景,咱們也一樣但願數據的強一致性,就是使用分佈式事務。可是衆所周知,分佈式事務的難度、成本是很是高的,並且採用分佈式事務的系統的吞吐量都會比較低,系統的可用性也會比較低。因此,不少時候,咱們也會放棄數據的強一致性,而採用最終一致性;從CAP定理的角度來講,就是放棄一致性,選擇可用性。編程

CQRS架構,則徹底秉持最終一致性的理念。這種架構基於一個很重要的假設,就是用戶看到的數據老是舊的。對於一個多用戶操做的系統,這種現象很廣泛。好比秒殺的場景,當你下單前,也許界面上你看到的商品數量是有的,可是當你下單的時候,系統提示商品賣完了。其實咱們只要仔細想一想,也確實如此。由於咱們在界面上看到的數據是從數據庫取出來的,一旦顯示到界面上,就不會變了。可是極可能其餘人已經修改了數據庫中的數據。這種現象在大部分系統中,尤爲是高併發的WEB系統,尤爲常見。後端

因此,基於這樣的假設,咱們知道,即使咱們的系統作到了數據的強一致性,用戶仍是極可能會看到舊的數據。因此,這就給咱們設計架構提供了一個新的思路。咱們可否這樣作:咱們只須要確保系統的一切添加、刪除、修改操做所基於的數據是最新的,而查詢的數據沒必要是最新的。這樣就很天然的引出了CQRS架構了。C端數據保持最新、作到數據強一致;Q端數據沒必要最新,經過C端的事件異步更新便可。因此,基於這個思路,咱們開始思考,如何具體的去實現CQ兩端。看到這裏,也許你還有一個疑問,就是爲什麼C端的數據是必需要最新的?這個其實很容易理解,由於你要修改數據,那你可能會有一些修改的業務規則判斷,若是你基於的數據不是最新的,那意味着判斷就失去意義或者說不許確,因此基於老的數據所作的修改是沒有意義的。緩存

擴展性

傳統架構,各個組件之間是強依賴,都是對象之間直接方法調用;而CQRS架構,則是事件驅動的思想;從微觀的聚合根層面,傳統架構是應用層經過過程式的代碼協調多個聚合根一次性以事務的方式完成整個業務操做。而CQRS架構,則是以Saga的思想,經過事件驅動的方式,最終實現多個聚合根的交互。另外,CQRS架構的CQ兩端也是經過事件的方式異步進行數據同步,也是事件驅動的一種體現。上升到架構層面,那前者就是SOA的思想,後者是EDA的思想。SOA是一個服務調用另外一個服務完成服務之間的交互,服務之間緊耦合;EDA是一個組件訂閱另外一個組件的事件消息,根據事件信息更新組件本身的狀態,因此EDA架構,每一個組件都不會依賴其餘的組件;組件之間僅僅經過topic產生關聯,耦合性很是低。服務器

上面說了兩種架構的耦合性,顯而易見,耦合性低的架構,擴展性必然好。由於SOA的思路,當我要加一個新功能時,須要修改原來的代碼;好比原來A服務調用了B,C兩個服務,後來咱們想多調用一個服務D,則須要改A服務的邏輯;而EDA架構,咱們不須要動現有的代碼,原來有B,C兩訂閱者訂閱A產生的消息,如今只須要增長一個新的消息訂閱者D便可。數據結構

從CQRS的角度來講,也有一個很是明顯的例子,就是Q端的擴展性。假設咱們原來Q端只是使用數據庫實現的,可是後來系統的訪問量增大,數據庫的更新太慢或者知足不了高併發的查詢了,因此咱們但願增長緩存來應對高併發的查詢。那對CQRS架構來講很容易,咱們只須要增長一個新的事件訂閱者,用來更新緩存便可。應該說,咱們能夠隨時方便的增長Q端的數據存儲類型。數據庫、緩存、搜索引擎、NoSQL、日誌,等等。咱們能夠根據本身的業務場景,選擇合適的Q端數據存儲,實現快速查詢的目的。這一切都歸功於咱們C端記錄了全部模型變化的事件,當咱們要增長一種新的View存儲時,能夠根據這些事件獲得View存儲的最新狀態。這種擴展性在傳統架構下是很難作到的。架構

可用性

可用性,不管是傳統架構仍是CQRS架構,均可以作到高可用,只要咱們作到讓咱們的系統中每一個節點都無單點便可。可是,相比之下,我以爲CQRS架構在可用性方面,咱們能夠有更多的迴避餘地和選擇空間。併發

傳統架構,由於讀寫沒有分離,因此可用性要把讀寫合在一塊兒綜合考慮,難度會比較更大。由於傳統架構,若是一個系統的高峯期的併發寫入很大,好比爲2W,併發讀取也很大,好比爲10W。那該系統必須優化到能同時支持這種高併發的寫入和查詢,不然系統就會在高峯時掛掉。這個就是基於同步調用思路的系統的缺點,沒有一個東西去削峯填谷,保存瞬間多出來的請求,而必須讓系統無論遇到多少請求,都必須能及時處理完,不然就會形成雪崩效應,形成系統癱瘓。可是一個系統,不會一直處在高峯,高峯可能只有半小時或1小時;但爲了確保高峯時系統不掛掉,咱們必須使用足夠的硬件去支撐這個高峯。而大部分時候,都不須要這麼高的硬件資源,因此會形成資源的浪費。因此,咱們說基於同步調用、SOA思想的系統的實現成本是很是昂貴的。

而在CQRS架構下,由於CQRS架構把讀和寫分離了,因此可用性至關於被隔離在了兩個部分去考慮。咱們只須要考慮C端如何解決寫的可用性,Q端如何解決讀的可用性便可。C端解決可用性,我以爲是更加容易的,由於C端是消息驅動的。咱們要作任何數據修改時,都會發送Command到分佈式消息隊列,而後後端消費者處理Command->產生領域事件->持久化事件->發佈事件到分佈式消息隊列->最後事件被Q端消費。這個鏈路是消息驅動的。相比傳統架構的直接服務方法調用,可用性要高不少。由於就算咱們處理Command的後端消費者暫時掛了,也不會影響前端Controller發送Command,Controller依然可用。從這個角度來講,CQRS架構在數據修改上可用性要更高。不過你可能會說,要是分佈式消息隊列掛了呢?呵呵,對,這確實也是有可能的。可是通常分佈式消息隊列屬於中間件,通常中間件都具備很高的可用性(支持集羣和主備切換),因此相比咱們的應用來講,可用性要高不少。另外,由於命令是先發送到分佈式消息隊列,這樣就能充分利用分佈式消息隊列的優點:異步化、拉模式、削峯填谷、基於隊列的水平擴展。這些特性能夠保證即使前端Controller在高峯時瞬間發送大量的Command過來,也不會致使後端處理Command的應用掛掉,由於咱們是根據本身的消費能力拉取Command。這點也是CQRS C端在可用性方面的優點,其實本質也是分佈式消息隊列帶來的優點。因此,從這裏咱們能夠體會到EDA架構(事件驅動架構)是很是有價值的,這個架構也體現了咱們目前比較流行的Reactive Programming(響應式編程)的思想。

而後,對於Q端,應該說和傳統架構沒什麼區別,由於都是要處理高併發的查詢。這點之前怎麼優化的,如今仍是怎麼優化。可是就像我上面可擴展性裏強調的,CQRS架構能夠更方便的提供更多的View存儲,數據庫、緩存、搜索引擎、NoSQL,並且這些存儲的更新徹底能夠並行進行,互相不會拖累。理想的場景,我以爲應該是,若是你的應用要實現全文索引這種複雜查詢,那能夠在Q端使用搜索引擎,好比ElasticSearch;若是你的查詢場景能夠經過keyvalue這種數據結構知足,那咱們能夠在Q端使用Redis這種NoSql分佈式緩存。總之,我認爲CQRS架構,咱們解決查詢問題會比傳統架構更加容易,由於咱們選擇更多了。可是你可能會說,個人場景只能用關係型數據庫解決,且查詢的併發也是很是高。那沒辦法了,惟一的辦法就是分散查詢IO,咱們對數據庫作分庫分表,以及對數據庫作一主多備,查詢走備機。這點上,解決思路就是和傳統架構同樣了。

性能、伸縮性

原本想把性能和伸縮性分開寫的,可是想一想這兩個其實有必定的關聯,因此決定放在一塊兒寫。

伸縮性的意思是,當一個系統,在100人訪問時,性能(吞吐量、響應時間)很不錯,在100W人訪問時性能也一樣不錯,這就是伸縮性。100人訪問和100W人訪問,對系統的壓力顯然是不一樣的。若是咱們的系統,在架構上,可以作到經過簡單的增長機器,就能提升系統的服務能力,那咱們就能夠說這種架構的伸縮性很強。那咱們來想一想傳統架構和CQRS架構在性能和伸縮性上面的表現。

說到性能,你們通常會先思考一個系統的性能瓶頸在哪裏。只要咱們解決了性能瓶頸,那系統就意味着具備經過水平擴展來達到可伸縮的目的了(固然這裏沒有考慮數據存儲的水平擴展)。因此,咱們只要分析一下傳統架構和CQRS架構的瓶頸點在哪裏便可。

傳統架構,瓶頸一般在底層數據庫。而後咱們通常的作法是,對於讀:一般使用緩存就能夠解決大部分查詢問題;對於寫:辦法也有不少,好比分庫分表,或者使用NoSQL,等等。好比阿里大量採用分庫分表的方案,並且將來應該會所有使用高大上的OceanBase來替代分庫分表的方案。經過分庫分表,原本一臺數據庫服務器高峯時可能要承受10W的高併發寫,若是咱們把數據放到十臺數據庫服務器上,那每臺機器只須要承擔1W的寫,相對於要承受10W的寫,如今寫1W就顯得輕鬆不少了。因此,應該說數據存儲對傳統架構來講,也早已再也不是瓶頸了。

傳統架構一次數據修改的步驟是:1)從DB取出數據到內存;2)內存修改數據;3)更新數據回DB。總共涉及到2次數據庫IO。

而後CQRS架構,CQ兩端加起來所用的時間確定比傳統架構要多,由於CQRS架構最多有3次數據庫IO,1)持久化命令;2)持久化事件;3)根據事件更新讀庫。爲何說最多?由於持久化命令這一步不是必須的,有一種場景是不須要持久化命令的。CQRS架構中持久化命令的目的是爲了作冪等處理,即咱們要防止同一個命令被處理兩次。那哪種場景下能夠不須要持久化命令呢?就是當命令時在建立聚合根時,能夠不須要持久化命令,由於建立聚合根所產生的事件的版本號老是爲1,因此咱們在持久化事件時根據事件版本號就能檢測到這種重複。

因此,咱們說,你要用CQRS架構,就必需要接受CQ數據的最終一致性,由於若是你以讀庫的更新完成爲操做處理完成的話,那一次業務場景所用的時間極可能比傳統架構要多。可是,若是咱們以C端的處理爲結束的話,則CQRS架構可能要快,由於C端可能只須要一次數據庫IO。我以爲這裏有一點很重要,對於CQRS架構,咱們更加關注C端處理完成所用的時間;而Q端的處理稍微慢一點不要緊,由於Q端只是供咱們查看數據用的(最終一致性)。咱們選擇CQRS架構,就必需要接受Q端數據更新有一點點延遲的缺點,不然就不該該使用這種架構。因此,但願你們在根據你的業務場景作架構選型時必定要充分認識到這一點。

另外,上面再談到數據一致性時提到,傳統架構會使用事務來保證數據的強一致性;若是事務越複雜,那一次事務鎖的表就越多,鎖是系統伸縮性的大敵;而CQRS架構,一個命令只會修改一個聚合根,若是要修改多個聚合根,則經過Saga來實現。從而繞過了復瑣事務的問題,經過最終一致性的思路作到了最大的並行和最少的併發,從而總體上提升系統的吞吐能力。

因此,整體來講,性能瓶頸方面,兩種架構都能克服。而只要克服了性能瓶頸,那伸縮性就不是問題了(固然,這裏我沒有考慮數據丟失而帶來的系統不可用的問題。這個問題是全部架構都沒法迴避的問題,惟一的解決辦法就是數據冗餘,這裏不作展開了)。二者的瓶頸都在數據的持久化上,可是傳統的架構由於大部分系統都是要存儲數據到關係型數據庫,因此只能本身採用分庫分表的方案。而CQRS架構,若是咱們只關注C端的瓶頸,因爲C端要保存的東西很簡單,就是命令和事件;若是你信的過一些成熟的NoSQL(我以爲使用文檔性數據庫如MongoDB這種比較適合存儲命令和事件),且你也有足夠的能力和經驗去運維它們,那能夠考慮使用NoSQL來持久化。若是你以爲NoSQL靠不住或者沒辦法徹底掌控,那可使用關係型數據庫。但這樣你也要付出努力,好比須要本身負責分庫分表來保存命令和事件,由於命令和事件的數據量都是很大的。不過目前一些雲服務如阿里雲,已經提供了DRDS這種直接支持分庫分表的數據庫存儲方案,極大的簡化了咱們存儲命令和事件的成本。就我我的而言,我以爲我仍是會採用分庫分表的方案,緣由很簡單:確保數據可靠落地、成熟、可控,並且支持這種只讀數據的落地,框架內置要支持分庫分表也不是什麼難事。因此,經過這個對比咱們知道傳統架構,咱們必須使用分庫分表(除非阿里這種高大上可使用OceanBase);而CQRS架構,能夠帶給咱們更多選擇空間。由於持久化命令和事件是很簡單的,它們都是不可修改的只讀數據,且對kv存儲友好,也能夠選擇文檔型NoSQL,C端永遠是新增數據,而沒有修改或刪除數據。最後,就是關於Q端的瓶頸,若是你Q端也是使用關係型數據庫,那和傳統架構同樣,該怎麼優化就怎麼優化。而CQRS架構容許你使用其餘的架構來實現Q,因此優化手段相對更多。

結束語

我以爲不管是傳統架構仍是CQRS架構,都是不錯的架構。傳統架構門檻低,懂的人也多,且由於大部分項目都沒有什麼大的併發寫入量和數據量。因此應該說大部分項目,採用傳統架構就OK了。可是經過本文的分析,你們也知道了,傳統架構確實也有一些缺點,好比在擴展性、可用性、性能瓶頸的解決方案上,都比CQRS架構要弱一點。你們有其餘意見,歡迎拍磚,交流才能進步,呵呵。因此,若是你的應用場景是高併發寫、高併發讀、大數據,且但願在擴展性、可用性、性能、可伸縮性上表現更優秀,我以爲能夠嘗試CQRS架構。可是還有一個問題,CQRS架構的門檻很高,我認爲若是沒有成熟的框架支持,很難使用。而目前據我瞭解,業界尚未不少成熟的CQRS框架,java平臺有axon framework, jdon framework;.NET平臺,ENode框架正在朝這個方向努力。因此,我想這也是爲何目前幾乎沒有使用CQRS架構的成熟案例的緣由之一。另外一個緣由是使用CQRS架構,須要開發者對DDD有必定的瞭解,不然也很難實踐,而DDD自己要理解沒個幾年也很難運用到實際。還有一個緣由,CQRS架構的核心是很是依賴於高性能的分佈式消息中間件,因此要選型一個高性能的分佈式消息中間件也是一個門檻(java平臺有RocketMQ),.NET平臺我我的專門開發了一個分佈式消息隊列EQueue,呵呵。另外,若是沒有成熟的CQRS框架的支持,那編碼複雜度也會很複雜,好比Event Sourcing,消息重試,消息冪等處理,事件的順序處理,併發控制,這些問題都不是那麼容易搞定的。而若是有框架支持,由框架來幫咱們搞定這些純技術問題,開發人員只須要關注如何建模,實現領域模型,如何更新讀庫,如何實現查詢,那使用CQRS架構纔有可能,由於這樣纔可能比傳統的架構開發更簡單,且能得到不少CQRS架構所帶來的好處。

相關文章
相關標籤/搜索