你們好,我叫湯雪華。我平時工做使用Java,業餘時間喜歡用C#作點開源項目,如ENode, EQueue。我我的對DDD領域驅動設計、CQRS架構、事件溯源(Event Sourcing,簡稱ES)、事件驅動架構(EDA)這些領域比較感興趣。我但願把本身所學的知識可否分享給你們,因此,把這個領域裏的一些知識串聯了起來,整理了一個PPT,併爲每張PPT配備註釋,分享給你們。但願能對這個領域有興趣的朋友有所幫助。html
上面的提綱是今天主要分享的內容概要。開始以前想先說一下微服務架構和CQRS架構的區別和聯繫。node
微服務架構如今很熱,處處能夠看到各大互聯網公司的微服務道路的分享總結。可是,我今天的分享和微服務沒有關係,但願能夠帶給你們一些新的東西。若是必定要說微服務和CQRS架構的關係,那我以爲微服務是一種邊界思惟,微服務的目的是爲了從業務角度拆分(職責分離)當前業務領域的不一樣業務模塊到不一樣的服務,每一個微服務之間的數據徹底獨立,它們之間的交互能夠經過SOA RPC調用(耦合比較高),也能夠經過EDA 消息驅動(耦合比較低);mysql
微服務架構和CQRS架構的關係:每一個微服務內部,咱們能夠用CQRS/ES架構來實現,也能夠用傳統三次架構來實現;git
首先,咱們須要先理解DDD中的聚合、聚合根這兩個概念。github
聚合,它經過定義對象之間清晰的所屬關係和邊界來實現領域模型的內聚,並避免了錯綜複雜的難以維護的對象關係網的造成。聚合定義了一組具備內聚關係的相關對象的集合,咱們把聚合看做是一個修改數據的最小原子單元。聚合根,每一個聚合都有一個根對象,根對象管理聚合內的其餘子對象(實體、值對象);聚合之間的交互都是經過聚合根來交互,不能繞過聚合根去直接和聚合下的子實體進行交互。上面的例子中,Car、Wheel、Position、Tire四個對象構成一個聚合,其中Car是聚合根;Customer也是聚合根,Customer不能直接訪問Car下的Tire(子實體),而是隻能經過聚合根Car來訪問。sql
上面表達了一個關於聚合的一致性設計原則:聚合內的數據修改,是ACID強一致性的;跨聚合的數據修改,是最終一致性的。遵照這個原則,可讓咱們最大化的下降併發衝突,從而最大化的提升整個系統的吞吐。數據庫
In-Memory的意思是指整個系統中的全部的聚合根對象都活在內存。而不是像咱們平時那樣,用到的時候才從DB獲取對象,而後再作修改,再保存回去。瀏覽器
在In-Memory的架構下,當要修改某個聚合根的狀態時,它已經在內存,咱們能夠直接拿到該對象的引用,且框架會盡可能保證聚合根對象的狀態就是最新的。聚合根是在內存中的最小計算單元,每一個聚合內部都封裝了業務規則,並保證數據的強一致性。緩存
上圖我是挪用了以前比較或的LMAX架構中的一個圖,表達的思想就是in-memory架構。其中Business Logic Processor就是中央業務邏輯處理器,內部承載了大量在機器內存中活着的聚合根對象。服務器
接下來,咱們再來看一下什麼是事件溯源。
一個對象從建立開始到消亡會經歷不少事件,之前咱們是在每次對象參與完一個業務動做後把對象的最新狀態持久化保存到數據庫中,也就是說咱們的數據庫中的數據是反映了對象的當前最新的狀態。而事件溯源則相反,不是保存對象的最新狀態,而是保存這個對象所經歷的每一個事件,全部的由對象產生的事件會按照時間前後順序有序的存放在數據庫中。能夠看出,事件溯源的這種作法是更符合事實觀的,由於它完整的描述了對象的整個生命週期過程當中所經歷的全部事件。
那麼,事件到底如何影響一個領域對象的狀態的呢?很簡單,當咱們在觸發某個領域對象的某個行爲時,該領域對象會先產生一個事件,而後該對象本身響應該事件並更新其本身的狀態,同時咱們還會持久化在該對象上所發生的每個事件;這樣當咱們要從新獲得該對象的最新狀態時,只要先建立一個空的對象,而後將和該對象相關的全部事件按照事件發生前後順序從先到後再所有應用一遍便可還原獲得該對象的最新狀態,這個過程就是所謂的事件溯源。
另外一方面,由於是用事件來表示對象的狀態,而事件是隻會增長不會修改。這就能讓數據庫裏的表示對象的數據很是穩定,不可能存在DELETE或UPDATE等操做。由於一個事件就是表示一個事實,事實是不能被磨滅或修改的。這種特性可讓領域模型很是穩定,在數據庫級別不會產生併發更新同一條數據的問題。
經過上面這個圖,你們應該能夠更直觀的理解事件溯源和傳統CRUD思想的區別。
Actor模型,這個概念你們應該都瞭解。Actor模型的核心思想是,對象直接不會直接調用來通訊,而是經過發消息來通訊。每一個Actor都有一個Mailbox,它收到的全部的消息都會先放入Mailbox中,而後Actor內部單線程處理Mailbox中的消息。從而保證對同一個Actor的任何消息的處理,都是線性的,無併發衝突。從全局上來看,就是整個系統中,有不少的Actor,每一個Actor都在處理本身Mailbox中的消息,Actor之間經過發消息來通訊。
Akka框架就是實現Actor模型的並行開發框架,而且Akka框架融入了聚合、In-Memory、Event Sourcing這些概念。Actor很是適合做爲DDD聚合根。Actor的狀態修改是由事件驅動的,事件被持久化起來,而後經過Event Sourcing的技術,還原特定Actor的最新狀態到內存。
上圖表達的是事件驅動的架構的思想。Node表示節點,每一個節點負責處理邏輯;Event表示消息,節點之間經過消息進行通訊。消息經過分佈式消息隊列如RocketMQ,Equeue進行通訊。
事件驅動架構的核心思想是:
上圖是一個面向Topic的分佈式MQ的邏輯架構圖,採用這種架構的MQ有:Kafka,RocketMQ,EQueue
好了,上面是基本概念的介紹。接下來咱們來看一下CQRS/ES架構。
上圖是CQRS架構的典型架構圖。
CQRS自己只是一個讀寫分離的架構思想,全稱是:Command Query Responsibility Segregation,即命令查詢職責分離,表示在架構層面,將一個系統分爲寫入(命令)和查詢兩部分。一個命令表示一種意圖,表示命令系統作什麼修改,命令的執行結果一般不須要返回;一個查詢表示向系統查詢數據並返回。
CQRS架構中,另一個重要的概念就是事件,事件表示命令操做領域中的聚合根,而後聚合根的狀態發生變化後產生的事件。
因爲CQRS架構的一致性模型爲最終一致性,因此,你的系統要接受查詢到的數據可能不是最新的,而是有幾個毫秒的延遲。之因此會有這個前提,是由於CQRS架構考慮到,做爲一個多用戶同時訪問的互聯網應用,當在高併發修改數據的狀況下,好比秒殺、12306購票等場景,用戶UI上看到的數據老是舊的。好比你秒殺時提交訂單前看到庫存還大於0,可是當你提交訂單時,系統提示你寶貝賣完了。這個就說明,在這種高併發修改同一資源的狀況下,任何人看到的數據老是Stale的,即舊的。
這裏我主要分享的CQRS架構是上面第3種實現方式,也就是上圖所畫的架構。在我心目中,只有第三種纔是真正意義上的CQRS架構。
C端的命令的執行流程
客戶端如(MVC Controller)發送命令通知系統作修改:
Q端的查詢的執行流程
客戶端如(MVC Controller)發出查詢請求系統返回數據:
讀庫能夠有不少種,依據咱們的業務場景來選擇:好比關係型DB、分佈式緩存等NoSQL、搜索引擎,etc.
前面的CQRS架構圖我介紹了CQRS架構的基本概念、設計初衷、一致性模型、實現方式、適用場景、架構的基本數據流這些方面。但這不是CQRS架構的所有,咱們還能夠挖掘出更多有用的特性出來。好比假設咱們爲這個架構引入如下一些特性,就能夠達到更多意想不到的好處:
經過引入上面這些架構設計原則,咱們可讓CQRS架構的C端更強大,性能更高;固然,複雜性也大大增長。因此,要完成這樣一套架構,沒有成熟框架的支撐,是幾乎不可能的,ENode框架就是在爲作這樣的一個框架而努力。
咱們能夠從上面幾個非功能性特性去考察這個架構。大部分你們應該均可以體會到,關於消息的冪等處理這塊,CQRS\ES這個架構能夠作的很是完全。
平時傳統咱們的消息驅動的架構,或者是RPC調用的SOA風格的應用,消息處理者或者服務被調用方,必須本身作到數據修改的冪等性。而冪等性的實現思路也不少,好比用kv來判重,用DB的惟一索引,等等。
而CQRS\ES架構,因爲使用了Event Sourcing的技術,因此能夠直接在EventStore中自動作到聚合根併發修改的衝突的檢測、以及同一個命令的重複處理的檢測。並能通知框架自動作併發處理或作從新發布該命令所產生的事件;
你們可能會疑問,爲什麼已經將命令經過聚合根ID進行路由了,且同一臺機器內頁已經經過Actor Mailbox技術解決併發問題了,仍是有併發衝突的可能呢?緣由是當咱們的服務器在出現擴容或縮容時,會出現因爲集羣中服務器變更致使的同一個聚合根的不一樣命令可能會在不一樣的機器上同時被處理,從而致使併發衝突。
最後,關於這個架構的瓶頸,相信你們已經能夠發現,是在EventStore。因此,這就要求咱們設計一個超高性能的EventStore數據庫。具體見後面的介紹吧。
上面這個圖演示了,當C端產生的事件,在Q端的處理順序若是不一致時,致使Q端的結果和C端不一致了。因此,事件的處理順序必須和產生的順序一致,這點必須保證,但能夠由框架來保證,開發者無需關注。須要強調的是,這個順序處理事件不須要交給分佈式消息中間件來保證,而是應該交給Consumer來本身保重。當Consumer收到一個版本爲N+2的時間,而當前Q端的版本爲N,則N+2的消息須要先hold一下,不要當即處理。而後等待N+1的事件過來,N+1的事件過來並處理後,再處理N+2的事件。若是N+1的事件一直不過來,則須要永遠等待。總之,這裏的順序必須保證。若是這個順序交給分佈式消息中間件去保證,那性能上會很是差,而要讓分佈式消息中間件實現絕對意義上的順序消費,又要實現高可用,高性能,難度很大。我我的不太同意,除非是Consumer本身沒法處理消息順序的場景才無可奈何讓分佈式消息中間件來保證,好比mysql binlog的同步。
上圖演示了假設一個命令修改兩個或多個聚合根時,會致使阻塞大大增長,從而整個系統的吞吐會下降。而好處是,咱們能夠獲得聚合根之間的數據的強一致性。
上圖演示了,當一個命令只修改一個聚合根時,先經過一級路由,將聚合根路由到分佈式MQ的同一個隊列裏,而後同一個隊列老是被一臺固定的機器消費,從而保證同一個聚合根的命令老是在一臺機器上處理。
上圖掩演示了,當命令進入一臺機器後,再經過Command Mailbox的二次路由,一樣是根據聚合根ID,從而保證單個機器內,同一個聚合根的命令的處理是順序線性的,從而避免了併發衝突。
EventStore處理併發和命令冪等的根本設計就是上圖的兩個惟一索引。
1. 聚合根ID + 事件版本號惟一;
2. 聚合根ID + 命令ID惟一;
當萬一出現了併發衝突,則框架須要取出從新加載該聚合根的最新狀態,而後重試當前命令;當出現了命令的重複處理,則框架須要把該命令以前產生的事件再從新取出來,發佈到分佈式消息中間件。由於有可能以前雖然這個事件被持久化了,但理論山有可能這個事件沒有成功發佈到分佈式消息中間件(由於那個時候斷電了,夠倒黴的,呵呵)。因此,事件的消費者可能會再次收到這個事件,並處理。但這麼作都是爲了保證整個業務流的最終一致性。想一想以前的EDA的架構圖的說明吧。
下面咱們來看看CQRS架構下,開發者須要寫的代碼有哪些?
首先是須要定義Command和Event。其中Command至關於DDD經典四層架構中的應用層的一個方法的參數。
Command表示命令系統作什麼,表達一種意圖,在架構上設計爲一個DTO便可。Event表示一個事件,表示領域內發生了什麼狀態變化,用過去式命名事件。事件是隻讀的。
Command Handler是無狀態的,用於處理一個或多個命令,不一樣的命令有不一樣的Handle方法。一個Command Handler作的典型的事情就兩個:
框架能夠作到開發人員無需關注底層的技術問題,好比如何存儲聚合根產生的事件,如何發佈事件到MQ;完全作到技術架構和業務邏輯分離。這點在傳統架構下是很難作到的。
Note表示一個DDD聚合根,這裏最核心的概念是:Note內部的狀態的修改都是經過事件來驅動的,也就是Note要作任何修改前,老是應該先產生事件,而後框架根據事件調用到對應的Handle方法,而後咱們在Handle方法中修改Note的內部狀態。
爲什麼要獨立拆分出Handle方法呢?由於是在Event Souring事件溯源還原聚合根狀態時,框架須要調用這些Handle方法。根據Event Sourcing的思想,會根據Note聚合根的ID獲取該聚合根的全部的事件,而後按照事件發生的順序,分別調用每一個事件的Handle方法,就能夠還原出聚合根的最新狀態了。
最後一個須要開發者寫的代碼就是Event Handler,根據CQRS架構的定義,Event Handler負責根據C端產生的事件來更新讀庫。上面的例子只是記錄日誌,實際咱們須要在Handle方法中更新讀庫,如數據庫,分佈式緩存等。
這是ENode中今年打算實現的文件版本的EventStore的設計思路,目前是使用的DB來實現的。我如今在作EQueue的高可用,等這個作完,就開始作EventStore的文件版本。上面PPT中的設計思路,還但願能和你們多多交流,一塊兒完善它。由於它是整個CQRS/ES架構的核心所在。
前面介紹了不少CQRS\ES架構方面的東西,最後咱們再看兩個實際應用的場景:秒殺、12036購票。
要實現高併發的訂單處理(生成訂單、預扣庫存兩個核心步驟)。淘寶作的很牛逼,能夠在這兩個步驟都完成後直接告訴用戶下單結果,固然,我認爲CQRS架構也徹底能夠在保證這兩點處理後再返回買家的前提下,實現淘寶同樣的吞吐。
這裏我列舉這些訂單狀態的目的,主要是想表達第一個狀態用意:訂單處理中。經過引入這個狀態,咱們處理訂單的的代價就輕不少了,不須要在完成生成訂單、預扣庫存這兩個核心步驟就能夠返回客戶端瀏覽器了。買家訂單提交成功後,服務端首先在分佈式緩存中檢查商品的庫存是否足夠,若是不夠,則當即返回並通知買家寶貝賣完了;若是足夠,則發送下單的命令到MQ(異步處理訂單)。而後通知買家「您好,您的訂單已收到,正在處理中。請稍後到個人訂單中心查看訂單處理結果。祝您購物愉快!」之類的提示。
而後當買家進入「個人訂單中心」查看訂單時,可能的狀況有:
經過這樣的訂單狀態的設計和交互體驗,至關於把輪訓查看訂單處理結果的職責交給了買家。而這個小小的設計,帶來的好處是極大的方便咱們實現很是高的訂單處理吞吐了。固然,若是咱們能作到像淘寶這樣的體驗,就是下單時直接告訴結果,那天然最好了。只是這樣代價更大而已。我提出這個例子的緣由是CQRS架構是一種C端異步處理命令的架構,因此在這種架構上,咱們須要一切儘可能以異步爲出發點去思考和設計業務流程,設計用戶交互體驗。實際上這個體驗在亞馬遜上買東西,你可能會遇到,甚至亞馬遜直接讓你去你的郵箱看訂單處理結果。因此,我以爲這裏只是一個購物習慣的差異,但對技術的要求卻差異很大。
上圖描述了一個DDD CQRS架構的典型的Saga的設計,對應前面的秒殺場景的訂單處理流程。
上圖中,Order、Conference、Payment爲三個聚合根,分別表示訂單、庫存、支付;Order Process Manager是無狀態的,表示一個流程管理器,CQRS架構中通常叫Saga。流程管理器的設計理念是:訂閱事件,根據不一樣的事件,發送不一樣的命令。也就是說,流程管理器的職責是對流程進行建模,負責封裝流程控制邏輯,而聚合根負責業務邏輯。整個訂單處理的流程大概爲業務層面的2PC。即下單時,要先預扣庫存;而後,買家付款後要真正扣庫存。
上圖中,棕色的線條表示命令,藍色的線條表示事件。
Saga是CQRS架構中處理複雜業務流程的典型作法,經過事件驅動的方式去替代傳統的分佈式事務。犧牲強一致性的方式來提升系統的吞吐。實際上,在高併發的狀況下,有時咱們不得不選擇最終一致性,由於分佈式事務的成本過高。
這個案例是關於12306購票的例子,上面說了核心的業務場景和領域概念。我舉這個例子的用意是爲了說明,12306購票的場景,C端的領域模型是比傳統的電商網站要複雜不少的,由於庫存是一個動態的概念。不像普通電商,一個庫存跟着SKU,很簡單。12306你買了一個車子的某個區間的票以後,這個區間內的其餘的票的庫存數都會發生變化,並且這個庫存數還要考慮座位的分配,很是複雜。
這個場景,就是我上面說的CQRS的應用場景中的:要知足高併發的寫、高併發的查詢,同時C端的業務模型很是複雜。要同時面對這3點,實現這個系統是很難的。
我認爲,這個場景的難點不在於技術層面,而是在於DDD領域建模層面。你們若是對這個場景的領域模型,架構實現,以及示例代碼感興趣,能夠看我下面的兩個地址:
淺談12306核心模型設計思路和架構設計
http://www.cnblogs.com/netfocus/p/5187241.html
12306購票領域建模示例代碼:
https://github.com/tangxuehua/enode,具體看ENode開源項目中的E12306案例代碼。
若是你們對這個領域感興趣,能夠訪問個人博客。我博客中錄製了大量的視頻介紹,視頻介紹彙總地址:
http://www.cnblogs.com/netfocus/p/4707789.html
謝謝你們!