哈嘍~你們好,時間過的真快,關於DDD領域驅動設計的講解基本就差很少了,原本想着週四再開一篇,感受沒有太多的內容了,剩下的一個就是驗證的問題,就和以前的JWT很相似,就不打開一個章節了,並且這個也不是領域驅動設計範疇以內的,下一個系列 Ids 的講解中,可能會穿插着講一講,而後到時候正好一塊兒完善了。html
雖然是完結了,不過內心仍是不是很開森呀,經過小夥伴的反饋,而後我也諮詢了官方的建議,好像這個DDD領域驅動設計系列,並無獲得不少的支持,影響力徹底比不過第一個系列《從壹開始先後端分離》,緣由多是,我也沒有在項目中真正的使用過DDD的緣由吧,也或許是寫的比較生硬,主要我也一直在研究,不過我這裏必定要說一下,仍是要多看看的,不必定要看我講的,能夠看看書也行,或者看看別人的博客,DDD領域驅動設計思想真的很不錯,而後還夾帶着CQRS命令查詢職責分離、Bus總線思想、EDA事件驅動思想、ES事件溯源思想(今天要說到的)、git
舉個溯源的例子(我瞎想的):github
可能不是很恰當,也可能他們根本不是這麼作的,我只是用這個感受來講明什麼是溯源:sql
你們在玩兒消消樂的時候,偶爾會遇到這個狀況,好比玩兒了二十步的時候,忽然閃退了,而後咱們從新進去,從新進這一關的時候,會看到系統快速的把咱們的這二十步進行了還原,有一個過程步驟,也許沒人注意,那我想問下,這個是怎麼保存的呢?難道直接獲取的當前關卡的狀態麼,有一絲絲的溯源的意味。數據庫
你們本身思考其餘的例子,好比銀行查帳,天天的數據彙總等等。編程
消息隊列等等這些之前沒有接觸到的思想設計,也爲微服務打下了必定的基礎,若是沒有這些基礎,你是很難理解爲何要使用微服務的,這裏咱們就先來回顧一下這些天咱們都說了什麼內容吧:json
FluentValidator
算上今天的內容,正好是十二篇,也是個人比較喜歡的一個數字(以前在文章中說到過這個緣由,這裏就很少說了),也是很辛苦寫了這麼多,但願有時間有精力的時候,仍是要多看看的,多品品思想,這樣咱們就不會一直問一些虛無縹緲的問題了,雖然我如今是越學的多,越不會的多😂。後端
可能你也發現了今天的題目有些不同,由於我以前說過,要在聖誕節簡單搞個小活動,既然說了,就不能食言,不過目前我寫了十六萬字了,就一個小夥伴給了我一塊錢紅包😂,好失敗,因此我就簡單來個小福利吧,由於這個系列的名字就是Christ3D,當時就是想着在聖誕節前能說完,還能夠,緊趕慢趕的說完了,我就想着一個給粉絲一個小小小福利:跨域
具體的參與形式看文章末尾:(已結束)緩存
一、免費給送三本書,多是《實現領域驅動設計》這本書,或者《領域驅動設計 軟件核心複雜性應對之道》,還有我本人的簽名+賀卡喲哈哈;
二、原本想抽十位粉絲,送精裝的聖誕節蘋果,可是考慮食品安全問題就算了,直接到時候發紅包吧(時間地點保密,提示:爲了老粉絲);
言歸正傳,今天的重點仍是要好好的說說新知識——事件溯(su)源,Event Source,也有人翻譯事件採購,或者是事件回溯,或者直接就是ES,其實都是一個意思,要是下次你發現這幾個詞語的時候,都是指的事件溯源,其實事件溯源已經有一隻腳邁進了微服務的你們族了,甚至能夠說已經在微服務的一員了,他配合着事件總線EventBus、消息隊列等,在微服務的工做中起着必定的做用。固然今天只是簡單的入門講解,要是想打開真正的微服務的大門,就須要你們本身去探索,固然,我也會繼續跟進這個講解,下一個系列 Ids ,其實也是微服務的一個分支,慢慢來,但願你們多捧場啦!
立刻開始今天的講解,仍是一天一問吧,但願你們帶着這個問題通讀本文,本身能想到合適的答案:
一、你認爲事件存儲 EventStore 和 日誌記錄的區別是什麼?
這裏要給你們再強調兩點:
一、CQRS、EDA和ES這些其實已經不在DDD設計的範圍以內,只不過這些技術都是一塊兒使用的,多個技術的相互結合使用,才能發揮很大的做用,因此說本系列教程是
DDD+CQRS+EDA+ES的結合體,之後被別人問到的時候,可別說事件溯源就是領域驅動設計的一部分喲。
二、事件溯源不是一兩句能說清的,這篇文章只是一個啓蒙的做用,等你們從事微服務工做的時候,就知道它深層次的意義了,切不可和平時的 CURD 項目生搬硬套作比較。
(我寫的十二篇文章中的知識點,這裏基本都有了,也算是一個圓滿了,集齊七顆啦😀)
事件溯源其實很好理解,首先從字面上的理解:
事件就是 Event,溯是一個動詞,能夠理解爲追溯,回溯的意思,源表明原始、源頭的意思,合起來表示一個事件追蹤源的過程。
這個時候你仔細想想,咱們在和領域專家(默認他們不懂技術)討論用戶下單流程的時候,專家必定會說:客戶首先選擇一個商品,而後添加到購物車,確認無誤下單,接着用戶支付,支付成功後,就給用戶發貨。而咱們呢,咱們做爲一個開發人員,和領域專家討論的時候,天然而然的也是這麼思考的,對不對!(你確定在討論需求的時候用的不是數據庫的思惟!),只不過咱們後期開發的時候,拘泥於技術和數據優先的思惟,不得不轉向CURD的道路了,固然這個沒有什麼錯誤,我只是說明一點,事件存儲真的離咱們不遠。
那咱們平時是怎麼作的呢,這裏說一個特別簡單的:
從這個特別簡單的流程中咱們能夠看到,平時咱們都是直接操做的 Order 這個領域聚合根,一直在修改模型狀態,這個看似正常的操做下,有一些問題,是咱們創建在每一步都正常執行的狀況下,不過通常總會出現一些問題,特別是分佈式的環境中。
然而,事件溯源與上述的狀況剛好相反,它並不關心當前狀態,而是關注持續不斷的變化事件。
舉個例子,假設咱們有一個「購物車」,咱們能夠建立購物車,往裏面添加商品或移除商品,而後結帳。
購物車的生命週期能夠包含以下一系列事件:
建立購物車
往購物車裏添加商品
再次往購物車裏添加商品
從購物車裏移除商品
結帳
這些就是一個購物車的生命週期,包含了一系列事件。這就是事件溯源,很是簡單吧?
幾乎全部的流程均可以被當作一系列事件。在與領域專家交談時,他們不會說起「表」和「鏈接」,他們會將流程描述成一系列事件以及能夠應用在這些事件上的規則。
事件溯源不是萬能的,不過它能夠在某一些領域發揮很大的做用,這個在之後的微服務設計中,會更能體現出來,那咱們就簡單說兩點:
傳統的應用中,數據庫裏存的是Domain Model的實例的當前狀態,好比某個儲戶銀行帳戶的存款數,一般是一個數字.若是考慮到以下的三個情形,咱們可能付出的代價比較大:
1) 老規則:問題跟蹤
若是某個儲戶的帳戶出現問題,那麼咱們只有從大到PB的日誌中去分析用戶的帳戶數據是如何出錯的,並且咱們在作日誌的時候,不可能全部的都考慮到,就算是把所有數據都保存,時間都記下來,操做者都備份,那ATM機信息呢?(可能不恰當,只是說明咱們總有想不到的地方),但若是一旦日誌不夠詳細,找出問題根源基本只能靠猜了。
2) 新需求:趨勢分析
歷史數據的做用在於分析將來的趨勢,若是僅僅從浩如煙海的日誌中尋找規律,咱們還得單獨寫邏輯,對日誌進行建模,清洗,其實咱們已經能接受,日誌就是用來記錄異常信息的,這個時候咱們就很崩潰了。
3) 更奇葩:事務回滾
在介紹事務修正模式中,咱們講到某個步驟發生錯誤,以前的各個節點能夠本身獨立地完成回滾,回滾的依據就是記錄的操做步驟及相關參數,根據這些有用信息就能夠每一個節點自行回滾到原始狀態,而且在失敗的時候能夠retry
可見存儲對於Domain Model 的各個事件仍是很是有用的,尤爲是對於複雜的系統,這也就是咱們今天要討論的事件溯源模式.
大多數的應用都和數據打交道,最多見的打交道方式就是將用戶在使用過程當中的數據最終狀態同步到數據庫中。例如,在傳統的增刪改查(CURD)模式中,一個典型的數據過程就是從數據庫中讀出數據,修改完後再把修改後的數據更新到數據庫中——一般來講,在這個更新過程這張數據表是被鎖住的。
這種傳統的增刪改查(CURD)方式存在一些侷限性:
這是一個大問題。在以表做爲驅動的系統裏,你只保存了系統的當前狀態,你根本就沒法知道系統是如何達到當前狀態的。若是我問你「這個用戶修改了幾回郵件地址」,你有辦法回答嗎?或者我再問「有多少人把一件商品添加到購物車裏,而後又移除掉,直到一個月以後纔買了那件商品」,你就更無法回答了。你存儲數據的方式丟掉了不少有用的業務信息!
儘管它是一個簡單的模式,但使用它有不少優勢:
事件日誌具備很高的商業價值;
它在DDD和事件驅動架構下運行得很是好。
調試用應用程序狀態中全部變動的來源;
它容許您重放失敗的事件;
易於調試,您能夠將目標實體的全部事件複製到您的機器並調試每一個事件,以瞭解應用程序如何達到特定狀態(忽略從生產環境複製數據的安全隱患);
容許您使用追溯事件模式重建/修復您的狀態。
許多做者還將優先級做爲時間查詢的能力,但我認爲查詢多個後續事件不是一項簡單的任務。所以,我一般認爲時間查詢是快照模式的一個優勢。
有許多理由使用Event Sourcing,當你瀏覽Greg Young的系列文章和談話你會發現下面要點:
1. 它不是一個新概念,真實世界中許多領域都很像它,看看你的銀行帳戶狀態,好比儲蓄卡,它打印出一筆筆進出明細和當前餘額,這一筆筆表明了領域事件。
2.經過重播事件,咱們可以獲得對象的任什麼時候刻狀態(這裏應該用正確術語:聚合aggregate),好比儲蓄卡每筆記錄的當前餘額表明你這個帳戶聚合對象的某刻時刻的狀態,這可能會極大地幫助咱們理解領域知識,當前狀態是怎麼來,由於什麼改變?方便調試關鍵問題的錯誤
3.領域中當前狀態和存儲數據庫中的數據沒有任何耦合,而傳統上咱們都是將應用狀態存儲到數據庫中,好比儲蓄卡當前餘額100元存儲到數據庫中,如今咱們存儲致使餘額的進出事件了,存款了多少錢,取
款了多少錢,這一筆筆領域事件都會記錄在數據庫中。
4.Append-only追加模型存儲這些事件,易於擴展,這樣咱們不管讀寫都有很好地性能,讀取可以轉爲查詢優化,也能夠轉爲寫優化(由於沒有讀,寫得很快),讀寫分離。
5除了能夠存儲用戶意圖數據,也就是操做事件,事件存儲順序可以用來分析用戶正在作什麼,通往大數據。
6.咱們能避免了對象與關係數據庫的不匹配。
7.審計日誌是免費的,一次審計日誌全部變化,由於沒有狀態改變,只有事件。
這樣不會浪費時間嗎?
一點也不。通常來講,要執行約束,只須要得到事件的一個很小子集。經過簡單的數據庫查詢就能夠得到有用的歷史事件,在加載完這些事件後重放它們,把它們「投射」出來,以此構建你的數據集。這樣的操做實際上是很快的,由於你使用的是本地的處理器,而不是執行一系列SQL查詢(跨域網絡的調用要比本地操做慢得多,至少會相差兩個數量等級)。
你能夠在後臺構建數據集,而後把中間結果保存在數據庫裏。這樣,用戶就能夠在很短的時間內查詢到這些數據。
下面是一些困難:
1.定義事件是一件藝術,須要熟悉的領域建模,DDD領域驅動設計是關鍵。
2.須要軟件和硬件支持事件採購,在之後幾年,你會看到這個領域的不少解決方案。
3.這方面是新生事物,可指導的經驗太少。
4.限制與真正成熟的DDD/ES技能。
其餘帶來的問題還有:
1.須要超級大的存儲消耗。雲存儲解決。
2.比較慢也不是問題,由於咱們優化優化IO來實現快照和持久。並利用基於事件的自然「推」性質,咱們能夠獲得當即失效緩存。簡而言之,可以事後有多個插入,須要這種多個的技術解決方案。
3.脆弱(丟失失過去的一個事件將致使整個流腐敗)不是一個問題,由於你能夠決定本身的SLA水平去(經過複製和冗餘)。使用Git的方法,能夠可靠地檢測在任何一個副本的腐敗事件包括SHA1簽名針對它的內容和之前的事件簽名計算。
在同步調用中不太直觀,由於須要首先將請求轉換爲事件。
不管什麼時候部署重大更新,若是您想要向後兼容(也稱爲「事件升級」),你將被迫遷移事件歷史記錄。
某些實現可能須要額外的工做來檢查最新事件的狀態,以確保全部事件都已被處理。
事件可能包含私有數據,因此不要忘記確保事件日誌獲得適當保護。
這種模式在如下幾種場景中是最理想的解決方案:
這種模式在如下幾種場景中可能並不適用:
CQRS與事件溯源有着相輔相成的關係。CQRS容許事件溯源做爲領域的數據存儲機制。然而,使用事件溯源的一個最大的缺點是,你沒法向你的系統提出相似「請告訴我全部名字爲Greg的用戶」這樣的問題,這是因爲事件溯源沒法提供對象的當前狀態而引發的。CQRS惟一支持的查詢就是:GetById - 經過ID來得到某個聚合。下圖爲基於CQRS/ES的應用系統結構:
CQRS常常和事件溯源模式結合使用
基於CQRS的系統使用分離的讀和寫模型,每個都對應相應的任務而且通常儲存在不一樣的數據庫中。當和事件溯源模式一塊兒使用的時候,一系列的事件存儲至關於「寫」模型,是全部信息的可信賴來源(authoritative source )。基於CQRS的系統的讀模型提供了數據的物化視圖,常常是一種高度格式化的視圖形式。這些視圖對應相應的界面而且展現了應用程序的需求,幫助最大化展現和查詢效率。
使用一系列的事件看成「寫」而不是某一個時間點的數據,避免了更新的衝突而且最大化性能和系統的伸縮性,這些事件能夠被異步地產生被用來展現數據的物化視圖。
由於事件數據庫是全部信息的可信賴來源,當系統改進的時候,有可能刪除物化視圖而且展現全部過去的時間來產生一個新的數據,或者當讀模型必須改變的時候。物化視圖是一個長久的數據緩存。
當將CQRS和事件溯源模式結合起來的時候,考慮如下幾點:
CQRS最核心的概念是Command、Event,「將數據(Data)看作是事實(Fact)。每一個事實都是過去的痕跡,雖然這種過去能夠遺忘,但卻沒法改變。」 這一思想直接發展了Event Source,即將這些事件的發生過程記錄下來,使得咱們能夠追溯業務流程。CQRS對設計者的影響,是將領域邏輯,尤爲是業務流程,皆看作是一種領域對象狀態遷移的過程。這一點與REST將HTTP應用協議看作是應用狀態遷移的引擎,有着殊途同歸之妙。
一、必須本身實現事務的統一commit和rollback:這個是不管哪種方式,都必須面對的問題。徹底逃不掉。在DDD中有一個叫
Saga
的概念,專門用於統理這種複雜交互業務的,CQRS/ES架構下,因爲自己就是最終一致性,因此都實現了Saga
,可使用該機制來作微服務下的transaction治理。
二、請求冪等:請求發送後,因爲各類緣由,未能收到正確響應,而被請求端已經正確執行了操做。若是這時重發請求,則會形成重複操做。
CQRS/ES架構下經過AggregateRootId、Version、CommandId三種標識來識別相同command,目前的開源框架都實現了冪等支持。
三、併發:單點上,CQRS/ES中按事件的先來後到嚴格執行,內存中
Aggregate
的狀態由單一線程原子操做進行改變。
多節點上,經過EventStore的broker機制,毫秒級將事件複製到其餘節點,保證同步性,同時支持版本回退。(Eventuate)
你們請注意,下邊的這一個流程,就和咱們平時開發的順序是同樣的,好比先創建模型,而後倉儲層,而後應用服務層,最後是調用的過程,東西雖然不少,可是很簡單,慢慢看都能看懂。
同時也複習下咱們DDD領域驅動設計是如何搭建環境的,正好在最後一篇和第一篇遙相呼應。
namespace Christ3D.Domain.Core.Events { /// <summary> /// 抽象類Message,用來獲取咱們事件執行過程當中的類名 /// 而後而且添加聚合根 /// </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; } // 存儲的數據 public string Data { get; private set; } // 用戶信息 public string User { get; private set; } }
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)"); } } }
二、而後再上下文文件夾 Context 下,新建事件存儲Sql上下文 EventStoreSQLContext.cs
namespace Christ3D.Infra.Data.Context { /// <summary> /// 事件存儲數據庫上下文,繼承 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數據庫鏈接 optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection")); } } }
這裏要說明下,由於已經建立了兩個上下文,之後遷移的時候,就要加上 上下文名稱 了:
namespace Christ3D.Infra.Data.Repository.EventSourcing { /// <summary> /// 事件存儲倉儲接口 /// 繼承IDisposable ,可手動回收 /// </summary> public interface IEventStoreRepository : IDisposable { void Store(StoredEvent theEvent); IList<StoredEvent> All(Guid aggregateId); } }
二、而後對上邊的接口進行實現
namespace Christ3D.Infra.Data.Repository.EventSourcing { /// <summary> /// 事件倉儲數據庫倉儲實現類 /// </summary> public class EventStoreSQLRepository : IEventStoreRepository { // 注入事件存儲數據庫上下文 private readonly EventStoreSQLContext _context; public EventStoreSQLRepository(EventStoreSQLContext context) { _context = context; } /// <summary> /// 根據聚合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(); } } }
這個時候,咱們的事件存儲模型、上下文和倉儲層已經創建好了,也就是說咱們能夠對咱們的事件模型進行持久化了,接下來就是在創建服務了,用來調用倉儲的服務,就好像咱們的應用服務層的概念。
建完了基礎設施層,那咱們接下來就須要創建服務層了,並對其進行調用:
一、仍是在覈心領域層中的Events文件夾下,創建接口
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; } }
二、而後再來實現該接口
在應用層 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> /// 保存事件模型統一方法 /// </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); } } }
這個時候你會問了,那咱們如今都寫好了,在哪裏使用呢,欸?!聰明,既然是事件存儲,那就是在事件保存的時候,進行存儲,請往下看。
/// <summary> /// 引起事件的實現方法 /// </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中介者模式中的第二種方法,發佈/訂閱模式 return _mediator.Publish(@event); }
DDD領域驅動設計就到這裏到一段落了,江湖很遠,話很少說,我們下一系列再見!
//一、聚合根是什麼?或者說是什麼數據結構?(言之成理便可) //二、個人項目中,有幾條總線,分別是? //三、個人項目中,在使用領域通知處理器以前,我是用什麼不當的臨時方法來處理驗證錯誤信息的?(提示:在自定義視圖組件中)