從橫切到縱切,架構模式CQRS,提升系統進化能力

曾幾什麼時候,你是否疑惑於VO、PO、DTO、BO、POJO、Entity、MODEL的區別?html

你是否有過疑問,爲何Java裏有這麼多的以O爲名稱結尾的對象?!web

你是否也厭倦了編寫從這個O對象到那個O對象之間的轉換代碼?!數據庫

你有沒有想過,這一切的根源在哪裏呢?有沒有辦法解決這個問題呢?markdown

本文試圖給你答案!數據結構

分層架構的「原罪」

架構風格:萬金油CS與分層一文中提到,分層架構是個萬金油架構,當你沒法肯定該使用哪一種架構風格的時候,那麼能夠先使用分層架構。而實際上確實是這樣,大部分的應用都採用了分層架構,特別是web應用。架構

以最簡單的三層架構來講:異步

  • 展現層:展現數據給用戶
  • 邏輯層:處理業務邏輯
  • 持久層:持久化數據

每一層都負責各自的任務、職責單一,開發也就相對簡單。每一層相對獨立,因此都可以獨立進化,這是分層架構所宣稱的優點!也是其「原罪」!oop

分層架構雖然將系統按層進行劃分,可是層與層之間仍是須要進行交互的。交互就須要有接口或協議以及傳輸的數據。性能

對於外部調用,咱們可使用TCP、HTTP、RPC、WebService等方式來進行通訊;而對於內部交互來講,咱們能夠直接使用方法調用,使用接口來進行解耦。優化

可是傳輸的數據結構該如何定呢?

  • 第一種方式是直接使用基礎數據結構,好比Map?這有幾個問題:
  • 沒有代碼提示,包括IDE層面的提示以及業務層面的字段提示,手誤的概率較大。將編譯期的錯誤延後到了運行期,下降了開發效率
  • 沒有較完備的基礎設施,例如基於註解的字段校驗
  • 性能相對對象會差一點
  • 第二種方式是使用一個對象進行傳遞,例如ActiveRecord或者直接使用Model。可是這會使各層強耦合,使得分層架構的優點消失。因爲每層的進化速度不一樣:持久層相對比較穩定;邏輯層可能須要根據業務邏輯的不一樣而進行調整,例如打折策略;而展現層可能須要過一段時間調整,避免審美疲勞。其中一層對傳輸對象的調整均可能致使其它層跟着一塊兒修改。
  • 第三種方式就是上面說的使用各類傳輸對象:各層之間的數據傳輸使用獨立的傳輸對象,使得各層鬆耦合。可是增長了各類傳輸對象以及轉換代碼。同時轉換也消耗了部分性能。

各層的獨立進化,致使了交互的額外操做!這就是分層架構的「原罪」!也是須要這麼多傳輸對象的其中一個緣由!

而另一個緣由是表現力差別

再談表現力

領域設計:聚合與聚合根聊到了表現力問題,「數據設計」的表現力要弱於「對象設計」!相對應的,其實「數據展示」的表現力也是弱於「對象設計」的!

咱們仍是以訂單來舉例!假設我下單購買了多個商品,也就是說一個訂單包含了多個明細。那麼訂單與訂單明細的這層關係在「持久層」是經過主鍵來表現的:

從橫切到縱切,架構模式CQRS,提升系統進化能力

訂單明細包含了訂單的主鍵,表示哪些訂單明細是屬於哪一個訂單的。

而這層關係在「邏輯層」是經過對象引用來表現的:

從橫切到縱切,架構模式CQRS,提升系統進化能力

訂單對象中持有了指向訂單明細列表的引用。

而到了「展現層」,訂單和訂單詳情之間的關係就徹底靠展現方式來表現了:

從橫切到縱切,架構模式CQRS,提升系統進化能力

若是你不瞭解業務,光看代碼,是看不出訂單與訂單明細之間的關係的。上面只是純粹的展現了訂單明細在訂單信息的下面。

也就是說,當咱們訪問頁面的時候,請求從「持久層」將扁平的數據查詢到了「邏輯層」,組裝成告終構化的對象,最後被傳遞到了「展示層」,又被拍扁了展現在咱們面前

因爲每層表現形式的不一樣,亦致使了須要數據傳輸對象。

從橫切到縱切

既然橫向封層不可避免的須要數據傳輸對象來解耦各層之間的關係,那咱們是否不使用橫向封層,而使用縱向切分呢?這就是CQRS架構模式!

CQRS經過對系統進行縱向切分:將「數據讀」和「數據寫」分離開,使得數據讀寫獨立進化,來解決數據顯示覆雜性問題

CQRS架構以下:

從橫切到縱切,架構模式CQRS,提升系統進化能力

流程以下:

  • 客戶端構建命令對象CommandModel發送給服務端
  • 服務端經過命令總線CommandBus接收到命令,委託給對應的CommandHandler去處理
  • CommandHandler處理完業務,將此命令經過Repository進行持久化(不必定是DB,下面會具體說)
  • 同時會構建一個對應的事件Event,添加到事件總線EventBus中(該事件能夠是同步事件、也能夠是異步事件)
  • 對應的EventHandler會對該事件進行處理,好比處理成便於展現的模型,存儲到ReadDB中
  • 客戶端能夠對服務端發送查詢,服務端直接從ReadDB中獲取數據,構建QueryModel返回

這又什麼優點呢?

  • 首先,如今只須要CommandModel和QueryModel兩個數據傳輸對象,再也不須要那麼多的中間傳輸對象了。也就是說,省略了這部分的代碼和性能損耗。
  • 其次,讀寫分離,能夠對讀寫進行專門的優化。
  • 最後,就是能夠事件溯源EventSourcing。這個咱們來詳細說一下。

咱們以訂單保存和展現流程來詳細的看一下CQRS的優點!

對於普通分層架構來講,在保存訂單時須要一個DTO用於存儲相關信息,而後轉成多個對應的Model來進行持久化;而查詢訂單的時候,你須要查詢出多個Model,而後組裝成另外一個DTO來存儲查詢的信息,由於展現的時候可能要展現更多的信息,好比買家和賣家相關信息。

同時因爲數據都存儲在數據庫中,且表結構與Model是對應的,你能作的優化就是數據庫相關的優化手段。

而在CQRS中,數據庫被分紅了讀庫和寫庫。那存在讀庫中的數據結構就能夠徹底按照展現邏輯來優化,好比:我能夠有一張訂單展現表,表中包含了買家信息和賣家信息。在展現時,直接查詢這張表就能夠了,不須要和用戶表進行關聯查詢,提升了數據讀性能。

而對於數據持久化來講,就不須要考慮數據展現了,只要提升持久化性能就能夠了。例如不使用數據庫,而使用順序寫入的文件方式。同時也不必定要存儲數據自己,轉而存儲事件,就能夠實現事件重演,這就是事件溯源。

事件溯源

領域設計:Entity與VO一文中,提到了「狀態」!

通常咱們處理狀態都是直接去修改它,像下面這樣:

從橫切到縱切,架構模式CQRS,提升系統進化能力

那麼請問,這個開關剛纔經歷了什麼?!這是典型的ABA問題,即你只知道這個開關目前的狀態,可是它曾經有沒有開過或關過,你就無從得知了。

咱們對數據的處理也是這樣,你只知道當前存在數據庫中的數據是什麼,而它曾經被修改過沒有?被修改爲過什麼,你無從知曉。

由於咱們存的只是「即時狀態」,即「快照」!

事件溯源存儲的不是數據「快照」,而是「事件自己」!即它記錄了全部對該數據的事件。

若是你瞭解Redis的持久化方案,你對事件溯源就必定不會感到陌生。Redis有兩種持久化方式RDB方式和AOF方式:

  • RDB:在指定的時間間隔內,執行指定次數的寫操做,則會將內存中的數據寫入到磁盤中。對當前數據快照進行持久化
  • AOF:將指令追加到文件末尾。經過指令重演來恢復數據

咱們通常的持久化方式實際對應的就是Redis的RDB方式,而事件溯源就是AOF方式。

回到上圖,在CQRS中,WriteDB能夠經過類AOF的方式來存儲命令,也就是事件溯源。當須要對ReadDB中的數據進行恢復操做時,能夠經過命令重演的方式來恢復。

不過你應該發現問題了,命令重演的方式性能上有問題。因此咱們能夠參考Redis,使用快照+事件溯源的方式來存儲。即WriteDB中存儲事件,額外再定時對數據進行快照備份。恢復數據時先經過快照備份恢復,再從指定位置進行命令重演,來提升性能。

強一致性or最終一致性

讀寫分離後,致使的一個問題就是讀寫一致性。在原來的分層架構中,數據寫入後再讀取,是能夠當即讀取到寫入的數據的(事務保障)。

可是讀寫分離後,讀到的數據不必定是寫入的最新數據。通常狀況下,這個問題並不大。由於實際上你讀的基本上都是歷史數據!爲何這麼說呢?由於你無法保證數據在展示到你面前的過程當中,沒有新的寫入。除非展現是基於推送機制的。

可是對於特殊狀況下,可能不能容忍這樣的狀況。有幾種解決方案:

  • 臨時性的顯示先前提交給命令模型的參數
  • 在頁面展現查詢模型的時間
  • 使用相似Comet這樣的長連接的方式或者事件模式來監聽數據

參考資料

相關文章
相關標籤/搜索