微服務和事件驅動前端
例:在電商業務的下訂單凍結庫存場景。須要根據庫存狀況肯定訂單是否成交。假設你已經採用了分佈式系統,這裏訂單模塊和庫存模塊是兩個服務,分別擁有本身的存儲(關係型數據庫)。數據庫
在一個數據庫的時候,一個事務就能搞定兩張表的修改,可是微服務中,就無法這麼作了。在DDD理念中,一次事務只能改變一個聚合內部的狀態,若是多個聚合之間須要狀態一致,那麼就要經過最終一致性。後端
訂單和庫存明顯是分屬於兩個不一樣的限界上下文的聚合,這裏須要實現最終一致性,就須要使用事件驅動的架構。架構
事件驅動架構在領域對象之間經過異步的消息來同步狀態,有些消息也能夠同時發佈給多個服務,在消息引發了一個服務的同步後可能會引發另外消息,事件會擴散開。嚴格意義上的事件驅動是沒有同步調用的。併發
例子:在訂單服務新增訂單後,訂單的狀態是「已開啓」,而後發佈一個Order Created事件到消息隊列上異步
庫存服務在接收到Order Created 事件後,將庫存表格中的某sku減掉可銷售庫存,增長訂單佔用庫存,而後再發送一個Inventory Locked事件給消息隊列分佈式
訂單服務接收到Inventory Locked事件,將訂單的狀態改成「已確認」微服務
有人問,若是庫存不足,鎖定不成功怎麼辦? 簡單,庫存服務發送一個Lock Fail事件, 訂單服務接收後,把訂單置爲「已取消」。高併發
好消息,咱們能夠不用鎖!事件驅動有個很大的優點就是取消了併發,全部請求都是排隊進來,這對咱們實施充血模型有很大幫助,咱們能夠不須要本身來管理內存中的鎖了。取消鎖,隊列處理效率很高,事件驅動能夠用在高併發場景下,好比搶購。工具
是的,用戶體驗有改變,用了這個事件驅動,用戶的體驗有可能會有改變,好比原來同步架構的時候沒有庫存,就立刻告訴你條件不知足沒法下單,不會生成訂單;可是改了事件機制,訂單是當即生成的,極可能過了一會系統通知你訂單被取消掉。 就像搶購「小米手機」同樣,幾十萬人在排隊,排了好久告訴你沒貨了,明天再來吧。若是但願用戶當即獲得結果,能夠在前端想辦法,在BFF(Backend For Frontend)使用CountDownLatch這樣的鎖把後端的異步轉成前端同步,固然這樣BFF消耗比較大。
沒辦法,產品經理不接受,產品經理說用戶的體驗必須是沒有庫存就不會生成訂單,這個方案會不斷的生成取消的訂單,他不能接受,怎麼辦?那就在訂單列表查詢的時候,略過這些「已取消」狀態的訂單吧,也許須要一個額外的視圖來作。我並非一個理想主義者,解決當前的問題是我首先要考慮的,咱們設計微服務的目的是本想是解決業務併發量。而如今面臨的倒是用戶體驗的問題,因此架構設計也是須要妥協的:( 可是至少分析完了,我知道我妥協在什麼地方,爲何妥協,將來還有可能改變。
我我的認爲聚合根這樣的模式對修改狀態是特別合適,可是對搜索數據的確是不方便,好比篩選出一批符合條件的訂單這樣的需求,自己聚合根對象不能承擔批量的查詢任務,由於這不是他的職責。那就必須依賴「領域服務(Domain Service)」這種設施。
當一個方法不便放在實體或者值對象上,使用領域服務即是最佳的解決方法,請確保領域服務是無狀態的。
咱們的查詢任務每每很複雜,好比查詢商品列表,要求按照上個月的銷售額進行排序; 要按照商品的退貨率排序等等。可是在微服務和DDD以後,咱們的存儲模型已經被拆離開,上述的查詢都是要涉及訂單、用戶、商品多個領域的數據。如何搞? 此時咱們要引入一個視圖的概念。好比下面的,查詢用戶名下訂單的操做,直接調用兩個服務本身在內存中join效率無疑是很低的,再加上一些filter條件、分頁,無法作了。因而咱們將事件廣播出去,由一個單獨的視圖服務來接收這些事件,並造成一個物化視圖(materialized view),這些數據已經join過,處理過,放在一個單獨的查詢庫中,等待查詢,這是一個典型的以空間換時間的處理方式。
限界上下文(Bounded Context)和數據耦合
除了多領域join的問題,咱們在業務中還會常常碰到一些場景,好比電商中的商品信息是基礎信息,屬於單獨的BC,而其餘BC,無論是營銷服務、價格服務、購物車服務、訂單服務都是須要引用這個商品信息的。可是須要的商品信息只是所有的一小部分而已,營銷服務須要商品的id和名稱、上下架狀態;訂單服務須要商品id、名稱、目錄、價格等等。這比起商品中心定義一個商品(商品id、名稱、規格、規格值、詳情等等)只是一個很小的子集。這說明不一樣的限界上下文的一樣的術語,可是所指的概念不同。 這樣的問題映射到咱們的實現中,每次在訂單、營銷模塊中直接查詢商品模塊,確定是不合適,由於
商品中心須要適配每一個服務須要的數據,提供不一樣的接口
併發量必然很大
服務之間的耦合嚴重,一旦宕機、升級影響的範圍很大。特別是最後一條,嚴重限制了咱們得到微服務提供的優點「鬆耦合、每一個服務本身能夠頻繁升級不影響其餘模塊」。這就須要咱們經過事件驅動方法,適當冗餘一些數據到不一樣的BC去,把這種耦合拆解開。這種耦合有時候是經過Value Object嵌入到實體中的方式,在生成實體的時候就冗餘,好比訂單在生成的時候就冗餘了商品的信息;有時候是經過額外的Value Object列表方式,營銷中心冗餘一部分相關的商品列表數據,並隨時關注監聽商品的上下級狀態,同步替換掉本限界上下文的商品列表。
下圖一個下單場景分析,在電商系統中,咱們能夠認爲會員和商品是全部業務的基礎數據,他們的變動應該是經過廣播的方式發佈到各個領域,每一個領域保留本身須要的信息。
最終一致性成功依賴不少條件
依賴消息傳遞的可靠性,可能A系統變動了狀態,消息發到B系統的時候丟失了,致使AB的狀態不一致
依賴服務的可靠性,若是A系統變動了本身的狀態,可是還沒來得及發送消息就掛了。也會致使狀態不一致我記得JavaEE規範中的JMS中有針對這兩種問題的處理要求,一個是JMS經過各類確認消息(Client Acknowledge等)來保證消息的投遞可靠性,另外是JMS的消息投遞操做能夠加入到數據庫的事務中-即沒有發送消息,會引發數據庫的回滾(沒有查資料,不是很準確的描述,請專家指正)。不過如今符合JMS規範的MQ沒幾個,特別是保一致性須要下降性能,如今標榜高吞吐量的MQ都把問題拋給了咱們本身的應用解決。因此這裏介紹幾個常見的方法,來提高最終一致性的效果。
仍是以上面的訂單扣取信用的例子
訂單服務開啓本地事務,首先新增訂單;
而後將Order Created事件插入一張專門Event表,事務提交;
有一個單獨的定時任務線程,按期掃描Event表,掃出來須要發送的就丟到MQ,同時把Event設置爲「已發送」。
方案的優點是使用了本地數據庫的事務,若是Event沒有插入成功,那麼訂單也不會被建立;線程掃描後把event置爲已發送,也確保了消息不會被漏發(咱們的目標是寧肯重發,也不要漏發,由於Event處理會被設計爲冪等)。缺點是須要單獨處理Event發佈在業務邏輯中,繁瑣容易忘記;Event發送有些滯後;定時掃描性能消耗大,並且會產生數據庫高水位隱患;
咱們稍做改進,使用數據庫特有的MySQL Binlog跟蹤(阿里的Canal)或者Oracle的GoldenGate技術能夠得到數據庫的Event表的變動通知,這樣就能夠避免經過定時任務來掃描了
不過用了這些數據庫日誌的工具,會和具體的數據庫實現(甚至是特定的版本)綁定,決策的時候請慎重。
事件溯源對咱們來講是一個特別的思路,他並不持久化Entity對象,而是隻把初始狀態和每次變動的Event記錄下來,並在內存中根據Event還原Entity對象的最新狀態,具體實現很相似數據庫的Redolog的實現,只是他把這種機制放到了應用層來。雖然事件溯源有不少宣稱的優點,引入這種技術要特別當心,首先他不必定適合大部分的業務場景,一旦變動不少的狀況下,效率的確是個大問題;另一些查詢的問題也是困擾。咱們僅僅在個別的業務上探索性的使用Event Souring和AxonFramework,因爲實現起來比較複雜,具體的狀況還須要等到實踐一段時間後再來總結,也許須要額外的一篇文章來詳細描述。