接上篇,咱們採用了領域驅動的開發方式,使用了充血模型,享受了他的好處,可是也不得不面對他帶來的弊端。這個弊端在分佈式的微服務架構下面又被放大。前端
事務一致性程序員
事務一致性的問題在Monolithic下面不是大問題,在微服務下面倒是很致命,咱們回顧一下所謂的ACID原則數據庫
Atomicity – 原子性,改變數據狀態要麼是一塊兒完成,要麼一塊兒失敗json
Consistency – 一致性,數據的狀態是完整一致的後端
Isolation – 隔離線,即便有併發事務,互相之間也不影響架構
Durability – 持久性, 一旦事務提交,不可撤銷併發
在單體服務和關係型數據庫的時候,咱們很容易經過數據庫的特性去完成ACID。可是一旦你按照DDD拆分聚合根-微服務架構,他們的數據庫就已經分離開了,你就要獨立面對分佈式事務,要在本身的代碼裏面知足ACID。框架
對於分佈式事務,你們通常會想到之前的JTA標準,2PC兩段式提交。我記得當年在Dubbo羣裏面,基本每週都會有人詢問Dubbo啥時候支撐分佈式事務。實際上根據分佈式系統中CAP原則,當P(分區容忍)發生的時候,強行追求C(一致性),會致使(A)可用性、吞吐量降低,此時咱們通常用最終一致性來保證咱們系統的AP能力。固然不是說放棄C,而是在通常狀況下CAP都能保證,在發生分區的狀況下,咱們能夠經過最終一致性來保證數據一致。異步
例:分佈式
在電商業務的下訂單凍結庫存場景。須要根據庫存狀況肯定訂單是否成交。
假設你已經採用了分佈式系統,這裏訂單模塊和庫存模塊是兩個服務,分別擁有本身的存儲(關係型數據庫),
在一個數據庫的時候,一個事務就能搞定兩張表的修改,可是微服務中,就無法這麼作了。
在DDD理念中,一次事務只能改變一個聚合內部的狀態,若是多個聚合之間須要狀態一致,那麼就要經過最終一致性。訂單和庫存明顯是分屬於兩個不一樣的限界上下文的聚合,這裏須要實現最終一致性,就須要使用事件驅動的架構。
事件驅動實現最終一致性
事件驅動架構在領域對象之間經過異步的消息來同步狀態,有些消息也能夠同時發佈給多個服務,在消息引發了一個服務的同步後可能會引發另外消息,事件會擴散開。嚴格意義上的事件驅動是沒有同步調用的。
例子:
在訂單服務新增訂單後,訂單的狀態是「已開啓」,而後發佈一個Order Created事件到消息隊列上
庫存服務在接收到Order Created 事件後,將庫存表格中的某sku減掉可銷售庫存,增長訂單佔用庫存,而後再發送一個Inventory Locked事件給消息隊列
訂單服務接收到Inventory Locked事件,將訂單的狀態改成「已確認」
有人問,若是庫存不足,鎖定不成功怎麼辦? 簡單,庫存服務發送一個Lock Fail事件, 訂單服務接收後,把訂單置爲「已取消」。
好消息,咱們能夠不用鎖!事件驅動有個很大的優點就是取消了併發,全部請求都是排隊進來,這對咱們實施充血模型有很大幫助,咱們能夠不須要本身來管理內存中的鎖了。取消鎖,隊列處理效率很高,事件驅動能夠用在高併發場景下,好比搶購。
是的,用戶體驗有改變,用了這個事件驅動,用戶的體驗有可能會有改變,好比原來同步架構的時候沒有庫存,就立刻告訴你條件不知足沒法下單,不會生成訂單;可是改了事件機制,訂單是當即生成的,極可能過了一會系統通知你訂單被取消掉。 就像搶購「小米手機」同樣,幾十萬人在排隊,排了好久告訴你沒貨了,明天再來吧。若是但願用戶當即獲得結果,能夠在前端想辦法,在BFF(Backend For Frontend)使用CountDownLatch這樣的鎖把後端的異步轉成前端同步,固然這樣BFF消耗比較大。
沒辦法,產品經理不接受,產品經理說用戶的體驗必須是沒有庫存就不會生成訂單,這個方案會不斷的生成取消的訂單,他不能接受,怎麼辦?那就在訂單列表查詢的時候,略過這些cancel狀態的訂單吧,也許須要一個額外的視圖來作。我並非一個理想主義者,解決當前的問題是我首先要考慮的,咱們設計微服務的目的是本想是解決業務併發量。而如今面臨的倒是用戶體驗的問題,因此架構設計也是須要妥協的:( 可是至少分析完了,我知道我妥協在什麼地方,爲何妥協,將來還有可能改變。
針對上面的技術我特地整理了一下,有不少技術不是靠幾句話能講清楚,因此乾脆找朋友錄製了一些視頻,不少問題其實答案很簡單,可是背後的思考和邏輯不簡單,要作到知其然還要知其因此然。若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java進階羣:582505643,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們
多個領域多表Join查詢
我我的認爲聚合根這樣的模式對修改狀態是特別合適,可是對搜索數據的確是不方便,好比篩選出一批符合條件的訂單這樣的需求,自己聚合根對象不能承擔批量的查詢任務,由於這不是他的職責。那就必須依賴「領域服務(Domain Service)」這種設施。
當一個方法不便放在實體或者值對象上,使用領域服務即是最佳的解決方法,請確保領域服務是無狀態的。
咱們的查詢任務每每很複雜,好比查詢商品列表,要求按照上個月的銷售額進行排序; 要按照商品的退貨率排序等等。可是在微服務和DDD以後,咱們的存儲模型已經被拆離開,上述的查詢都是要涉及訂單、用戶、商品多個領域的數據。如何搞? 此時咱們要引入一個視圖的概念。好比下面的,查詢用戶名下訂單的操做,直接調用兩個服務本身在內存中join效率無疑是很低的,再加上一些filter條件、分頁,無法作了。因而咱們將事件廣播出去,由一個單獨的視圖服務來接收這些事件,並造成一個物化視圖(materialized view),這些數據已經join過,處理過,放在一個單獨的查詢庫中,等待查詢,這是一個典型的以空間換時間的處理方式。
通過分析,除了簡單的根據主鍵Find或者沒有太多關聯的List查詢,咱們大部分的查詢任務能夠放到單獨的查詢庫中,這個查詢庫能夠是關係數據庫的ReadOnly庫,也能夠是NoSQL的數據庫,實際上咱們在項目中使用了ElasticSearch做爲專門的查詢視圖,效果很不錯
限界上下文(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處理會被設計爲冪等)。
針對上面的技術我特地整理了一下,有不少技術不是靠幾句話能講清楚,因此乾脆找朋友錄製了一些視頻,不少問題其實答案很簡單,可是背後的思考和邏輯不簡單,要作到知其然還要知其因此然。若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java進階羣:582505643,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們
缺點是須要單獨處理Event發佈在業務邏輯中,繁瑣容易忘記;Event發送有些滯後;定時掃描性能消耗大,並且會產生數據庫高水位隱患;
咱們稍做改進,使用數據庫特有的MySQL Binlog跟蹤(阿里的Canal)或者Oracle的GoldenGate技術能夠得到數據庫的Event表的變動通知,這樣就能夠避免經過定時任務來掃描了
不過用了這些數據庫日誌的工具,會和具體的數據庫實現(甚至是特定的版本)綁定,決策的時候請慎重。
使用Event Sourcing 事件溯源
事件溯源對咱們來講是一個特別的思路,他並不持久化Entity對象,而是隻把初始狀態和每次變動的Event記錄下來,並在內存中根據Event還原Entity對象的最新狀態,具體實現很相似數據庫的Redolog的實現,只是他把這種機制放到了應用層來。
雖然事件溯源有不少宣稱的優點,引入這種技術要特別當心,首先他不必定適合大部分的業務場景,一旦變動不少的狀況下,效率的確是個大問題;另一些查詢的問題也是困擾。
我認可我是標題黨, 爲何要寫這篇充滿爭議的文章?目前架構師這個職位特別火熱,程序員的目標都是成爲一個使人尊敬的架構師。可是咱們真的理解架構師應該作些什麼?不少人把架構師和框架師等同起來,認爲研究框架多的纔是架構師
下面說的狀況請勿對號入座。
盲目的追新:
技術人員的喜愛每每是什麼技術流行就追什麼技術。如今的技術發展快,先後端不斷涌現各類框架,咱們巴不得把這些框架都用在本身的項目裏才行,要否則怎麼好意思和別人打招呼啊。
我親身經歷,有個技術人員必定要把原來單元測試框架的xml初始數據改成json,他的原話是」json看的更舒服」,可是改完後,咱們的單元測試反而難落地了,緣由是原來的單元測試框架有個工具是能夠將表中的數據自動生成xml的,而改爲json後,咱們必須手寫json數據了。 他的喜愛不包括給你們更好用的工具。
按技術站隊,以結果反推:
不少人把手段當成了目的,成爲了框架的信徒。用了Java開發,你的設計就必定是面向對象的?用了Spring boot就是微服務了嗎?這些荒唐的事情卻在技術圈不斷髮生,技術人員甚至會按照語言、框架造成不一樣的圈子,各類技術圈互相鄙視,互相踩,真相此時沒法越辯越明,反而把技術方向帶歪了。
技術要和實際場景結合
架構師也要深刻了解掌握技術,可是更多的是瞭解技術的優劣和使用場景,而不是簡單的生搬硬套。以如今流行的微服務架構來講,Netflix使用RESTful接口做爲通信,咱們是否是要把公司的用了n年的基於TCP的RPC換成RESTful接口,由於根據Netflix的實踐,RESTful能夠更好的解耦、更強的伸縮性等優勢,還能支持多種語言開發,互通性好。可是咱們須要對RESTful完全的理解清楚:
RESTful接口不簡單是是http+json,Richardson成熟度模型中哪一個層級更合適咱們的內網API通信,HATEOAS是否須要?
RESTful的核心是資源,如何在微服務中抽象資源概念,如何將基於過程的RPC調用平滑的遷移到RESTful上?
多語言開發是快,可是後續維護如何找到穩定的Go、Scala、xxx語言程序員來源?
以上的問題應該是架構師在考慮引入新技術的時候的重點,其中對技術優劣和架構思路是核心