前言
在微服務架構中編寫查詢具有挑戰性。查詢通常需要檢索分散在多個服務所擁有的資料庫中的資料,使用傳統的分散式查詢處理機制雖然在技術上可行,但會打破服務之間的隔離與封裝;
在微服務架構中實現查詢操作有兩種不同的模式:
- 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檢視的最終一致性;