1. 程式人生 > >軟體設計架構之DDD,SOA,原始,REST,Actor,CQRS

軟體設計架構之DDD,SOA,原始,REST,Actor,CQRS

from:http://www.jdon.com/soa.html

http://www.jdon.com/45728

http://www.jdon.com/cqrs.html

1.SOA:

 首先Martin Fowler提出SOA歧義Service Oriented Ambiguity,認為"什麼是SOA"是不可能回答,因為不同的人意味著不同的事情,SOA意味服務介面,意味流程整合,意味資源再利用,意味著管制,在下面SOA元件圖中,服務和服務消費者(客戶端)之間存在多個約束,當一個服務顯式暴露後,客戶端能夠通過繫結定位到該服務,相當於兩者簽訂了合同,規定了合同內容和如何實施,具體合同的描述是通過訊息方式進行:

  由於Java等傳統語言主要是以類表達物件,將功能行為封裝在類內部,而業務客戶一般都是注重軟體的功能,包括同行業公司系統之間資料交流也是以功能服務為介面,因此,面向服務的架構SOA更加貼近業務客戶,也更適合業務夥伴之間流程整合,各個行業已經誕生自己行業特點的SOA,例如電信聯盟的NGOSS,已經成為電信行業業務支撐系統BOSS的標準。SOA不但是技術用語,也是業務銷售用語,通過服務這個中間概念,可以實現業務和技術之間的無縫轉換,如今SOA已經和RESTDDD以及雲端計算等新技術方法結合。其內部主要概念有SCA ESB JBI等等,涉及工作流 規則引擎 訊息匯流排等多個技術細節方面。

  通常,一個架構師進行系統架構頂層設計時,必須考慮使用者的利益,不能單單實現軟體的功能,還要考慮到軟體的效能Scalable 可用性available/usable 安全性等軟體質量,還要借鑑社群的最佳實踐和經驗形成的模式和反模式,避免重蹈覆轍和陷阱,再大膽採取最新的軟體技術(比如用REST替代SOAP等)。

  服務的提出其實隱含了兩個概念,服務提供者和服務消費者,這兩者之間有一個合同約定,這非常類似我們現實生活中籤訂的服務合同,A單位和B單位分別是服務的提供者和消費者,兩者簽訂了一個服務合同,規定A為B提供某項服務。服務就是提供一些公共需求的設施,通過一個工作過程能提供幫助,使用,讓使用者受益。

  服務具體有如下:Windows Service:如PC定位者RPC Locator, 事件日誌EventLog, DHCP Client,;. 軟體服務Software Service,如分散式服務Distribution Service, 警告服務Alert Service 安全服務 Security Service, 日誌服務;業務服務Business Service,如 帳號和客戶服務,銷售服務,訂單服務,採購服務。

  服務兩個重要特點:自治和管制,自治代表服務不能被外部勢力牽制,比如如果一個服務內部處理中需要呼叫外部資源或等待外部流程結束,這種等待不能影響服務本身的呼叫,如果一個服務分為顯式對外和隱式內部兩個部分,那麼自治是針對隱式內部,意味著我們不能在具體一個服務中直接使用同步程式碼實現複雜功能。如:

public AServiceImpl implements AService{
  public void productSale(...){
    Product product = productService.getProduct();
    int inventory = InventoryService.getInventory(product);
    int price = priceService.getPrice(product);
 
  }
}

  在AServiceImpl的productSale方法中,我們獲得商品的庫存和定價,都是通過同步的RPC實現呼叫的,這樣造成productSale方法依賴於InventoryService和priceService,無法實現自身自治。

  實現服務真正自治,實際就是解決類之間依賴耦合的問題,訊息是一種方式,但是基於訊息又有兩種通訊方式,基於請求響應和基於事件的EDA。

  從服務自治可以看出,為什麼要提出服務必須自治,因為服務是受管制的,在實際業務活動中,不同服務是被不同部分管理,比如定價服務歸屬財務部門系統,庫存歸屬倉庫系統,涉及系統之間呼叫協調不能自己使用同步RPC,而是需要訊息。

  在實際應用中,很多單位使用SOA主要看中其能夠無縫整合新舊系統,稱為EAI企業應用整合,下圖是蘇寧的一種SOA圖,使用ESB企業服務匯流排這樣的訊息系統整合了新舊各種系統。

蘇寧SOA

  使用SOA和ESB能夠靈活實現業務流程管理,工作流的管理BPM,如下圖,一個訂單的產生可能需要幾個部門批准才能完成,而且這幾個部門經常是變化的,如何靈活實現這種批准流程的定製也成為SOA實現的一部分,如下:

bpm

  注意圖中1 2 3 4 5 6 7 8 9標註的訂單處理流程步驟,這種不同服務之間呼叫處理順序可通過BPM進行靈活定製。

  目前提供SOA全套解決方案和產品的廠商很多,包括IBM SAP和Oracle,國內金蝶用友浪潮軟體等等,比如蘇寧的SOA是以SAP為主的八國聯軍組裝,既然SOA中介軟體服務商已經為我們提供了成熟的架構方案和產品,那麼作為SOA使用者是否就無需頂層架構設計了呢?當然不是,SOA使用者要根據自己業務進行模組劃分,進行領域建模設計,根據DDD領域驅動設計將業務分解為一個上下文模組,然後再用服務作為對外介面,內部封裝的是DDD聚合根,而傳統SOA作法是內部封裝的是資料表的DTO,從而導致SOA服務內部腐爛堵塞,違背SOA自治和可用性等原則約束。具體可見DDD領域驅動設計

SOA的好處

1. 鬆耦合:由於服務自治,有一定封裝邊界,服務呼叫互動是通過釋出介面。這意味著應用程式不感興趣的服務如何被實現
2.位置透明:服務的消費者不必關係服務位於什麼地方。
3.可在異構平臺間複用。可以將遺留系統包裝成服務。
4.便於測試,能並行開發,較高可靠性和良好可伸縮性。

2.REST基礎概念:

  • 在REST中的一切都被認為是一種資源。
  • 每個資源由URI標識。
  • 使用統一的介面。處理資源使用POST,GET,PUT,DELETE操作類似建立,讀取,更新和刪除(CRUD)操作。
  • 無狀態。每個請求是一個獨立的請求。從客戶端到伺服器的每個請求都必須包含所有必要的資訊,以便於理解。
  • 通訊都是通過展現。例如XML,JSON

  以狀態為角度,提出將狀態移植到客戶端處理的新思路。 提出一個既適於客戶端應用又適於服務端的應用的、統一的Web檢視。適合B/S C/S S/S。 HTTP客戶端與HTTP伺服器之間的差別,對架構來說無所謂。一個軟體應可以既充當Web客戶端又充當Web伺服器,而無須採用兩套完全不同的APIs。

  提供資源操作方法的統一:POST, GET, PUT, DELETE ,以超文字或超媒體驅動(hypertext/Hypermedia)的狀態轉移是REST架構核心。 操作帶來狀態變化,狀態轉移遍歷使用連結導航方式實現。

  如下圖:首先通過GET方法訪問/well-known-uri(1)獲得當前所有資源(2),然後選擇其中一個資源名FooService通過Get方法訪問/well-known-uri/foo(3),這樣得到foo下的資源列表。

rest

  foo可能是一個領域模型或其他代表業務核心的資源,假設foo是訂單,使用者如果希望改變訂單狀態,比如撤銷訂單,一旦點按撤銷訂單按鈕,客戶端將向/well-known-uri/foo/reverse發出PUT命令(5),代表撤銷訂單,這其實一個修改訂單狀態的命令。

  客戶端再次發出GET命令(6),獲得狀態已經改變的結果。

  值得注意的是,當發出PUT命令後,不是通常由伺服器端立即返回業務操作結果,而是返回Http的200,表示PUT操作完成,具體業務結果必須由客戶端再次根據第三步獲得的資源列表中URI資源,再次由客戶端發出查詢命令獲得(6)。

-----------------------------------

3.DDD

Eric Evans的“Domain-Driven Design領域驅動設計”簡稱DDD,Evans DDD是一套綜合軟體系統分析和設計的面向物件建模方法,本站Jdon.com是國內公開最早討論DDD網站之一,可訂閱DDD專題。初學者學習DDD可從研究本站Jdon框架的DDD應用原始碼開始,戳這裡開始

  過去系統分析和系統設計都是分離的,正如我們國家“系統分析師” 和“系統設計師” 兩種職稱考試一樣,這樣割裂的結果導致,需求分析的結果無法直接進行設計程式設計,而能夠進行程式設計執行的程式碼卻扭曲需求,導致客戶執行軟體後才發現很多功能不是自己想要的,而且軟體不能快速跟隨需求變化。

  DDD則打破了這種隔閡,提出了領域模型概念,統一了分析和設計程式設計,使得軟體能夠更靈活快速跟隨需求變化。見下面DDD與傳統CRUD或過程指令碼或者面向資料表等在開發效率上比較:

ddd

  伺服器後端發展三個階段:

  1. UI+DataBase的兩層架構,這種面向資料庫的架構(上圖table module )沒有靈活性。
  2. UI+Service+DataBase的多層SOA架構,這種服務+表模型的架構易使服務變得囊腫,難於維護拓展,伸縮效能差,見這裡討論Spring Web 應用的最大敗筆.
  3. DDD+SOA的事件驅動的CQRS讀寫分離架構,應付複雜業務邏輯,以聚合模型替代資料表模型,以併發的事件驅動替代串聯的訊息驅動。真正實現以業務實體為核心的靈活拓展。

  DDD革命性在於:領域模型準確反映了業務語言,而傳統J2EE或Spring+Hibernate等事務性程式設計模型只關心資料,這些資料物件除了簡單setter/getter方法外,沒有任何業務方法,被比喻成失血模型,那麼領域模型這種帶有業務方法的充血模型到底好在哪裡?

  以比賽Match為案例,比賽有“開始”和“結束”等業務行為,但是傳統經典的方式是將“開始”和“結束”行為放在比賽的服務Service中,而不是放在比賽物件本身之中。我們不能因為用了計算機,用了資料庫,用了框架,業務模型反而被技術框架給綁架,就像人雖然是由母親生的,但是人的吃喝拉撒母親不能替代,更不能以母愛名義肢解人的正常職責行為,如果是這樣,這個人就是被母愛綁架了。

  提倡充血模型,實際就是讓過去被肢解被黑crack的業務模型迴歸正常,當然這也會被一些先入為主或被洗過腦的程式設計師看成反而不正常,這更是極大可悲之處。看到領域模型程式碼,就看到業務需求,沒有翻譯沒有轉換,保證軟體真正實現“拷貝不走樣”。

  DDD最大的好處是:接觸到需求第一步就是考慮領域模型,而不是將其切割成資料和行為,然後資料用資料庫實現,行為使用服務實現,最後造成需求的首肢分離。DDD讓你首先考慮的是業務語言,而不是資料。重點不同導致程式設計世界觀不同。

  DDD是解決複雜中大型軟體的一套行之有效方式,在國外已經成為主流。DDD認為很多原因造成軟體的複雜性,我們不可能避免這些複雜性,能做的是對複雜的問題進行控制。而一個好的領域模型是控制複雜問題的關鍵。領域模型的價值在於提供一種通用的語言,使得領域專家和軟體技術人員聯絡在一起,溝通無歧義。

  DDD在軟體生產流程中定位i如下圖,DDD落地實現離不開in-memory快取、 CQRS、 DCIEDAEvent Source幾大大相關領域。

cache

4.Actor

首先看看道友提出的一個問題:
使用者甲的操作
1.開始事務
2.訪問表A
3.訪問表B
4.提交事務
乙使用者在操作
1.開始事務
2.訪問表B
3.訪問表A
4.提交事務 

如果甲使用者和乙使用者的兩個事務同時發生,甲事務鎖住了表A未釋放(因為整個事務未完成),正在準備訪問B表,而乙事務鎖住了表B未釋放(因為整個事務未完成),正在準備訪問A表,可是A表被甲事務鎖住了,等甲事務釋放,而甲事務真正等待乙事務釋放B表,陷入了無限等待,也就是死鎖Dead Lock。

也有道友使用多執行緒來模擬儲存過程:http://www.jdon.com/45727,每個執行緒裡開啟一個事務,類似上述問題也會出現死鎖。

問題出在哪裡?

是我們的思路方向出現問題:

其實無論是使用資料庫鎖 還是多執行緒,這裡有一個共同思路,就是將資料餵給執行緒,就如同計算機是一套加工流水線,資料作為原材料投入這個流水線的開始,流水線出來後就是成品,這套模式的前提是資料是被動的,自身不復雜,沒有自身業務邏輯要求。適合大資料處理或網際網路網站應用等等。

但是如果資料自身要求有嚴格的一致性,也就是事務機制,資料就不能被動被加工,要讓資料自己有行為能力保護實現自己的一致性,就像孩子小的時候可以任由爸媽怎麼照顧關心都可以,但是如果孩子長大有自己的思想和要求,他就可能不喜歡被爸媽照顧,他要求自己通過行動實現自己的要求。

資料也是如此。

只有我們改變思路,讓資料自己有行為維護自己的一致性,才能真正安全實現真正的事務。

資料+行為=物件,有人問了,物件不是也要被執行緒呼叫嗎?

例如下述程式碼,因為物件的行為要被執行緒呼叫,我們要使用同步鎖synchronized :

public class A { 
        private volatile int lower, upper; //兩個狀態值
        public int getLower() { return lower; } 
        public int getUpper() { return upper; }
        public synchronized void setAUpper(int value){
             if (value < a.getUpper()) 
                    a.setLower(value);
        }

        public asynchronization void setALower(int value){
            if (value > a.getLower()) 
                   a.setUpper(value);
         }
   }
上面這段程式碼業務邏輯是想實現lower<upper:

1. lower和upper的初始值是(0, 5),
2.一個客戶端請求執行緒A: setLower(4) 
一個客戶端請求執行緒B: setUpper(3) 
3. lower和upper是 (4, 3) 

這個結果破壞了lower<upper這個邏輯一致性,所以,用鎖並不能保證邏輯一致性,而且還帶來了堵塞。鎖用錯了地方,不但沒有得到想要的,而且還失去更多。

下圖展示了鎖帶來堵塞,每個時刻只能允許一個執行緒工作,如同只能允許一個人蹲馬桶一樣。




從歷史上看,鎖的問題如鬼魂一直伴隨著我們:
1.用資料表一個欄位來表示狀態,比如1表示已付款未發貨,2表示已付款已發貨,然後使用者來一個請求用SQL或儲存過程修改,這時使用的資料庫鎖。

2.用ORM實現,比如Hibernate JPA來修改狀態,雖然不用SQL了,但是Hibernate的悲觀鎖和樂觀鎖也讓人抓狂。

3.徹底拋棄資料庫,直接在記憶體快取中進行修改,使用Java的同步鎖,效能還是不夠,吞吐量上不去。如上圖提示,只能一個廁所蹲位一個人用,其他人必須排隊。

4.Actor模型。

Actor模型原理
Actor模型=資料+行為+訊息。

Actor模型內部的狀態由自己的行為維護,外部執行緒不能直接呼叫物件的行為,必須通過訊息才能激發行為,這樣就保證Actor內部資料只有被自己修改。

Actor模型如何實現?

Scala或ErLang的程序信箱都是一種Actor模型,也有Java的專門的Actor模型,這裡是幾種Actor模型比較

明白了Actor模型原理,使用Disruptor這樣無鎖佇列也可以自己實現Actor模型,讓一個普通物件與外界的互動呼叫通過Disruptor訊息佇列實現,比如LMAX架構就是這樣實現高頻交易,從2009年成功執行至今,被Martin Fowler推崇。

回到本帖最初問題,如何使用Actor模型解決高併發事務呢?

轉賬是典型的符合該問題的案例,轉賬是將A帳號到B帳號轉賬,使用Actor模型解決如下:

發出是否可轉出訊息--->訊息佇列--->A

A作為一個物件,注意不是資料表,物件是有行為的,檢查自己餘額是否可轉賬,如果可以,凍結這部分金額,比如轉賬100元,凍結100元,從餘額中扣除。因為外部命令是通過訊息順序進來的,所以下一個訊息如果也是扣除,再次檢查餘額是否足夠......

具體詳細流程可見:REST和DDD

那麼,既然Actor模型如此巧妙,而解決方向與我們習慣的資料喂機器的方式如此不同,那麼如何在實戰中能明顯發現某個資料修改應該用Actor模型解決呢?因為我們習慣將資料喂機器的思路啊?

使用DDD領域驅動設計或CQRS架構就能明顯發現這些特殊情況,CQRS是讀寫分離,其中寫操作是應領域專家要求編寫的功能,在這類方向,我們都有必要使用Actor模型實現,因為在這個方向上,領域專家的要求都表達為聚合根實體,聚合根就是用Actor模型實現最合適不過了。而讀方向,比如大資料處理,報表查詢,OLTP等等都是資料喂機器的方式。

有的道友會疑問,我們經常使用SSH,也就是Spring + Hibernate架構,這個預設是哪種方向呢?很顯然,預設是資料喂機器的方向,所以在實現寫操作時,特別警惕高併發發生死鎖等影響效能問題,當然也包括EJB架構。

有一種togaf架構,將企業軟體架構分為資料架構和應用架構等,實際是EJB或SSH的變相描述,這種架構的問題我們已經一目瞭然了,特別這樣的系統如果從面向內部管理轉向到SaaS模型時,這類高併發死鎖問題就特別容易發生,幾乎不具備可用性。前期12306火車票系統是這類問題的典型體現。

5.CQRS

命令查詢的責任分離Command Query Responsibility Segregation (簡稱CQRS)模式是一種架構體系模式,能夠使改變模型的狀態的命令和模型狀態的查詢實現分離。這屬於DDD應用領域的一個模式,主要解決DDD在資料庫報表輸出上處理方式。

  Greg Young在infoQ的採訪中“State Transitions in Domain-Driven Design”談到了CQRS,Greg 解釋了把領域模型分為兩種:狀態校驗,以及狀態轉換,維持當前狀態的一個檢視。

  在客戶端就將資料的新增修改刪除等動作和查詢進行分離,前者稱為Command,走Command bus進入Domain對模型進行操作,而查詢則從另外一條路徑直接對資料進行操作,比如報表輸出等。

  當一個Command進來時,從倉儲Repository載入一個聚合aggregate物件群,然後執行其方法和行為。這樣,會激發聚合物件群產生一個事件,這個事件可以分發給倉儲Repository,或者分發給Event Bus事件匯流排,比如JavaEE的訊息匯流排等等。事件匯流排將再次啟用所有監聽本事件的處理者。當然一些處理者會執行其他聚合物件群的操作,包括資料庫的更新。

  因為領域物件操作和資料庫儲存持久這兩個動作分離,因此,資料表結構可以和領域物件鬆耦合(JiveJdon原始碼可展示領域物件和資料表不再是一對一對應依賴,這也是使用Hibernate 等ORM框架容易造成的問題),你可以優化資料表結構專門用於查詢。

  再者,由於事件驅動了領域模型的狀態改變,如果你記錄這些事件audit ,將可以將一些使用者操作進行回放,從而找到重要狀態改變的軌跡,而不是單純只能依靠資料表字段顯示當前狀態,至於這些當前狀態怎麼來的,你無法得知。當你從資料庫中獲得聚合體時,可以將相關的事件也取出來,這些叫Event Sourcing,事件源雖然沒有何時何地發生,但是可以清楚說明使用者操作的意圖。

  雖然這種架構有些複雜,但是好處卻很多,主要的是實現透明的分散式處理Transparent distributed processing,當使用事件作為狀態改變的引擎時,你可以通過實現多工併發處理,比如通過JVM平行計算或事件訊息匯流排機制,事件能夠很容易序列化,並在多個伺服器之間傳送,(EJB提倡貧血失血模型,實際就是為解決胖模型在多個伺服器之間傳送時序列化耗費效能,現在我們不序列化模型,而是改變模型資料的事件)。