1. 程式人生 > >從經典架構項目中透析微服務架構的核心概念和充血模型

從經典架構項目中透析微服務架構的核心概念和充血模型

java 架構 微服務

微服務架構和SOA區別

微服務現在辣麽火,業界流行的對比的卻都是所謂的Monolithic單體應用,而大量的系統在十幾年前都是已經是分布式系統了,那麽微服務作為新的理念和原來的分布式系統,或者說SOA(面向服務架構)是什麽區別呢?

我們先看相同點:

需要Registry,實現動態的服務註冊發現機制;
需要考慮分布式下面的事務一致性,CAP原則下,兩段式提交不能保證性能,事務補償機制需要考慮;
同步調用還是異步消息傳遞,如何保證消息可靠性?SOA由ESB來集成所有的消息;
都需要統一的Gateway來匯聚、編排接口,實現統一認證機制,對外提供APP使用的RESTful接口;
同樣的要關註如何再分布式下定位系統問題,如何做日誌跟蹤,就像我們電信領域做了十幾年的信令跟蹤的功能;

那麽差別在哪?

是持續集成、持續部署?對於CI、CD(持續集成、持續部署),這本身和敏捷、DevOps是交織在一起的,我認為這更傾向於軟件工程的領域而不是微服務技術本身;
使用不同的通訊協議是不是區別?微服務的標桿通訊協議是RESTful,而傳統的SOA一般是SOAP,不過目前來說采用輕量級的RPC框架Dubbo、Thrift、gRPC非常多,在Spring Cloud中也有Feign框架將標準RESTful轉為代碼的API這種仿RPC的行為,這些通訊協議不應該是區分微服務架構和SOA的核心差別;
是流行的基於容器框架還是虛擬機為主?Docker和虛擬機還是物理機都是架構實現的一種方式,不是核心區別;

微服務架構的精髓在切分

服務的切分上有比較大的區別,SOA原本是以一種“集成”技術出現的,很多技術方案是將原有企業內部服務封裝為一個獨立進程,這樣新的業務開發就可重用這些服務,這些服務很可能是類似供應鏈、CRM這樣的非常大的顆粒;而微服務這個“微”,就說明了他在切分上有講究,不妥協。無數的案例證明,如果你的切分是錯誤的,那麽你得不到微服務承諾的“低耦合、升級不影響、可靠性高”之類的優勢,而會比使用Monolithic有更多的麻煩。
不拆分存儲的微服務是偽服務:在實踐中,我們常常見到一種架構,後端存儲是全部和在一個數據庫中,僅僅把前端的業務邏輯拆分到不同的服務進程中,本質上和一個Monolithic一樣,只是把模塊之間的進程內調用改為進程間調用,這種切分不可取,違反了分布式第一原則,模塊耦合沒有解決,性能卻受到了影響。

分布式設計第一原則 — “不要分布你的對象”
微服務的“Micro”這個詞並不是越小越好,而是相對SOA那種粗粒度的服務,我們需要更小更合適的粒度,這種Micro不是無限制的小。
如果我們將兩路(同步)通信與小/微服務結合使用,並根據比如“1個類=1個服務”的原則,那麽我們實際上回到了使用Corba、J2EE和分布式對象的20世紀90年代。遺憾的是,新生代的開發人員沒有使用分布式對象的經驗,因此也就沒有認識到這個主意多麽糟糕,他們正試圖重復歷史,只是這次使用了新技術,比如用HTTP取代了RMI或IIOP。
微服務和Domain Driven Design

一個簡單的圖書管理系統肯定無需微服務架構。既然采用了微服務架構,那麽面對的問題空間必然是比較宏大,比如整個電商、CRM。

如何拆解服務呢?

使用什麽樣的方法拆解服務?業界流行1個類=1個服務、1個方法=1個服務、2 Pizza團隊、2周能重寫完成等方法,但是這些都缺乏實施基礎。我們必須從一些軟件設計方法中尋找,面向對象和設計模式適用的問題空間是一個模塊,而函數式編程的理念更多的是在代碼層面的微觀上起作用。

Eric Evans 的《領域驅動設計》這本書對微服務架構有很大借鑒意義,這本書提出了一個能將一個大問題空間拆解分為領域和實體之間的關系和行為的技術。目前來說,這是一個最合理的解決拆分問題的方案,透過限界上下文(Bounded Context,下文簡稱為BC)這個概念,我們能將實現細節封裝起來,讓BC都能夠實現SRP(單一職責)原則。而每個微服務正是BC在實際世界的物理映射,符合BC思路的微服務互相獨立松耦合。

微服務架構是一件好事,逼著大家關註設計軟件的合理性,如果原來在Monolithic中領域分析、面向對象設計做不好,換微服務會把這個問題成倍的放大
以電商中的訂單和商品兩個領域舉例,按照DDD拆解,他們應該是兩個獨立的限界上下文,但是訂單中肯定是包含商品的,如果貿然拆為兩個BC,查詢、調用關系就耦合在一起了,甚至有了麻煩的分布式事務的問題,這個關聯如何拆解?BC理論認為在不同的BC中,即使是一個術語,他的關註點也不一樣,在商品BC中,關註的是屬性、規格、詳情等等(實際上商品BC這個領域有價格、庫存、促銷等等,把他作為單獨一個BC也是不合理的,這裏為了簡化例子,大家先認為商品BC就是商品基礎信息), 而在訂單BC中更關註商品的庫存、價格。所以在實際編碼設計中,訂單服務往往將關註的商品名稱、價格等等屬性冗余在訂單中,這個設計解脫了和商品BC的強關聯,兩個BC可以獨立提供服務,獨立數據存儲

微服務架構首先要關註的不是RPC/ServiceDiscovery/Circuit Breaker這些概念,也不是Eureka/Docker/SpringCloud/Zipkin這些技術框架,而是服務的邊界、職責劃分,劃分錯誤就會陷入大量的服務間的相互調用和分布式事務中,這種情況微服務帶來的不是便利而是麻煩。

DDD給我們帶來了合理的劃分手段,但是DDD的概念眾多,晦澀難以理解,如何抓住重點,合理的運用到微服務架構中呢?

我認為如下的幾個架構思想是重中之重

充血模型
事件驅動
實際上DDD和面向對象設計、設計模式等等理論有千絲萬縷的聯系,如果不熟悉OOA、OOD,DDD也是使用不好的。不過學習這些OO理論的時候,大家往往感覺到無用武之地,因為大部分的Java程序員開發生涯是從學習J2EE經典的分層理論開始的(Action、Service、Dao),在這種分層理論中,我們基本沒有啥機會使用那些所謂的“行為型”的設計模式,這裏的核心原因,就是J2EE經典分層的開發方式是“貧血模型”。

Martin Fowler在他的《企業應用架構模式》這本書中提出了兩種開發方式“事務腳本”和“領域模型”,這兩種開發分別對應了“貧血模型”和“充血模型”。

針對上面的技術我特意整理了一下,有很多技術不是靠幾句話能講清楚,所以幹脆找朋友錄制了一些視頻,很多問題其實答案很簡單,但是背後的思考和邏輯不簡單,要做到知其然還要知其所以然。如果想學習Java工程化、高性能及分布式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java進階群:582505643,群裏有阿裏大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給大家。

事務腳本開發模式

事務腳本的核心是過程,可以認為大部分的業務處理都是一條條的SQL,事務腳本把單個SQL組織成為一段業務邏輯,在邏輯執行的時候,使用事務來保證邏輯的ACID。最典型的就是存儲過程。當然我們在平時J2EE經典分層架構中,經常在Service層使用事務腳本。
技術分享圖片

使用這種開發方式,對象只用於在各層之間傳輸數據用,這裏的對象就是“貧血模型”,只有數據字段和Get/Set方法,沒有邏輯在對象中。

我們以一個庫存扣減的場景來舉例:

業務場景
首先談一下業務場景,一個下訂單扣減庫存(鎖庫存),這個很簡單

先判斷庫存是否足夠,然後扣減可銷售庫存,增加訂單占用庫存,然後再記錄一個庫存變動記錄日誌(作為憑證)

貧血模型的設計
首先設計一個庫存表 Stock,有如下字段技術分享圖片

從經典架構項目中透析微服務架構的核心概念和充血模型

設計一個Stock對象(Getter和Setter省略)

public class Stock {
private String spuId;
private String skuId;
private int stockNum;
private int orderStockNum;
}
Service入口
設計一個StockService,在其中的lock方法中寫邏輯

入參為(spuId, skuId, num)

實現偽代碼

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合一,這樣可以利用一條語句完成自旋,解決並發問題(高手)。
小結一下:

有沒有發現,在這個業務領域非常重要的核心邏輯 — 下訂單扣減庫存中操作過程中,Stock對象根本不用出現,全部是數據庫操作SQL,所謂的業務邏輯就是由多條SQL構成。Stock只是CRUD的數據對象而已,沒邏輯可言。

馬丁福勒定義的“貧血模型”是反模式,面對簡單的小系統用事務腳本方式開發沒問題,業務邏輯復雜了,業務邏輯、各種狀態散布在大量的函數中,維護擴展的成本一下子就上來,貧血模型沒有實施微服務的基礎。
針對上面的技術我特意整理了一下,有很多技術不是靠幾句話能講清楚,所以幹脆找朋友錄制了一些視頻,很多問題其實答案很簡單,但是背後的思考和邏輯不簡單,要做到知其然還要知其所以然。如果想學習Java工程化、高性能及分布式、深入淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java進階群:582505643,群裏有阿裏大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給大家。
雖然我們用Java這樣的面向對象語言來開發,但是其實和過程型語言是一樣的,所以很多情況下大家用數據庫的存儲過程來替代Java寫邏輯反而效果會更好,(ps:用了Spring boot也不是微服務),
領域模型的開發模式

領域模型是將數據和行為封裝在一起,並與現實世界的業務對象相映射。各類具備明確的職責劃分,使得邏輯分散到合適對象中。這樣的對象就是“充血模型” 。
在具體實踐中,我們需要明確一個概念,就是領域模型是有狀態的,他代表一個實際存在的事物。還是接著上面的例子,我們設計Stock對象需要代表一種商品的實際庫存,並在這個對象上面加上業務邏輯的方法
從經典架構項目中透析微服務架構的核心概念和充血模型
技術分享圖片
這樣做下單鎖庫存業務邏輯的時候,每次必須先從Repository根據主鍵load還原Inventory這個對象,然後執行對應的lock(num)方法改變這個Inventory對象的狀態(屬性也是狀態的一種),然後再通過Repository的save方法把這個對象持久化到存儲去。

完成上述一系列操作的是Application,Application對外提供了這種集成操作的接口
技術分享圖片

領域模型開發方法最重要的是把扣減造成的狀態變化的細節放到了Inventory對象執行,這就是對業務邏輯的封裝。

Application對象的lock方法可以和事務腳本方法的StockService的lock來做個對比,StockService是完全掌握所有細節,一旦有了變化(比如庫存為0也可以扣減),Service方法要跟著變;而Application這種方式不需要變化,只要在Inventory對象內部計算就可以了。代碼放到了合適的地方,計算在合適層次,一切都很合理。這種設計可以充分利用各種OOD、OOP的理論把業務邏輯實現的很漂亮。

充血模型的缺點
從上面的例子,在Repository的load 到執行業務方法,再到save回去,這是需要耗費一定時間的,但是這個過程中如果多個線程同時請求對Inventory庫存的鎖定,那就會導致狀態的不一致,麻煩的是針對庫存的並發不僅難處理而且很常見。

貧血模型完全依靠數據庫對並發的支撐,實現可以簡化很多,但充血模型就得自己實現了,不管是在內存中通過鎖對象,還是使用Redis的遠程鎖機制,都比貧血模型復雜而且可靠性下降,這是充血模型帶來的挑戰。更好的辦法是可以通過事件驅動的架構來取消並發。

領域模型和微服務的關系

上面講了領域模型的實現,但是他和微服務是什麽關系呢?在實踐中,這個Inventory是一個限界上下文的聚合根,我們可以認為一個聚合根就是一個微服務進程。

不過問題又來了,一個庫存的Inventory一定和商品信息是有關聯的,僅僅靠Inventory中的冗余那點商品ID是不夠的,商品的上下架狀態等等都是業務邏輯需要的,那不是又把商品Sku這樣的重型對象引入了這個微服務?兩個重型的對象在一個服務中?這樣的微服務拆不開啊,還是必須依靠商品庫?!

從經典架構項目中透析微服務架構的核心概念和充血模型