微服務架構中實施CQRS失敗的四個重大原因,引以為戒!
CQRS(命令查詢職責分離)是一種在復雜商業應用中非常有用的模式,特別是當讀操作和寫操作有不同需求時。舉個例子,寫操作可能想要在關系型數據庫中以規范化形式維護一個模型,而讀操作則可以將模型表現為文檔數據庫中的文檔。但是理解CQRS并不容易。它涉及到讀操作、寫操作、事件、命令、領域驅動設計(DDD)、事件溯源以及最終一致性等概念。實現CQRS的常見方式是創建兩個服務,并通過事件進行通信。
我們的CQRS實現
為了將CQRS集成到我們的自定義框架中,我們使用了Axon框架。因為Axon是最容易使用的,并且對Spring Boot框架有很好的支持。架構圖如下所示:
我們為寫入和讀取創建了兩個單獨的服務。這兩個服務通過 RabbitMQ 連接。
寫入服務
寫入服務處理所有的更新操作。我們并沒有試圖說服開發人員認為寫請求就是真正的命令。控制器負責從請求中創建命令,并通過命令網關發布命令。控制器與命令處理程序之間的通信是異步的。命令處理程序通過加載聚合根,執行業務邏輯,然后將事件發布到RabbitMQ來處理命令。與此同時,事件也被保存到事件存儲中。在這個實現中,事件存儲充當我們的單一真相來源。
讀取服務
讀取服務處理所有讀取請求。這里沒有應用任何業務邏輯。通過監聽發布到 RabbitMQ 的事件來更新數據。開發人員可以選擇任何關系數據庫或 MongoDB 作為讀取數據庫。我們希望開發人員實現的是讀取優化的模型。它們可以存儲數據而不進行任何規范化以提高讀取性能。
這是一個非常簡單的實現。我們將讀取和寫入分為兩個服務,那么我們為什么失敗了呢?
為什么我們失敗了?
1. 異步性
當我們引入CQRS時,開發人員的主要抱怨是“我們將如何更新UI,我們不知道記錄是否成功保存”。這是一個非常合理的問題,它涉及到我們所熟悉的同步通信方式。這種方式很好,也是我們從一開始就習慣使用的編碼方式。因為它易于推理。我們可以得到請求的完整流程。
那么為什么我們在事件驅動架構(EDA)中使用異步調用呢?很簡單,就是為了提高響應能力。EDA 與異步通信無關。我們可以在不使用異步通信的情況下實現 CQRS。
2.工作量大
第二個主要抱怨是“完成基于 CQRS 的微服務需要兩倍的工作量。“ 開發人員必須創建兩項服務:命令服務和讀取服務。但這就是我們實施 CQRS 的方式。
3.對所有服務使用 CQRS
我們告訴開發人員的是在每個微服務中使用 CQRS。我們認為整個系統應該是一個CQRS系統。其結果是引入了難以管理的復雜性,這是我們在實施 CQRS 時犯的主要錯誤之一。
4.較小的微服務
我們不能說微服務應該有多小,但我們可以確定它的邊界。我們可以使用DDD和限界上下文等概念來識別微服務邊界。在 CQRS 中,我們使用聚合的概念。聚合是 邏輯上相關的域對象。例如訂單和訂單項目。我們將訂單和訂單項目視為一件事。當我們保存訂單時,會將其與訂單項目一起保存。當我們無法識別聚合時,最終會將其拆分為單獨的微服務。這給 CQRS 增加了巨大的復雜性。當收到保存訂單的請求時,要如何保存訂單項目?
更小的微服務意味著更多的微服務。由于我們將 CQRS 應用于所有服務,最終使服務數量增加了一倍。這是無法控制的。
總結
這次失敗的主要原因是因為錯誤的實現,并且大多數與 CQRS 無關。我們從中學到的是,必須理解微服務和 CQRS 的關鍵概念,然后才能將它們整合好。