回首
哈嘍~大家好前鹅,時間過的真快摘悴,關于DDD領域驅(qū)動設計的講解基本就差不多了,本來想著周四再開一篇舰绘,感覺沒有太多的內(nèi)容了蹂喻,剩下的一個就是驗證的問題,就和之前的JWT很類似捂寿,就不打開一個章節(jié)了口四,而且這個也不是領域驅(qū)動設計范疇之內(nèi)的,下一個系列 Ids 的講解中秦陋,可能會穿插著講一講蔓彩,然后到時候正好一起完善了。
雖然是完結了驳概,不過心里還是不是很開森呀赤嚼,通過小伙伴的反饋拜效,然后我也咨詢了官方的建議漓骚,好像這個DDD領域驅(qū)動設計系列蛙埂,并沒有得到很多的支持灌侣,影響力完全比不過第一個系列《從壹開始前后端分離》,原因可能是鲫忍,我也沒有在項目中真正的使用過DDD的原因吧舰始,也或許是寫的比較生硬第焰,主要我也一直在研究,不過我這里一定要說一下上枕,還是要多看看的绳瘟,不一定要看我講的,可以看看書也行姿骏,或者看看別人的博客,DDD領域驅(qū)動設計思想真的很不錯斤彼,然后還夾帶著CQRS命令查詢職責分離分瘦、Bus總線思想、EDA事件驅(qū)動思想琉苇、ES事件溯源思想(今天要說到的)嘲玫、消息隊列等等這些以前沒有接觸到的思想設計,也為微服務打下了一定的基礎并扇,如果沒有這些基礎去团,你是很難理解為什么要使用微服務的,這里我們就先來回顧一下這些天我們都說了什么內(nèi)容吧:
- 我們第一次開始討論DDD領域驅(qū)動設的概念已經(jīng)我的個人計劃書
- DDD入門 & 項目整體的第一次搭建
- 領域穷蛹、子域土陪、限界上下文
- 又一次討論了DDD設計思想的重大意義 以及使用EFCore
- 實體 與 值對象
- 聚合 與 聚合根
- 第一次把項目跑通,然后也簡單說了下 CQRS
- 剪不斷理還亂的 值對象和Dto
- 明白領域驗證**
FluentValidator
** - 命令總線 Bus 分發(fā)(一)
- 事件驅(qū)動 EDA 詳解
算上今天的內(nèi)容肴熏,正好是十二篇鬼雀,也是我的比較喜歡的一個數(shù)字(之前在文章中說到過這個原因,這里就不多說了)蛙吏,也是很辛苦寫了這么多源哩,希望有時間有精力的時候,還是要多看看的鸦做,多品品思想励烦,這樣我們就不會一直問一些虛無縹緲的問題了,雖然我現(xiàn)在是越學的多泼诱,越不會的多??坛掠。
可能你也發(fā)現(xiàn)了今天的題目有些不一樣,因為我之前說過坷檩,要在圣誕節(jié)簡單搞個小活動却音,既然說了,就不能食言矢炼,不過目前我寫了十六萬字了系瓢,就一個小伙伴給了我一塊錢紅包??,好失敗句灌,所以我就簡單來個小福利吧夷陋,因為這個系列的名字就是Christ3D欠拾,當時就是想著在圣誕節(jié)前能說完,還可以骗绕,緊趕慢趕的說完了藐窄,我就想著一個給粉絲一個小小小福利:
具體的參與形式看文章末尾:(已結束)
1、免費給送三本書酬土,可能是《實現(xiàn)領域驅(qū)動設計》這本書荆忍,或者《領域驅(qū)動設計 軟件核心復雜性應對之道》,還有我本人的簽名+賀卡喲哈哈撤缴;
2刹枉、本來想抽十位粉絲,送精裝的圣誕節(jié)蘋果屈呕,但是考慮食品安全問題就算了微宝,直接到時候發(fā)紅包吧(時間地點保密,提示:為了老粉絲)虎眨;
緣起
言歸正傳蟋软,今天的重點還是要好好的說說新知識——事件溯(su)源,Event Source嗽桩,也有人翻譯事件采購岳守,或者是事件回溯,或者直接就是ES涤躲,其實都是一個意思棺耍,要是下次你發(fā)現(xiàn)這幾個詞語的時候,都是指的事件溯源种樱,其實事件溯源已經(jīng)有一只腳邁進了微服務的大家族了蒙袍,甚至可以說已經(jīng)在微服務的一員了,他配合著事件總線EventBus嫩挤、消息隊列等害幅,在微服務的工作中起著一定的作用。當然今天只是簡單的入門講解岂昭,要是想打開真正的微服務的大門以现,就需要大家自己去探索,當然约啊,我也會繼續(xù)跟進這個講解邑遏,下一個系列 Ids ,其實也是微服務的一個分支恰矩,慢慢來记盒,希望大家多捧場啦!
馬上開始今天的講解外傅,還是一天一問吧纪吮,希望大家?guī)е@個問題通讀本文俩檬,自己能想到合適的答案:
1、你認為事件存儲 EventStore 和 日志記錄的區(qū)別是什么碾盟?
這里要給大家再強調(diào)兩點:
1棚辽、CQRS、EDA和ES這些其實已經(jīng)不在DDD設計的范圍之內(nèi)冰肴,只不過這些技術都是一起使用的屈藐,多個技術的相互結合使用,才能發(fā)揮很大的作用熙尉,所以說本系列教程是
DDD+CQRS+EDA+ES的結合體估盘,以后被別人問到的時候,可別說時間溯源就是領域驅(qū)動設計的一部分喲骡尽。
2、事件溯源不是一兩句能說清的擅编,這篇文章只是一個啟蒙的作用攀细,等大家從事微服務工作的時候,就知道它深層次的意義了爱态,切不可和平時的 CURD 項目生搬硬套做比較谭贪。
零、今天要實現(xiàn)右下角綠色的部分
(我寫的十二篇文章中的知識點锦担,這里基本都有了俭识,也算是一個圓滿了,集齊七顆啦??)
一洞渔、什么是事件溯源 —— Event Source
時間溯源其實很好理解套媚,首先從字面上的理解:
事件就是 Event,溯是一個動詞磁椒,可以理解為追溯堤瘤,回溯的意思,源代表原始浆熔、源頭的意思本辐,合起來表示一個事件追蹤源的過程。
1医增、理解事件溯源的概念
我們知道慎皱,一個對象從創(chuàng)建開始到消亡會經(jīng)歷很多階段,同樣也會經(jīng)歷很多個事件叶骨,現(xiàn)在我們在設計的時候茫多,都是在每次對象參與完一個業(yè)務動作后,把它最新的狀態(tài)持久化保存到數(shù)據(jù)庫中邓萨,也就是說我們的數(shù)據(jù)庫中的數(shù)據(jù)是反映了對象的當前最新的狀態(tài)地梨。當然我們也是一直這么使用的(面向?qū)ο缶幊蹋┚站怼H欢录菰磩t相反,它不是保存對象的最新狀態(tài)宝剖,而是保存這個對象所經(jīng)歷的每個事件洁闰,所有的由對象產(chǎn)生的事件會按照時間先后順序有序的存放在數(shù)據(jù)庫中——這個就是事件存儲 Event Store,然后我們一一取出來做處理就是事件溯源 Event Source万细∑嗣迹可以看出,事件溯源的這種做法是更符合事實觀的赖钞,因為它完整的描述了對象的整個生命周期過程中所經(jīng)歷的所有事件腰素。
你一定會問,為什么記錄事件雪营,就是更符合客觀事實呢弓千,為什么會這么說呢?別慌献起,聽我繼續(xù)往下說洋访。
2、事件溯源的執(zhí)行過程
這個時候你仔細想一想谴餐,我們在和領域?qū)<?/strong>(默認他們不懂技術)討論用戶下單流程的時候姻政,專家一定會說:客戶首先選擇一個商品,然后添加到購物車岂嗓,確認無誤下單汁展,接著用戶支付,支付成功后厌殉,就給用戶發(fā)貨食绿。而我們呢,我們作為一個開發(fā)人員公罕,和領域?qū)<矣懻摰臅r候炫欺,自然而然的也是這么思考的,對不對Q帧(你肯定在討論需求的時候用的不是數(shù)據(jù)庫的思維F仿濉),只不過我們后期開發(fā)的時候摩桶,拘泥于技術和數(shù)據(jù)優(yōu)先的思維桥状,不得不轉(zhuǎn)向CURD的道路了,當然這個沒有什么錯誤硝清,我只是說明一點辅斟,事件存儲真的離我們不遠。
那我們平時是怎么做的呢芦拿,這里說一個特別簡單的:
從這個特別簡單的流程中我們可以看到士飒,平時我們都是直接操作的 Order 這個領域聚合根查邢,一直在修改模型狀態(tài),這個看似正常的操作下酵幕,有一些問題扰藕,是我們建立在每一步都正常執(zhí)行的情況下,不過一般總會出現(xiàn)一些問題芳撒,特別是分布式的環(huán)境中邓深。
然而,事件溯源與上述的情況恰好相反笔刹,它并不關心當前狀態(tài)芥备,而是關注持續(xù)不斷的變化事件。
舉個例子舌菜,假設我們有一個“購物車”萌壳,我們可以創(chuàng)建購物車,往里面添加商品或移除商品日月,然后結賬讶凉。
購物車的生命周期可以包含如下一系列事件:
創(chuàng)建購物車
往購物車里添加商品
再次往購物車里添加商品
從購物車里移除商品
結賬
這些就是一個購物車的生命周期,包含了一系列事件山孔。這就是事件溯源,非常簡單吧荷憋?
幾乎所有的流程都可以被看成一系列事件台颠。在與領域?qū)<医徽剷r,他們不會提及“表”和“連接”勒庄,他們會將流程描述成一系列事件以及可以應用在這些事件上的規(guī)則串前。
3、事件溯源是如何更新實時狀態(tài)的
那么实蔽,事件到底如何影響一個領域?qū)ο蟮臓顟B(tài)的呢荡碾?很簡單,當我們在觸發(fā)某個領域?qū)ο蟮哪硞€行為時局装,該領域?qū)ο髸犬a(chǎn)生一個事件坛吁,然后該對象自己響應該事件并更新其自己的狀態(tài),同時我們還會持久化在該對象上所發(fā)生的每一個事件铐尚;這樣當我們要重新得到該對象的最新狀態(tài)時拨脉,只要先創(chuàng)建一個空的對象,然后將和該對象相關的所有事件按照事件發(fā)生先后順序從先到后再全部應用一遍即可還原得到該對象的最新狀態(tài)宣增,這個過程就是所謂的事件溯源玫膀。
二、事件溯源的存在意義與問題
事件溯源不是萬能的爹脾,不過它可以在某一些領域發(fā)揮很大的作用帖旨,這個在以后的微服務設計中箕昭,會更能體現(xiàn)出來,那我們就簡單說兩點:
1解阅、傳統(tǒng)應用中出現(xiàn)的某些問題
從業(yè)務的角度
傳統(tǒng)的應用中落竹,數(shù)據(jù)庫里存的是Domain Model的實例的當前狀態(tài),比如某個儲戶銀行賬戶的存款數(shù)瓮钥,通常是一個數(shù)字.如果考慮到如下的三個情形,我們可能付出的代價比較大:
1) 老規(guī)則:問題跟蹤
如果某個儲戶的賬戶出現(xiàn)問題筋量,那么我們只有從大到PB的日志中去分析用戶的賬戶數(shù)據(jù)是如何出錯的,而且我們在做日志的時候碉熄,不可能所有的都考慮到桨武,就算是把全部數(shù)據(jù)都保存,時間都記下來锈津,操作者都備份呀酸,那ATM機信息呢?(可能不恰當琼梆,只是說明我們總有想不到的地方)性誉,但如果一旦日志不夠詳細,找出問題根源基本只能靠猜了茎杂。
2) 新需求:趨勢分析
歷史數(shù)據(jù)的作用在于分析未來的趨勢错览,如果僅僅從浩如煙海的日志中尋找規(guī)律,我們還得單獨寫邏輯煌往,對日志進行建模倾哺,清洗,其實我們已經(jīng)能接受刽脖,日志就是用來記錄異常信息的羞海,這個時候我們就很崩潰了。
3) 更奇葩:事務回滾
在介紹事務修正模式中曲管,我們講到某個步驟發(fā)生錯誤却邓,之前的各個節(jié)點可以自己獨立地完成回滾,回滾的依據(jù)就是記錄的操作步驟及相關參數(shù)院水,根據(jù)這些有用信息就可以每個節(jié)點自行回滾到原始狀態(tài)腊徙,并且在失敗的時候可以retry
可見存儲對于Domain Model 的各個事件還是非常有用的,尤其是對于復雜的系統(tǒng),這也就是我們今天要討論的事件溯源模式.
從技術角度
大多數(shù)的應用都和數(shù)據(jù)打交道檬某,最常見的打交道方式就是將用戶在使用過程中的數(shù)據(jù)最終狀態(tài)同步到數(shù)據(jù)庫中昧穿。例如,在傳統(tǒng)的增刪改查(CURD)模式中橙喘,一個典型的數(shù)據(jù)過程就是從數(shù)據(jù)庫中讀出數(shù)據(jù)时鸵,修改完后再把修改后的數(shù)據(jù)更新到數(shù)據(jù)庫中——通常來說,在這個更新過程這張數(shù)據(jù)表是被鎖住的。
這種傳統(tǒng)的增刪改查(CURD)方式存在一些局限性:
- 事實上執(zhí)行這種直接依賴數(shù)據(jù)庫的增刪改查(CRUD)開銷會影響系統(tǒng)的性能和響應性饰潜,不利于系統(tǒng)的可伸縮性初坠。
- 在一個存在多個用戶并發(fā)操作的領域中,因為多個用戶也許會同時操作同一張表彭雾,所以數(shù)據(jù)更新造成的沖突更加可能發(fā)生碟刺。
- 除非系統(tǒng)額外有一個可以記錄所有業(yè)務細節(jié)的日志系統(tǒng)以實現(xiàn)審查機制,否則所有的歷史都會丟失薯酝。
這是一個大問題半沽。在以表作為驅(qū)動的系統(tǒng)里,你只保存了系統(tǒng)的當前狀態(tài)吴菠,你根本就無法知道系統(tǒng)是如何達到當前狀態(tài)的者填。如果我問你“這個用戶修改了幾次郵件地址”,你有辦法回答嗎做葵?或者我再問“有多少人把一件商品添加到購物車里占哟,然后又移除掉,直到一個月之后才買了那件商品”酿矢,你就更沒法回答了榨乎。你存儲數(shù)據(jù)的方式丟掉了很多有用的業(yè)務信息!
2瘫筐、我們有哪些理由使用Event Sourcing(優(yōu)點)
盡管它是一個簡單的模式蜜暑,但使用它有很多優(yōu)點:
事件日志具有很高的商業(yè)價值;
它在DDD和事件驅(qū)動架構下運行得非常好。
調(diào)試用應用程序狀態(tài)中所有變更的來源;
它允許您重放失敗的事件;
易于調(diào)試策肝,您可以將目標實體的所有事件復制到您的機器并調(diào)試每個事件肛捍,以了解應用程序如何達到特定狀態(tài)(忽略從生產(chǎn)環(huán)境復制數(shù)據(jù)的安全隱患);
允許您使用追溯事件模式重建/修復您的狀態(tài)。
許多作者還將優(yōu)先級作為時間查詢的能力驳糯,但我認為查詢多個后續(xù)事件不是一項簡單的任務。因此氢橙,我通常認為時間查詢是快照模式的一個優(yōu)點酝枢。
有許多理由使用Event Sourcing,當你瀏覽Greg Young的系列文章和談話你會發(fā)現(xiàn)下面要點:
1. 它不是一個新概念悍手,真實世界中許多領域都很像它帘睦,看看你的銀行賬戶狀態(tài),比如儲蓄卡坦康,它打印出一筆筆進出明細和當前余額竣付,這一筆筆代表了領域事件。
2.通過重播事件滞欠,我們能夠得到對象的任何時刻狀態(tài)(這里應該用正確術語:聚合aggregate)古胆,比如儲蓄卡每筆記錄的當前余額代表你這個賬戶聚合對象的某刻時刻的狀態(tài),這可能會極大地幫助我們理解領域知識,當前狀態(tài)是怎么來逸绎,因為什么改變惹恃?方便調(diào)試關鍵問題的錯誤
3.領域中當前狀態(tài)和存儲數(shù)據(jù)庫中的數(shù)據(jù)沒有任何耦合,而傳統(tǒng)上我們都是將應用狀態(tài)存儲到數(shù)據(jù)庫中棺牧,比如儲蓄卡當前余額100元存儲到數(shù)據(jù)庫中巫糙,現(xiàn)在我們存儲導致余額的進出事件了,存款了多少錢颊乘,取
款了多少錢参淹,這一筆筆領域事件都會記錄在數(shù)據(jù)庫中。
4.Append-only追加模型存儲這些事件乏悄,易于擴展浙值,這樣我們無論讀寫都有很好地性能,讀取能夠轉(zhuǎn)為查詢優(yōu)化纲爸,也可以轉(zhuǎn)為寫優(yōu)化(因為沒有讀亥鸠,寫得很快),讀寫分離识啦。
5除了可以存儲用戶意圖數(shù)據(jù)负蚊,也就是操作事件,事件存儲順序能夠用來分析用戶正在做什么颓哮,通往大數(shù)據(jù)。
6.我們能避免了對象與關系數(shù)據(jù)庫的不匹配冕茅。
7.審計日志是免費的伤极,一次審計日志所有變化,因為沒有狀態(tài)改變姨伤,只有事件哨坪。
這樣不會浪費時間嗎?
一點也不乍楚。一般來說当编,要執(zhí)行約束,只需要獲得事件的一個很小子集徒溪。通過簡單的數(shù)據(jù)庫查詢就可以獲得有用的歷史事件忿偷,在加載完這些事件后重放它們,把它們“投射”出來臊泌,以此構建你的數(shù)據(jù)集鲤桥。這樣的操作其實是很快的,因為你使用的是本地的處理器渠概,而不是執(zhí)行一系列SQL查詢(跨域網(wǎng)絡的調(diào)用要比本地操作慢得多茶凳,至少會相差兩個數(shù)量等級)。
你可以在后臺構建數(shù)據(jù)集,然后把中間結果保存在數(shù)據(jù)庫里慧妄。這樣顷牌,用戶就可以在很短的時間內(nèi)查詢到這些數(shù)據(jù)。
3塞淹、事件溯源存在的一些問題(缺點)
下面是一些困難:
1.定義事件是一件藝術窟蓝,需要熟悉的領域建模,DDD領域驅(qū)動設計是關鍵饱普。
2.需要軟件和硬件支持事件采購运挫,在以后幾年,你會看到這個領域的很多解決方案套耕。
3.這方面是新生事物谁帕,可指導的經(jīng)驗太少。
4.限制與真正成熟的DDD/ES技能冯袍。
其他帶來的問題還有:
1.需要超級大的存儲消耗匈挖。云存儲解決。
2.比較慢也不是問題康愤,因為我們優(yōu)化優(yōu)化IO來實現(xiàn)快照和持久儡循。并利用基于事件的天然“推”性質(zhì),我們可以得到立即失效緩存征冷。簡而言之择膝,能夠過后有多個插入,需要這種多個的技術解決方案检激。
3.脆弱(丟失失過去的一個事件將導致整個流腐旊茸健)不是一個問題,因為你可以決定自己的SLA水平去(通過復制和冗余)叔收。使用Git的方法齿穗,可以可靠地檢測在任何一個副本的腐敗事件包括SHA1簽名針對它的內(nèi)容和以前的事件簽名計算。
在同步調(diào)用中不太直觀饺律,因為需要首先將請求轉(zhuǎn)換為事件窃页。
無論何時部署重大更新,如果您想要向后兼容(也稱為“事件升級”)蓝晒,你將被迫遷移事件歷史記錄腮出。
某些實現(xiàn)可能需要額外的工作來檢查最新事件的狀態(tài)帖鸦,以確保所有事件都已被處理芝薇。
事件可能包含私有數(shù)據(jù),所以不要忘記確保事件日志得到適當保護作儿。
4洛二、什么時候使用這種模式
這種模式在以下幾種場景中是最理想的解決方案:
- 當你想獲得數(shù)據(jù)的“意圖”,“目的”或者“原因”的時候。 例如晾嘶,一個客戶的實體改變可能用一系列的類似于”搬家“妓雾,”注銷賬戶“或者”死亡“等事件類型。
- 并發(fā)更新數(shù)據(jù)時候非常需要減少或者完全避免沖突的時候垒迂。
- 當你需要保存已經(jīng)發(fā)生的事件械姻,并且能夠重播他們來還原到某個狀態(tài)、使用這些事件去回滾系統(tǒng)的某些變化或者僅僅是歷史或者審查記錄的時候机断。例如 楷拳,當一個任務包括幾個步驟,你可能需要執(zhí)行一個撤銷更新的操作然后重播過去的每個步驟來回到穩(wěn)定的狀態(tài)吏奸。
- 當使用事件是一些應用程序的某些操作的天然屬性欢揖,并且需要很少的額外擴展或者實施的時候。
- 當你需要把插入奋蔚,更新數(shù)據(jù)和需要執(zhí)行這些操作的應用程序解耦開的時候她混。用這種模式可以提高UI的性能,或者把這些事件分發(fā)給其他的監(jiān)聽者泊碑,比如有些應用程序或系統(tǒng)坤按,它們在一些事件發(fā)生的時候必須做出一些反應。例如蛾狗,將一個工資系統(tǒng)和一個報銷系統(tǒng)結合起來晋涣,這樣的話當報銷系統(tǒng)更新一個事件給事件數(shù)據(jù)庫,數(shù)據(jù)庫對此做出的相應事件就可以被報銷系統(tǒng)和工資系統(tǒng)共享沉桌。
- 當要求變更或者——當和CQRS配合使用的時候——你需要適配一個讀的模型或者視圖來顯示數(shù)據(jù)谢鹊,而你想要更靈活地改變物化視圖的格式和實體數(shù)據(jù)的時候。
- 當和CQRS配合使用的時候,并且當一個讀模型被更新時能接受數(shù)據(jù)的最終一致性問題渗常,或者說從一系列的事件序列中生成實體對性能的影響可以被接受衷蜓。
這種模式在以下幾種場景中可能并不適用:
- 小而簡單的,業(yè)務邏輯簡單或者根本沒有業(yè)務邏輯兼耀,或者領域概念的,一般傳統(tǒng)的增刪改查(CURD)就能實現(xiàn)功能的業(yè)務領域求冷,或者系統(tǒng)瘤运。
- 需要實時一致和實時更新數(shù)據(jù)的系統(tǒng)。
- 不需要審查匠题,歷史和回滾的系統(tǒng)拯坟。
- 并發(fā)更新數(shù)據(jù)可能性非常小的系統(tǒng)。例如韭山,只增加數(shù)據(jù)不更新數(shù)據(jù)的系統(tǒng)郁季。
5冷溃、ES 和 CQRS 的關系
CQRS與事件溯源有著相輔相成的關系。CQRS允許事件溯源作為領域的數(shù)據(jù)存儲機制梦裂。然而似枕,使用事件溯源的一個最大的缺點是,你無法向你的系統(tǒng)提出類似“請告訴我所有名字為Greg的用戶”這樣的問題年柠,這是由于事件溯源無法提供對象的當前狀態(tài)而引起的凿歼。CQRS唯一支持的查詢就是:GetById - 通過ID來獲得某個聚合。下圖為基于CQRS/ES的應用系統(tǒng)結構:
CQRS經(jīng)常和事件溯源模式結合使用
基于CQRS的系統(tǒng)使用分離的讀和寫模型冗恨,每一個都對應相應的任務并且一般儲存在不同的數(shù)據(jù)庫中毅往。當和事件溯源模式一起使用的時候,一系列的事件存儲相當于“寫”模型派近,是所有信息的可信賴來源(authoritative source )攀唯。基于CQRS的系統(tǒng)的讀模型提供了數(shù)據(jù)的物化視圖渴丸,經(jīng)常是一種高度格式化的視圖形式侯嘀。這些視圖對應相應的界面并且展示了應用程序的需求,幫助最大化展示和查詢效率谱轨。
使用一系列的事件當作“寫”而不是某一個時間點的數(shù)據(jù)戒幔,避免了更新的沖突并且最大化性能和系統(tǒng)的伸縮性,這些事件可以被異步地產(chǎn)生被用來展示數(shù)據(jù)的物化視圖土童。
因為事件數(shù)據(jù)庫是所有信息的可信賴來源诗茎,當系統(tǒng)改進的時候,有可能刪除物化視圖并且展示所有過去的時間來產(chǎn)生一個新的數(shù)據(jù)献汗,或者當讀模型必須改變的時候敢订。物化視圖是一個長久的數(shù)據(jù)緩存。
當將CQRS和事件溯源模式結合起來的時候罢吃,考慮以下幾點:
- 對于任何的讀寫分離儲存的系統(tǒng)楚午,這些系統(tǒng)基于事件溯源模式都是“最終一致”的。因此在事件產(chǎn)生和數(shù)據(jù)存儲之間會有一些延遲尿招。
- 這種模式會造成一些額外的復雜度矾柜,因為代碼必須要能夠初始化和處理事件,然后組合或者更新相應的讀寫模型需要視圖或者對象就谜。這種復雜度會對讓系統(tǒng)的實現(xiàn)變得有些困難怪蔑,需要重新學習一些概念和一個不同的設計系統(tǒng)的方式。然而事件溯源可以讓為領域建模丧荐,讓重建視圖或者對象更加容易缆瓣。
- 生成物化視圖
CQRS最核心的概念是Command、Event篮奄,“將數(shù)據(jù)(Data)看做是事實(Fact)捆愁。每個事實都是過去的痕跡,雖然這種過去可以遺忘窟却,但卻無法改變昼丑。” 這一思想直接發(fā)展了Event Source夸赫,即將這些事件的發(fā)生過程記錄下來菩帝,使得我們可以追溯業(yè)務流程。CQRS對設計者的影響茬腿,是將領域邏輯呼奢,尤其是業(yè)務流程,皆看做是一種領域?qū)ο鬆顟B(tài)遷移的過程切平。這一點與REST將HTTP應用協(xié)議看做是應用狀態(tài)遷移的引擎握础,有著異曲同工之妙。
1悴品、必須自己實現(xiàn)事務的統(tǒng)一commit和rollback:這個是無論哪一種方式禀综,都必須面對的問題。完全逃不掉苔严。在DDD中有一個叫
Saga
的概念定枷,專門用于統(tǒng)理這種復雜交互業(yè)務的,CQRS/ES架構下届氢,由于本身就是最終一致性欠窒,所以都實現(xiàn)了Saga
,可以使用該機制來做微服務下的transaction治理退子。2岖妄、請求冪等:請求發(fā)送后,由于各種原因寂祥,未能收到正確響應衣吠,而被請求端已經(jīng)正確執(zhí)行了操作。如果這時重發(fā)請求壤靶,則會造成重復操作缚俏。
CQRS/ES架構下通過AggregateRootId、Version贮乳、CommandId三種標識來識別相同command忧换,目前的開源框架都實現(xiàn)了冪等支持。3向拆、并發(fā):單點上亚茬,CQRS/ES中按事件的先來后到嚴格執(zhí)行,內(nèi)存中
Aggregate
的狀態(tài)由單一線程原子操作進行改變浓恳。
多節(jié)點上刹缝,通過EventStore的broker機制碗暗,毫秒級將事件復制到其他節(jié)點,保證同步性梢夯,同時支持版本回退言疗。(Eventuate)
三、在項目中使用ES
大家請注意颂砸,下邊的這一個流程噪奄,就和我們平時開發(fā)的順序是一樣的,比如先建立模型人乓,然后倉儲層勤篮,然后應用服務層,最后是調(diào)用的過程色罚,東西雖然很多碰缔,但是很簡單,慢慢看都能看懂戳护。
同時也復習下我們DDD領域驅(qū)動設計是如何搭建環(huán)境的手负,正好在最后一篇和第一篇遙相呼應。
1姑尺、創(chuàng)建事件存儲模型 StoredEvent : Event
那既然說到了事件溯源竟终,我們就需要首先把事件存儲下來,那存下來之前切蟋,首先要進行建模:
在核心應用層 Christ3D.Domain.Core 的 Events文件夾下统捶,新建 Message.cs 用來獲取我們事件請求的類型:
namespace Christ3D.Domain.Core.Events
{
/// <summary>
/// 抽象類Message,用來獲取我們事件執(zhí)行過程中的類名 /// 然后并且添加聚合根
/// </summary>
public abstract class Message : IRequest
{
public string MessageType { get; protected set; }
public Guid AggregateId { get; protected set; }
protected Message()
{
MessageType = GetType().Name;
}
}
}
同時在該文件夾下柄粹,新建 存儲事件 模型StoredEvent.cs
public class StoredEvent : Event
{
/// <summary>
/// 構造方式實例化 /// </summary>
/// <param name="theEvent"></param>
/// <param name="data"></param>
/// <param name="user"></param>
public StoredEvent(Event theEvent, string data, string user)
{
Id = Guid.NewGuid();
AggregateId = theEvent.AggregateId;
MessageType = theEvent.MessageType;
Data = data;
User = user;
} // 為了EFCore能正確CodeFirst
protected StoredEvent() { } // 事件存儲Id
public Guid Id { get; private set; } // 存儲的數(shù)據(jù)
public string Data { get; private set; } // 用戶信息
public string User { get; private set; }
}
2喘鸟、定義事件存儲上下文 EventStoreSQLContext
定義好了模型,那我們接下來就是要建立數(shù)據(jù)庫上下文了:
1驻右、首先在基礎設施數(shù)據(jù)層 Christ3D.Infrastruct.Data 下的 Mappings文件夾下什黑,建立事件存儲Map模型 StoredEventMap.cs
namespace Christ3D.Infra.Data.Mappings
{
/// <summary>
/// 事件存儲模型Map /// </summary>
public class StoredEventMap : IEntityTypeConfiguration<StoredEvent> {
public void Configure(EntityTypeBuilder<StoredEvent> builder)
{
builder.Property(c => c.Timestamp)
.HasColumnName("CreationDate");
builder.Property(c => c.MessageType)
.HasColumnName("Action")
.HasColumnType("varchar(100)");
}
}
}
2、然后再上下文文件夾 Context 下堪夭,新建事件存儲Sql上下文 EventStoreSQLContext.cs
namespace Christ3D.Infra.Data.Context
{
/// <summary>
/// 事件存儲數(shù)據(jù)庫上下文愕把,繼承 DbContext ///
/// </summary>
public class EventStoreSQLContext : DbContext
{
// 事件存儲模型
public DbSet<StoredEvent> StoredEvent { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new StoredEventMap()); base.OnModelCreating(modelBuilder);
} protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ // 獲取鏈接字符串
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build(); // 使用默認的sql數(shù)據(jù)庫連接
optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection"));
}
}
}
這里要說明下,因為已經(jīng)創(chuàng)建了兩個上下文森爽,以后遷移的時候恨豁,就要加上 **上下文名稱 **了:
3、持久化事件倉儲 EventStoreSQLRepository : IEventStoreRepository
上邊咱們定義了用于持久化事件模型的上下文爬迟,那么現(xiàn)在我們就需要設計倉儲操作類了
1橘蜜、在 基礎設施數(shù)據(jù)層中的 Repository 文件夾下,定義事件存儲倉儲接口 IEventStoreRepository.cs
namespace Christ3D.Infra.Data.Repository.EventSourcing
{
/// <summary>
/// 事件存儲倉儲接口 /// 繼承IDisposable 付呕,可手動回收 /// </summary>
public interface IEventStoreRepository : IDisposable
{
void Store(StoredEvent theEvent);
IList<StoredEvent> All(Guid aggregateId);
}
}
2计福、然后對上邊的接口進行實現(xiàn)
namespace Christ3D.Infra.Data.Repository.EventSourcing
{
/// <summary>
/// 事件倉儲數(shù)據(jù)庫倉儲實現(xiàn)類 /// </summary>
public class EventStoreSQLRepository : IEventStoreRepository
{
// 注入事件存儲數(shù)據(jù)庫上下文
private readonly EventStoreSQLContext _context; public EventStoreSQLRepository(EventStoreSQLContext context)
{
_context = context;
}
/// <summary>
/// 根據(jù)聚合id 獲取全部的事件 /// 這個聚合是指領域模型的聚合根模型 /// </summary>
/// <param name="aggregateId"> 聚合根id 比如:訂單模型id</param>
/// <returns></returns>
public IList<StoredEvent> All(Guid aggregateId)
{
return (from e in _context.StoredEvent where e.AggregateId == aggregateId select e).ToList();
}
/// <summary>
/// 將命令事件持久化 /// </summary>
/// <param name="theEvent"></param>
public void Store(StoredEvent theEvent)
{
_context.StoredEvent.Add(theEvent);
_context.SaveChanges();
} /// <summary>
/// 手動回收 /// </summary>
public void Dispose()
{
_context.Dispose();
}
}
}
這個時候跌捆,我們的事件存儲模型、上下文和倉儲層已經(jīng)建立好了象颖,也就是說我們可以對我們的事件模型進行持久化了佩厚,接下來就是在建立服務了,用來調(diào)用倉儲的服務力麸,就好像我們的應用服務層的概念。
4育韩、建立事件存儲服務 SqlEventStoreService: IEventStoreService
建完了基礎設施層克蚂,那我們接下來就需要建立服務層了,并對其進行調(diào)用:
1筋讨、還是在核心領域?qū)又械腅vents文件夾下埃叭,建立接口
namespace Christ3D.Domain.Core.Events
{
/// <summary>
/// 領域存儲服務接口 /// </summary>
public interface IEventStoreService
{
/// <summary>
/// 將命令模型進行保存 /// </summary>
/// <typeparam name="T"> 泛型:Event命令模型</typeparam>
/// <param name="theEvent"></param>
void Save<T>(T theEvent) where T : Event;
}
}
2、然后再來實現(xiàn)該接口
在應用層 Christ3D.Application 中悉罕,新建 EventSourcing 文件夾赤屋,用來對我們的事件存儲進行溯源,然后新建 事件存儲服務類 SqlEventStoreService.cs
namespace Christ3D.Infra.Data.EventSourcing
{
/// <summary>
/// 事件存儲服務類 /// </summary>
public class SqlEventStoreService : IEventStoreService
{
// 注入我們的倉儲接口
private readonly IEventStoreRepository _eventStoreRepository;
public SqlEventStoreService(IEventStoreRepository eventStoreRepository)
{
_eventStoreRepository = eventStoreRepository;
}
/// <summary>
/// 保存事件模型統(tǒng)一方法 /// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="theEvent"></param>
public void Save<T>(T theEvent) where T : Event
{
// 對事件模型序列化
var serializedData = JsonConvert.SerializeObject(theEvent);
var storedEvent = new StoredEvent(
theEvent,
serializedData, "Laozhang");
_eventStoreRepository.Store(storedEvent);
}
}
}
這個時候你會問了壁袄,那我們現(xiàn)在都寫好了类早,在哪里使用呢,欸嗜逻?涩僻!聰明,既然是事件存儲栈顷,那就是在事件保存的時候逆日,進行存儲,請往下看萄凤。
5室抽、在總線中發(fā)布事件的同時,對事件保存 Task RaiseEvent<T>
在我們的總線實現(xiàn)類 InMemoryBus.cs 下的引發(fā)事件方法中靡努,將我們的事件都保存下來(除了領域通知坪圾,這個錯誤通知不需要保存):
/// <summary>
/// 引發(fā)事件的實現(xiàn)方法 /// </summary>
/// <typeparam name="T">泛型 繼承 Event:INotification</typeparam>
/// <param name="event">事件模型,比如StudentRegisteredEvent</param>
/// <returns></returns>
public Task RaiseEvent<T>(T @event) where T : Event
{ // 除了領域通知以外的事件都保存下來
if (!@event.MessageType.Equals("DomainNotification"))
_eventStoreService?.Save(@event); // MediatR中介者模式中的第二種方法惑朦,發(fā)布/訂閱模式
return _mediator.Publish(@event);
}
四神年、未完待續(xù)...
DDD領域驅(qū)動設計就到這里到一段落了,江湖很遠行嗤,話不多說已日,咱們下一系列再見!
五栅屏、粉絲活動
(活動結束)
這里是三個問題飘千,大家仔細思考
//1堂鲜、聚合根是什么?或者說是什么數(shù)據(jù)結構护奈?(言之成理即可)
//2缔莲、我的項目中,有幾條總線霉旗,分別是痴奏?
//3、我的項目中厌秒,在使用領域通知處理器之前读拆,我是用什么不當?shù)呐R時方法來處理驗證錯誤信息的?(提示:在自定義視圖組件中)