前言
在微服務架構中編寫查詢具有挑戰性。查詢通常需要檢索分散在多個服務所擁有的數據庫中的數據,使用傳統的分布式查詢處理機制雖然在技術上可行,但會打破服務之間的隔離與封裝;
在微服務架構中實現查詢操作有兩種不同的模式:
- API組合模式:這是最簡單的方法,應盡可能使用。它的工作原理是讓擁有數據的服務的客戶端負責調用服務,並組合服務返回的查詢結果;
- 命令查詢職責隔離(CQRS)模式:它比API組合模式更強大,但也更復雜。它維護一個或多個視圖數據庫,其唯一的目的是支持查詢;
這是一本關於微服務架構設計方面的書,這是本人閱讀的學習筆記。下面對一些符號做些說明:
()為補充,一般是書本里的內容;
[]符號為筆者筆注;
1. 使用API組合模式進行查詢
1.1 findOrder()查詢操作
findOrder()查詢操作是從多個服務獲取數據的查詢方法;
圖解:基於微服務架構的FTGO應用程序版本中,數據分散在以下服務中;
- Order Service:基本訂單信息,包括詳細信息和狀態;
- Kitchen Service:從餐館的角度看訂單的狀態以及預計取餐時間;
- Delivery Service:訂單的交付狀態、預計送餐時間及送餐員的當前位置;
- Accounting Service:訂單的付款狀態;
1.2 什么是API組合模式
圖解:
- API組合模式:通過查詢每個服務的API並組合結果,實現從多個服務檢索數據的查詢;
- 其有兩種類型的參與者:
- API組合器:它通過查詢數據提供方的服務來實現查詢操作;
- 數據提供方服務:擁有查詢返回的部分數據的服務;
- 是否可以使用此模式實現特定查詢操作取決於幾個因素,包括數據的分區方式、擁有數據的服務公開的API的功能,以及服務使用數據庫的功能;
1.3 使用API組合模式實現findOrder()查詢操作
圖解:
- 四個提供方服務實現一個REST接口,該接口返回對應於單個聚合的響應;
1.4 設計問題一:由誰來擔任API組合器的角色
由服務的客戶端:
- 缺點:對於防火牆之外的客戶以及通過較慢網絡訪問的服務,此選擇不可用(詳情查看第八章);
由實現應用程序外部API的API Gateway:
- 可以使得在防火牆外運行的客戶端能夠通過單個API調用有效地從眾多服務中檢索數據;
將API組合器實現為獨立的服務
- 在外部訪問查詢時,由於其聚合邏輯過於復雜,因此無法在API Gateway中完成查詢,必須使用單獨的服務;
1.5 設計問題二:如何編寫有效的聚合邏輯
- 應該使用響應式編程模型;
- 有時API聚合器需要一個提供方服務的結果才能調用另一個服務;在這種情況下,它需要按順序調用一部分提供方服務;
- 應該使用基於Java CompletableFuture、RxJava可觀測或其他類似的響應式設計(詳情見《第8章 API Gateway模式》);
1.6 API組合模式的好處與弊端
好處:
- 實現查詢操作簡單直觀;
弊端:
- 增加了額外的開銷:涉及多個請求和數據庫查詢,需要更多的計算和網絡資源;
- 帶來可用性降低的風險:操作的可用性隨着所涉及的服務數量而下降;
- 解決辦法:在提供方不可用時,返回先前緩存的數據;或者讓API組合器返回不完整的數據;
- 缺乏事務數據一致性;
2. 使用CQRS模式
2.1 為什么要使用CQRS
- 涉及多個服務的查詢,API組合模式無法有效實現;
- 因為並非所有服務都存儲用於過濾或排序的屬性,如Order Service和Kitchen Service兩項服務存儲了Order的菜單項,而Delivery Service和Accounting Service都不存儲菜單項;
- 解決辦法:讓API組合器進行內存中連接;讓API組合器從Order Service和Kitchen Service檢索匹配的訂單,然后通過ID從其他服務請求訂單;
- CQRS可以解決服務的數據庫(數據模型)不能有效查詢的問題;
- 如:在進行地理空間查詢時會生成數據副本,CQRS解決了同步副本問題;
- CQRS考慮隔離問題的必要性,避免過多的職責導致過載服務;
2.2 CQRS隔離命令與查詢
圖解:
- 位於命令端的領域模型處理CRUD操作並映射到其自己的數據庫;
- 命令端在數據發生變化時發布領域事件;
2.3 CQRS和查詢專用服務
圖解:
- 查詢服務的API只包含查詢操作,並無命令操作;
- 它通過訂閱一個或多個其他服務發布的事件來確保它的數據庫是不斷更新的,並由此實現查詢操作;
- 查詢端服務訂閱由多個服務發布的事件;
2.4 CQRS的好處與弊端
好處:
- 在微服務架構中高效地實現查詢;
- 高效地實現多種不同的查詢類型;
- 在基於事件溯源技術的應用程序中實現查詢;
- 更進一步地實現問題隔離;
弊端:
- 更加復雜的架構:開發人員必須編寫更新和查詢視圖的查詢端服務;
- 處理數據復制導致的延遲;
- 即:更新聚合后查詢聚合會看到聚合的先前版本;
- 解決方案:采用命令端和查詢端API為客戶端提供版本信息,使其能夠判斷查詢端是否過時;
3. 設計CQRS視圖
CQRS視圖模塊包括由一個或多個查詢操作組成的API;
3.1 選擇視圖存儲庫
NoSQL:
- CQRS視圖受益於NoSQL數據庫更豐富的數據模型和性能,不受NoSQL數據庫事務處理能力的限制;
SQL數據庫:
- 在主流硬件上運行的現代關系型數據庫具有出色的性能;
- SQL數據庫通常具有非關系特征的擴展,如地理空間數據類型和查詢;
- CQRS視圖可能需要使用SQL數據庫才能支持報表引擎;
支持更新操作:
- 通常使用其主鍵更新或刪除視圖數據庫中的記錄;
- 有時需要使用類似外鍵的做法來更新或刪除記錄;
3.2 設計數據訪問模塊
事件處理程序和查詢API模塊不直接訪問數據庫存儲區。相反,它們使用數據訪問模塊,該模塊由數據訪問對象(DAO)及其輔助類組成;
- 處理並發更新確保更新冪等;
- 當視圖訂閱由多個聚合類型發布的事件時,多個事件處理程序可能同時更新同一記錄;
- 冪等事件處理程序:
- 為了確保可靠,事件處理程序必須記錄事件ID並以原子化的方式更新數據存儲區,如何試下取決於數據庫類型;
- 事件處理程序不需要記錄每個事件的ID,每個記錄僅需要存儲從給定聚合實例接收的max(eventId);
- 讓客戶端應用程序采用最終一致性的視圖:
- 執行更新命令后執行查詢命令可能看到的是更新前的數據 [有延遲],客戶端可以使用以下方法檢測這種不一致性:
- 命令端操作將包含已發布事件和ID標記返回給客戶端。然后,客戶端將事件有關的ID傳遞給查詢操作,如果該事件尚未更新視圖,則返回查詢錯誤;
- 視圖模塊可以使用重復事件檢測機制來實現這樣的功能;
3.3 添加和更新CQRS視圖
- 添加和更新CQRS視圖在概念上很簡單,即:
- 創建新視圖:開發查詢端模塊、設置數據存儲區並部署服務。查詢端模塊的事件處理程序處理所有事件,最終視圖將是最新的;
- 更新現有視圖:更改事件處理程序並從頭開始重構視圖;
- 但在實際中會產生一些問題,如下:
- 消息代理無法無限期存儲信息;
- 如:RabbitMQ會在消費者處理完消息后刪除該消息;Apache Kafka可在配置的保留期內保留消息,但也不會無限期存儲事件;
- 解決辦法:使用歸檔事件構建CQRS視圖,使用可擴展的大數據技術(如Apache Spark)實現;
- 處理所有事件所需的時間和資源隨時間推移而不斷增長;
- 解決辦法:增量式構建CQRS視圖,使用兩步增量算法。第一步基於先前的快照和自創建快照以來發生的事件,定期計算每個聚合實例的快照;第二步使用快照和任何后續事件創建視圖;
4. 實現基於AWS DynamoDB的CQRS視圖
介紹如何使用DynamoDB為findOrderHistory()操作實現CQRS視圖;
4.1 OrderHistoryService的設計
圖解:
- OrderHistoryEventHandlers:訂閱各種服務發布的事件並調用OrderHistoryDAO;
- OrderHistoryQueryAPI模塊:實現REST API接口;
- OrderHistoryDataAccess:包含OrderHistoryDAO,它定義了更新和查詢ftgo-order-history DynamoDB表及其輔助類的方法;
- ftgo-order-history DynamoDB表:存儲訂單的DynamoDB表;
4.2 OrderHistoryEventHandlers模塊
此模塊由接收事件和更新DynamoDB表的事件處理程序組成;
圖解:
- 每個事件處理程序都有一個DomainEventEnvelope類型的參數,其中包含事件和描述事件的一些元數據;
5. 本章小結
- 實現從多個服務檢索數據的查詢具有挑戰性,因為每個服務的數據都是私有的;
- 有兩種方法可以實現這些類型的查詢:API組合模式和命令查詢職責隔離(CQRS)模式;
- 從多個服務獲取數據的API組合模式是實現查詢的最簡單方法,應盡可能使用;
- API組合模式的局限性是某些復雜查詢需要大型數據集的低效內存連接;
- 使用視圖數據庫實現查詢的CQRS模式功能更強大,但實現起來更復雜;
- CQRS視圖模塊必須處理並發更新以及檢測和丟棄重復事件;
- CQRS有助於改善問題隔離,服務不必為自己擁有的數據實現查詢功能;
- 客戶必須處理CQRS視圖的最終一致性;
最后
