微服務架構和SOA區(qū)別
微服務現(xiàn)在辣么火蚯舱,業(yè)界流行的對比的卻都是所謂的Monolithic單體應用改化,而大量的系統(tǒng)在十幾年前都是已經是分布式系統(tǒng)了,那么微服務作為新的理念和原來的分布式系統(tǒng)枉昏,或者說SOA(面向服務架構)是什么區(qū)別呢陈肛?
我們先看相同點:
- 需要Registry,實現(xiàn)動態(tài)的服務注冊發(fā)現(xiàn)機制兄裂;
- 需要考慮分布式下面的事務一致性句旱,CAP原則下,兩段式提交不能保證性能晰奖,事務補償機制需要考慮前翎;
- 同步調用還是異步消息傳遞,如何保證消息可靠性畅涂?SOA由ESB來集成所有的消息港华;
- 都需要統(tǒng)一的Gateway來匯聚、編排接口午衰,實現(xiàn)統(tǒng)一認證機制立宜,對外提供APP使用的RESTful接口;
- 同樣的要關注如何再分布式下定位系統(tǒng)問題臊岸,如何做日志跟蹤橙数,就像我們電信領域做了十幾年的信令跟蹤的功能;
那么差別在哪帅戒?
- 是持續(xù)集成灯帮、持續(xù)部署?對于CI逻住、CD(持續(xù)集成钟哥、持續(xù)部署),這本身和敏捷瞎访、DevOps是交織在一起的腻贰,我認為這更傾向于軟件工程的領域而不是微服務技術本身;
- 使用不同的通訊協(xié)議是不是區(qū)別扒秸?微服務的標桿通訊協(xié)議是RESTful播演,而傳統(tǒng)的SOA一般是SOAP,不過目前來說采用輕量級的RPC框架Dubbo伴奥、Thrift写烤、gRPC非常多,在Spring Cloud中也有Feign框架將標準RESTful轉為代碼的API這種仿RPC的行為拾徙,這些通訊協(xié)議不應該是區(qū)分微服務架構和SOA的核心差別洲炊;
- 是流行的基于容器框架還是虛擬機為主?Docker和虛擬機還是物理機都是架構實現(xiàn)的一種方式,不是核心區(qū)別选浑;
微服務架構的精髓在切分
- 服務的切分上有比較大的區(qū)別,SOA原本是以一種“集成”技術出現(xiàn)的玄叠,很多技術方案是將原有企業(yè)內部服務封裝為一個獨立進程古徒,這樣新的業(yè)務開發(fā)就可重用這些服務,這些服務很可能是類似供應鏈读恃、CRM這樣的非常大的顆粒隧膘;而微服務這個“微”,就說明了他在切分上有講究寺惫,不妥協(xié)疹吃。無數(shù)的案例證明,如果你的切分是錯誤的西雀,那么你得不到微服務承諾的“低耦合萨驶、升級不影響、可靠性高”之類的優(yōu)勢艇肴,而會比使用Monolithic有更多的麻煩腔呜。
- 不拆分存儲的微服務是偽服務:在實踐中,我們常常見到一種架構再悼,后端存儲是全部和在一個數(shù)據(jù)庫中核畴,僅僅把前端的業(yè)務邏輯拆分到不同的服務進程中,本質上和一個Monolithic一樣冲九,只是把模塊之間的進程內調用改為進程間調用谤草,這種切分不可取,違反了分布式第一原則莺奸,模塊耦合沒有解決丑孩,性能卻受到了影響。
分布式設計第一原則 — “不要分布你的對象”
- 微服務的“Micro”這個詞并不是越小越好灭贷,而是相對SOA那種粗粒度的服務嚎杨,我們需要更小更合適的粒度,這種Micro不是無限制的小氧腰。
如果我們將兩路(同步)通信與小/微服務結合使用枫浙,并根據(jù)比如“1個類=1個服務”的原則,那么我們實際上回到了使用Corba古拴、J2EE和分布式對象的20世紀90年代箩帚。遺憾的是,新生代的開發(fā)人員沒有使用分布式對象的經驗黄痪,因此也就沒有認識到這個主意多么糟糕紧帕,他們正試圖重復歷史,只是這次使用了新技術,比如用HTTP取代了RMI或IIOP是嗜。
微服務和Domain Driven Design
一個簡單的圖書管理系統(tǒng)肯定無需微服務架構愈案。既然采用了微服務架構,那么面對的問題空間必然是比較宏大鹅搪,比如整個電商站绪、CRM。
如何拆解服務呢丽柿?
使用什么樣的方法拆解服務恢准?業(yè)界流行1個類=1個服務、1個方法=1個服務甫题、2 Pizza團隊馁筐、2周能重寫完成等方法,但是這些都缺乏實施基礎坠非。我們必須從一些軟件設計方法中尋找敏沉,面向對象和設計模式適用的問題空間是一個模塊,而函數(shù)式編程的理念更多的是在代碼層面的微觀上起作用炎码。
Eric Evans 的《領域驅動設計》這本書對微服務架構有很大借鑒意義赦抖,這本書提出了一個能將一個大問題空間拆解分為領域和實體之間的關系和行為的技術。目前來說辅肾,這是一個最合理的解決拆分問題的方案队萤,透過限界上下文(Bounded Context,下文簡稱為BC)這個概念矫钓,我們能將實現(xiàn)細節(jié)封裝起來要尔,讓BC都能夠實現(xiàn)SRP(單一職責)原則匆瓜。而每個微服務正是BC在實際世界的物理映射承璃,符合BC思路的微服務互相獨立松耦合烛恤。
微服務架構是一件好事古今,逼著大家關注設計軟件的合理性,如果原來在Monolithic中領域分析畏梆、面向對象設計做不好蟹腾,換微服務會把這個問題成倍的放大
以電商中的訂單和商品兩個領域舉例煌珊,按照DDD拆解私杜,他們應該是兩個獨立的限界上下文蚕键,但是訂單中肯定是包含商品的,如果貿然拆為兩個BC衰粹,查詢锣光、調用關系就耦合在一起了,甚至有了麻煩的分布式事務的問題铝耻,這個關聯(lián)如何拆解誊爹?BC理論認為在不同的BC中,即使是一個術語,他的關注點也不一樣频丘,在商品BC中办成,關注的是屬性、規(guī)格搂漠、詳情等等(實際上商品BC這個領域有價格迂卢、庫存、促銷等等状答,把他作為單獨一個BC也是不合理的冷守,這里為了簡化例子刀崖,大家先認為商品BC就是商品基礎信息)惊科, 而在訂單BC中更關注商品的庫存、價格亮钦。所以在實際編碼設計中馆截,訂單服務往往將關注的商品名稱、價格等等屬性冗余在訂單中蜂莉,這個設計解脫了和商品BC的強關聯(lián)蜡娶,兩個BC可以獨立提供服務,獨立數(shù)據(jù)存儲
小結
微服務架構首先要關注的不是RPC/ServiceDiscovery/Circuit Breaker這些概念映穗,也不是Eureka/Docker/SpringCloud/Zipkin這些技術框架窖张,而是服務的邊界、職責劃分蚁滋,劃分錯誤就會陷入大量的服務間的相互調用和分布式事務中宿接,這種情況微服務帶來的不是便利而是麻煩。
DDD給我們帶來了合理的劃分手段辕录,但是DDD的概念眾多睦霎,晦澀難以理解,如何抓住重點走诞,合理的運用到微服務架構中呢副女?
我認為如下的幾個架構思想是重中之重
- 充血模型
- 事件驅動
上文我們聊了微服務的DDD之間的關系,很多人還是覺得很虛幻蚣旱,DDD那么復雜的理論碑幅,聚合根、值對象塞绿、事件溯源枕赵,到底我們該怎么入手呢?
實際上DDD和面向對象設計位隶、設計模式等等理論有千絲萬縷的聯(lián)系拷窜,如果不熟悉OOA、OOD,DDD也是使用不好的篮昧。不過學習這些OO理論的時候赋荆,大家往往感覺到無用武之地,因為大部分的Java程序員開發(fā)生涯是從學習J2EE經典的分層理論開始的(Action懊昨、Service窄潭、Dao),在這種分層理論中酵颁,我們基本沒有啥機會使用那些所謂的“行為型”的設計模式嫉你,這里的核心原因,就是J2EE經典分層的開發(fā)方式是“貧血模型”躏惋。
Martin Fowler在他的《企業(yè)應用架構模式》這本書中提出了兩種開發(fā)方式“事務腳本”和“領域模型”幽污,這兩種開發(fā)分別對應了“貧血模型”和“充血模型”。
事務腳本開發(fā)模式
事務腳本的核心是過程簿姨,可以認為大部分的業(yè)務處理都是一條條的SQL距误,事務腳本把單個SQL組織成為一段業(yè)務邏輯,在邏輯執(zhí)行的時候扁位,使用事務來保證邏輯的ACID准潭。最典型的就是存儲過程。當然我們在平時J2EE經典分層架構中域仇,經常在Service層使用事務腳本刑然。
使用這種開發(fā)方式,對象只用于在各層之間傳輸數(shù)據(jù)用暇务,這里的對象就是“貧血模型”泼掠,只有數(shù)據(jù)字段和Get/Set方法,沒有邏輯在對象中般卑。
我們以一個庫存扣減的場景來舉例:
- 業(yè)務場景
首先談一下業(yè)務場景武鲁,一個下訂單扣減庫存(鎖庫存),這個很簡單
先判斷庫存是否足夠蝠检,然后扣減可銷售庫存沐鼠,增加訂單占用庫存,然后再記錄一個庫存變動記錄日志(作為憑證)
- 貧血模型的設計
首先設計一個庫存表 Stock叹谁,有如下字段
設計一個Stock對象(Getter和Setter省略)
<colgroup style="box-sizing: border-box;"><col style="box-sizing: border-box; width: 70px;"><col style="box-sizing: border-box; width: 592px;"></colgroup>
| 1
2
3
4
5
6 | public class Stock {
private String spuId;
private String skuId;
private int stockNum;
private int orderStockNum;
} |
- Service入口
設計一個StockService饲梭,在其中的lock方法中寫邏輯
入參為(spuId, skuId, num)
實現(xiàn)偽代碼
<colgroup style="box-sizing: border-box;"><col style="box-sizing: border-box; width: 70px;"><col style="box-sizing: border-box; width: 717px;"></colgroup>
| 1
2
3
4
5
6
7 | count = select stocknum from stock where spuId=xx and skuid=xx
if count>num {
update stock set stocknum=stocknum-num, orderstocknum=orderstocknum+num where skuId=xx and spuId=xx
} else {
//庫存不足,扣減失敗
}
insert stock_log set xx=xx, date= new Date() |
- ok焰檩,打完收工憔涉,如果做的好一些,可以把update和select count合一析苫,這樣可以利用一條語句完成自旋兜叨,解決并發(fā)問題(高手)穿扳。
小結一下:
有沒有發(fā)現(xiàn),在這個業(yè)務領域非常重要的核心邏輯 — 下訂單扣減庫存中操作過程中国旷,Stock對象根本不用出現(xiàn)矛物,全部是數(shù)據(jù)庫操作SQL,所謂的業(yè)務邏輯就是由多條SQL構成跪但。Stock只是CRUD的數(shù)據(jù)對象而已履羞,沒邏輯可言。
- 馬丁福勒定義的“貧血模型”是反模式屡久,面對簡單的小系統(tǒng)用事務腳本方式開發(fā)沒問題忆首,業(yè)務邏輯復雜了,業(yè)務邏輯被环、各種狀態(tài)散布在大量的函數(shù)中糙及,維護擴展的成本一下子就上來,貧血模型沒有實施微服務的基礎蛤售。
- 雖然我們用Java這樣的面向對象語言來開發(fā)丁鹉,但是其實和過程型語言是一樣的妒潭,所以很多情況下大家用數(shù)據(jù)庫的存儲過程來替代Java寫邏輯反而效果會更好悴能,(ps:用了Spring boot也不是微服務),
領域模型的開發(fā)模式
- 領域模型是將數(shù)據(jù)和行為封裝在一起雳灾,并與現(xiàn)實世界的業(yè)務對象相映射漠酿。各類具備明確的職責劃分,使得邏輯分散到合適對象中谎亩。這樣的對象就是“充血模型” 炒嘲。
- 在具體實踐中,我們需要明確一個概念匈庭,就是領域模型是有狀態(tài)的夫凸,他代表一個實際存在的事物。還是接著上面的例子阱持,我們設計Stock對象需要代表一種商品的實際庫存夭拌,并在這個對象上面加上業(yè)務邏輯的方法
這樣做下單鎖庫存業(yè)務邏輯的時候,每次必須先從Repository根據(jù)主鍵load還原Inventory這個對象衷咽,然后執(zhí)行對應的lock(num)方法改變這個Inventory對象的狀態(tài)(屬性也是狀態(tài)的一種)鸽扁,然后再通過Repository的save方法把這個對象持久化到存儲去。
完成上述一系列操作的是Application,Application對外提供了這種集成操作的接口
領域模型開發(fā)方法最重要的是把扣減造成的狀態(tài)變化的細節(jié)放到了Inventory對象執(zhí)行镶骗,這就是對業(yè)務邏輯的封裝桶现。
Application對象的lock方法可以和事務腳本方法的StockService的lock來做個對比,StockService是完全掌握所有細節(jié)鼎姊,一旦有了變化(比如庫存為0也可以扣減)骡和,Service方法要跟著變相赁;而Application這種方式不需要變化,只要在Inventory對象內部計算就可以了慰于。代碼放到了合適的地方噪生,計算在合適層次,一切都很合理东囚。這種設計可以充分利用各種OOD跺嗽、OOP的理論把業(yè)務邏輯實現(xiàn)的很漂亮。
- 充血模型的缺點
從上面的例子页藻,在Repository的load 到執(zhí)行業(yè)務方法桨嫁,再到save回去,這是需要耗費一定時間的份帐,但是這個過程中如果多個線程同時請求對Inventory庫存的鎖定璃吧,那就會導致狀態(tài)的不一致,麻煩的是針對庫存的并發(fā)不僅難處理而且很常見废境。
貧血模型完全依靠數(shù)據(jù)庫對并發(fā)的支撐畜挨,實現(xiàn)可以簡化很多,但充血模型就得自己實現(xiàn)了噩凹,不管是在內存中通過鎖對象巴元,還是使用Redis的遠程鎖機制,都比貧血模型復雜而且可靠性下降驮宴,這是充血模型帶來的挑戰(zhàn)逮刨。更好的辦法是可以通過事件驅動的架構來取消并發(fā)。
領域模型和微服務的關系
上面講了領域模型的實現(xiàn)堵泽,但是他和微服務是什么關系呢修己?在實踐中,這個Inventory是一個限界上下文的聚合根迎罗,我們可以認為一個聚合根就是一個微服務進程睬愤。
不過問題又來了,一個庫存的Inventory一定和商品信息是有關聯(lián)的纹安,僅僅靠Inventory中的冗余那點商品ID是不夠的尤辱,商品的上下架狀態(tài)等等都是業(yè)務邏輯需要的,那不是又把商品Sku這樣的重型對象引入了這個微服務钻蔑?兩個重型的對象在一個服務中啥刻?這樣的微服務拆不開啊,還是必須依靠商品庫咪笑?可帽!
接上文,我們采用了領域驅動的開發(fā)方式窗怒,使用了充血模型映跟,享受了他的好處蓄拣,但是也不得不面對他帶來的弊端。這個弊端在分布式的微服務架構下面又被放大努隙。
事務一致性
事務一致性的問題在Monolithic下面不是大問題球恤,在微服務下面卻是很致命,我們回顧一下所謂的ACID原則
- Atomicity – 原子性荸镊,改變數(shù)據(jù)狀態(tài)要么是一起完成咽斧,要么一起失敗
- Consistency – 一致性,數(shù)據(jù)的狀態(tài)是完整一致的
- Isolation – 隔離線躬存,即使有并發(fā)事務张惹,互相之間也不影響
- Durability – 持久性, 一旦事務提交岭洲,不可撤銷
在單體服務和關系型數(shù)據(jù)庫的時候宛逗,我們很容易通過數(shù)據(jù)庫的特性去完成ACID。但是一旦你按照DDD拆分聚合根-微服務架構盾剩,他們的數(shù)據(jù)庫就已經分離開了雷激,你就要獨立面對分布式事務,要在自己的代碼里面滿足ACID告私。
對于分布式事務屎暇,大家一般會想到以前的JTA標準,2PC兩段式提交德挣。我記得當年在Dubbo群里面恭垦,基本每周都會有人詢問Dubbo啥時候支撐分布式事務快毛。實際上根據(jù)分布式系統(tǒng)中CAP原則格嗅,當P(分區(qū)容忍)發(fā)生的時候,強行追求C(一致性)唠帝,會導致(A)可用性屯掖、吞吐量下降,此時我們一般用最終一致性來保證我們系統(tǒng)的AP能力襟衰。當然不是說放棄C贴铜,而是在一般情況下CAP都能保證,在發(fā)生分區(qū)的情況下瀑晒,我們可以通過最終一致性來保證數(shù)據(jù)一致绍坝。
例:
在電商業(yè)務的下訂單凍結庫存場景。需要根據(jù)庫存情況確定訂單是否成交苔悦。
假設你已經采用了分布式系統(tǒng)轩褐,這里訂單模塊和庫存模塊是兩個服務,分別擁有自己的存儲(關系型數(shù)據(jù)庫)玖详,
在一個數(shù)據(jù)庫的時候把介,一個事務就能搞定兩張表的修改勤讽,但是微服務中,就沒法這么做了拗踢。
在DDD理念中脚牍,一次事務只能改變一個聚合內部的狀態(tài),如果多個聚合之間需要狀態(tài)一致巢墅,那么就要通過最終一致性诸狭。訂單和庫存明顯是分屬于兩個不同的限界上下文的聚合,這里需要實現(xiàn)最終一致性君纫,就需要使用事件驅動的架構作谚。
推薦一個交流學習群:688583154里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis庵芭,Netty源碼分析妹懒,高并發(fā)、高性能双吆、分布式眨唬、微服務架構的原理,JVM性能優(yōu)化這些成為架構師必備的知識體系好乐。還能領取免費的學習資源匾竿,目前受益良多:
事件驅動實現(xiàn)最終一致性
事件驅動架構在領域對象之間通過異步的消息來同步狀態(tài),有些消息也可以同時發(fā)布給多個服務蔚万,在消息引起了一個服務的同步后可能會引起另外消息岭妖,事件會擴散開。嚴格意義上的事件驅動是沒有同步調用的反璃。
例子:
在訂單服務新增訂單后昵慌,訂單的狀態(tài)是“已開啟”,然后發(fā)布一個Order Created事件到消息隊列上
庫存服務在接收到Order Created 事件后淮蜈,將庫存表格中的某sku減掉可銷售庫存斋攀,增加訂單占用庫存,然后再發(fā)送一個Inventory Locked事件給消息隊列
訂單服務接收到Inventory Locked事件梧田,將訂單的狀態(tài)改為“已確認”
有人問淳蔼,如果庫存不足,鎖定不成功怎么辦裁眯? 簡單鹉梨,庫存服務發(fā)送一個Lock Fail事件, 訂單服務接收后穿稳,把訂單置為“已取消”存皂。
好消息,我們可以不用鎖司草!
事件驅動有個很大的優(yōu)勢就是取消了并發(fā)艰垂,所有請求都是排隊進來泡仗,這對我們實施充血模型有很大幫助,我們可以不需要自己來管理內存中的鎖了猜憎。取消鎖娩怎,隊列處理效率很高,事件驅動可以用在高并發(fā)場景下胰柑,比如搶購截亦。
是的,用戶體驗有改變柬讨,
用了這個事件驅動崩瓤,用戶的體驗有可能會有改變,比如原來同步架構的時候沒有庫存踩官,就馬上告訴你條件不滿足無法下單却桶,不會生成訂單;但是改了事件機制蔗牡,訂單是立即生成的颖系,很可能過了一會系統(tǒng)通知你訂單被取消掉。 就像搶購“小米手機”一樣辩越,幾十萬人在排隊嘁扼,排了很久告訴你沒貨了,明天再來吧黔攒。如果希望用戶立即得到結果趁啸,可以在前端想辦法,在BFF(Backend For Frontend)使用CountDownLatch這樣的鎖把后端的異步轉成前端同步督惰,當然這樣BFF消耗比較大不傅。
沒辦法,產品經理不接受姑丑,
產品經理說用戶的體驗必須是沒有庫存就不會生成訂單蛤签,這個方案會不斷的生成取消的訂單,他不能接受栅哀,怎么辦?那就在訂單列表查詢的時候称龙,略過這些cancel狀態(tài)的訂單吧留拾,也許需要一個額外的視圖來做。我并不是一個理想主義者鲫尊,解決當前的問題是我首先要考慮的痴柔,我們設計微服務的目的是本想是解決業(yè)務并發(fā)量。而現(xiàn)在面臨的卻是用戶體驗的問題疫向,所以架構設計也是需要妥協(xié)的:( 但是至少分析完了咳蔚,我知道我妥協(xié)在什么地方豪嚎,為什么妥協(xié),未來還有可能改變谈火。
多個領域多表Join查詢
- 我個人認為聚合根這樣的模式對修改狀態(tài)是特別合適侈询,但是對搜索數(shù)據(jù)的確是不方便,比如篩選出一批符合條件的訂單這樣的需求糯耍,本身聚合根對象不能承擔批量的查詢任務扔字,因為這不是他的職責。那就必須依賴“領域服務(Domain Service)”這種設施温技。
當一個方法不便放在實體或者值對象上革为,使用領域服務便是最佳的解決方法,請確保領域服務是無狀態(tài)的舵鳞。
- 我們的查詢任務往往很復雜震檩,比如查詢商品列表,要求按照上個月的銷售額進行排序蜓堕; 要按照商品的退貨率排序等等恳蹲。但是在微服務和DDD之后,我們的存儲模型已經被拆離開俩滥,上述的查詢都是要涉及訂單嘉蕾、用戶、商品多個領域的數(shù)據(jù)霜旧。如何搞错忱? 此時我們要引入一個視圖的概念。比如下面的挂据,查詢用戶名下訂單的操作以清,直接調用兩個服務自己在內存中join效率無疑是很低的,再加上一些filter條件崎逃、分頁掷倔,沒法做了。于是我們將事件廣播出去个绍,由一個單獨的視圖服務來接收這些事件勒葱,并形成一個物化視圖(materialized view),這些數(shù)據(jù)已經join過巴柿,處理過凛虽,放在一個單獨的查詢庫中,等待查詢广恢,這是一個典型的以空間換時間的處理方式凯旋。
經過分析,除了簡單的根據(jù)主鍵Find或者沒有太多關聯(lián)的List查詢,我們大部分的查詢任務可以放到單獨的查詢庫中至非,這個查詢庫可以是關系數(shù)據(jù)庫的ReadOnly庫钠署,也可以是NoSQL的數(shù)據(jù)庫,實際上我們在項目中使用了ElasticSearch作為專門的查詢視圖荒椭,效果很不錯
限界上下文(Bounded Context)和數(shù)據(jù)耦合
除了多領域join的問題谐鼎,我們在業(yè)務中還會經常碰到一些場景,比如電商中的商品信息是基礎信息戳杀,屬于單獨的BC该面,而其他BC,不管是營銷服務信卡、價格服務隔缀、購物車服務、訂單服務都是需要引用這個商品信息的傍菇。但是需要的商品信息只是全部的一小部分而已猾瘸,營銷服務需要商品的id和名稱、上下架狀態(tài)丢习;訂單服務需要商品id牵触、名稱、目錄咐低、價格等等揽思。這比起商品中心定義一個商品(商品id、名稱见擦、規(guī)格钉汗、規(guī)格值、詳情等等)只是一個很小的子集鲤屡。這說明不同的限界上下文的同樣的術語损痰,但是所指的概念不一樣。 這樣的問題映射到我們的實現(xiàn)中酒来,每次在訂單卢未、營銷模塊中直接查詢商品模塊,肯定是不合適堰汉,因為
- 商品中心需要適配每個服務需要的數(shù)據(jù)辽社,提供不同的接口
- 并發(fā)量必然很大
- 服務之間的耦合嚴重,一旦宕機衡奥、升級影響的范圍很大爹袁。
特別是最后一條,嚴重限制了我們獲得微服務提供的優(yōu)勢“松耦合矮固、每個服務自己可以頻繁升級不影響其他模塊”。這就需要我們通過事件驅動方法,適當冗余一些數(shù)據(jù)到不同的BC去档址,把這種耦合拆解開盹兢。這種耦合有時候是通過Value Object嵌入到實體中的方式,在生成實體的時候就冗余守伸,比如訂單在生成的時候就冗余了商品的信息绎秒;有時候是通過額外的Value Object列表方式,營銷中心冗余一部分相關的商品列表數(shù)據(jù)尼摹,并隨時關注監(jiān)聽商品的上下級狀態(tài)见芹,同步替換掉本限界上下文的商品列表。
下圖一個下單場景分析蠢涝,在電商系統(tǒng)中玄呛,我們可以認為會員和商品是所有業(yè)務的基礎數(shù)據(jù),他們的變更應該是通過廣播的方式發(fā)布到各個領域和二,每個領域保留自己需要的信息徘铝。
保證最終一致性
最終一致性成功依賴很多條件
- 依賴消息傳遞的可靠性,可能A系統(tǒng)變更了狀態(tài)惯吕,消息發(fā)到B系統(tǒng)的時候丟失了惕它,導致AB的狀態(tài)不一致
- 依賴服務的可靠性,如果A系統(tǒng)變更了自己的狀態(tài)废登,但是還沒來得及發(fā)送消息就掛了淹魄。也會導致狀態(tài)不一致
我記得JavaEE規(guī)范中的JMS中有針對這兩種問題的處理要求,一個是JMS通過各種確認消息(Client Acknowledge等)來保證消息的投遞可靠性堡距,另外是JMS的消息投遞操作可以加入到數(shù)據(jù)庫的事務中-即沒有發(fā)送消息甲锡,會引起數(shù)據(jù)庫的回滾(沒有查資料,不是很準確的描述吏颖,請專家指正)搔体。不過現(xiàn)在符合JMS規(guī)范的MQ沒幾個,特別是保一致性需要降低性能半醉,現(xiàn)在標榜高吞吐量的MQ都把問題拋給了我們自己的應用解決疚俱。所以這里介紹幾個常見的方法,來提升最終一致性的效果缩多。
推薦一個交流學習群:688583154里面會分享一些資深架構師錄制的視頻錄像:有Spring呆奕,MyBatis,Netty源碼分析衬吆,高并發(fā)梁钾、高性能、分布式逊抡、微服務架構的原理姆泻,JVM性能優(yōu)化這些成為架構師必備的知識體系零酪。還能領取免費的學習資源,目前受益良多:
使用本地事務
還是以上面的訂單扣取信用的例子
- 訂單服務開啟本地事務拇勃,首先新增訂單四苇;
- 然后將Order Created事件插入一張專門Event表,事務提交方咆;
- 有一個單獨的定時任務線程月腋,定期掃描Event表,掃出來需要發(fā)送的就丟到MQ瓣赂,同時把Event設置為“已發(fā)送”榆骚。
方案的優(yōu)勢是使用了本地數(shù)據(jù)庫的事務,如果Event沒有插入成功煌集,那么訂單也不會被創(chuàng)建妓肢;線程掃描后把event置為已發(fā)送,也確保了消息不會被漏發(fā)(我們的目標是寧可重發(fā)牙勘,也不要漏發(fā)职恳,因為Event處理會被設計為冪等)。
缺點是需要單獨處理Event發(fā)布在業(yè)務邏輯中方面,繁瑣容易忘記放钦;Event發(fā)送有些滯后;定時掃描性能消耗大恭金,而且會產生數(shù)據(jù)庫高水位隱患操禀;
我們稍作改進,使用數(shù)據(jù)庫特有的MySQL Binlog跟蹤(阿里的Canal)或者Oracle的GoldenGate技術可以獲得數(shù)據(jù)庫的Event表的變更通知横腿,這樣就可以避免通過定時任務來掃描了
不過用了這些數(shù)據(jù)庫日志的工具颓屑,會和具體的數(shù)據(jù)庫實現(xiàn)(甚至是特定的版本)綁定,決策的時候請慎重耿焊。
使用Event Sourcing 事件溯源
事件溯源對我們來說是一個特別的思路揪惦,他并不持久化Entity對象,而是只把初始狀態(tài)和每次變更的Event記錄下來罗侯,并在內存中根據(jù)Event還原Entity對象的最新狀態(tài)器腋,具體實現(xiàn)很類似數(shù)據(jù)庫的Redolog的實現(xiàn),只是他把這種機制放到了應用層來钩杰。
雖然事件溯源有很多宣稱的優(yōu)勢纫塌,引入這種技術要特別小心,首先他不一定適合大部分的業(yè)務場景讲弄,一旦變更很多的情況下措左,效率的確是個大問題;另外一些查詢的問題也是困擾避除。
我們僅僅在個別的業(yè)務上探索性的使用Event Souring和AxonFramework怎披,由于實現(xiàn)起來比較復雜胸嘁,具體的情況還需要等到實踐一段時間后再來總結,也許需要額外的一篇文章來詳細描述