淺談 CQRS 的實現方法

微服務架構設計基本原理

Fred Chien(錢逢祥)
Brobridge - 寬橋微服務
7 min readFeb 25, 2020

--

Brobridge 過去在指導客戶進行微服務架構設計時,總不免講到「命令與查詢責任分離(CQRS, Command Query Responsibility Segregation)」,這個詞因為對許多人來說很陌生,通常又被人簡化理解為「讀寫分離」。在微服務架構的領域中,CQRS 主要被用於解決資料庫效能問題、讓實現資料分散化的目標。當然,一涉及分散式架構,其中就帶來更多議題,如:如資料一致性、交易處理等。

由於 CQRS 的概念在分散式系統,以及現今的微服務架構下被廣泛應用,小從資料儲存,大到整個服務應用系統層面都可以看到其存在,而且使用方法和層面非常多元且複雜,往往我們很難單從一些表面上的名詞和概念就能完全理解 CQRS 所帶來的好處,和對於微服務架構的重要性。‌

有鑑於此,本文將分成「資料庫系統」以及「應用程式」兩個角度,來討論 CQRS 模式在實務上的應用方法,也試著利用簡單的方式說明實務面我們如何採用這樣的設計模式。

什麼是 CQRS?

這個概念來自於物件導向的「命令與查詢分離(CQS, Command Query Separation)」,出自於 1987 年 Betrand Meyer 的 Object-Oriented Software Construction(物件導向軟體建構)一書,其原始概念是我們可以把物件操作分為命令(Command)和查詢(Query)兩種形式。

而這兩種操作的行為分別為:

  • 命令(Command):執行後會改變物件狀態
  • 查詢(Query):查看物件結果,而且不會改變物件的狀態、對物件本身沒有副作用

類似的概念,我們可以應用在分散式架構或微服務架構之下,只是我們不討論物件的行為,取而代之的是服務的行為,在實務上,此概念主要是用來解決資料操作的效率、服務責任的劃分,以及分散式系統下利用「重複性」來提升系統整體效能的問題。

如果你閱讀過我們的另一篇文章:「淺談微服務拆分原理」,裡面對於服務進行業務拆分的部分,圖上 Action 所表示的「 Commands/Query 」指的就是 CQRS 當中的命令和查詢。

資料庫系統的 CQRS 實現

以往在討論服務系統設計時,因為資料庫總是會遭遇到效能瓶頸,就會嘗試使用「讀寫分離」來解決問題,所以這樣的做法早就被大量使用。資料儲存層面的 CQRS,一直都是最直覺也最常見的應用場景,主要被使用在資料庫系統上。‌

而多數人在採用 CQRS 模式時,第一時間都是在資料庫系統上使用,除了直覺以外,這也是在單體式應用程式中,以及三層式架構下,對於資料庫最終的效能改善手段。

資料庫的讀寫分離

資料庫複寫 (Database Replication)

資料庫複寫的主要實現方式,是透過複寫(Replication)機制,通常每一家資料庫系統都有各自的解決方案,被歸類在叢集的設計範疇之中。主要的運行方式,依賴著一些較低階的系統工具(多半是官方所提供或是一些第三方的工具),從資料庫系統底層發現資料被變更(新增、修改、刪除),就會將資料同步到其他的資料儲存區。

資料變更攔截 (CDC, Change Data Capture)

使用資料庫系統原生或系統層的工具,通常重心放在「實體資料一致性」的問題上,只考慮資料是否正確的被同步在不同的資料儲存區,而不考慮實際應用上的需求。所以近年來也出現一些利用資料變更截取(CDC, Change Data Capture)的技術,所實現的解決方案,例如:

  • Yelp — MySQL Streamer
  • LinkedIn — Databus
  • Zendesk — Maxwell

‌這類 CDC 技術主要是去掃描或監聽資料庫日誌(Logs),發現資料變更時,即開始進行資料同步工作。

應用程式的 CQRS 實現

在微服務架構和分散式系統的設計上,CQRS 的應用範圍更為廣泛和多變,不單只是用於資料儲存層面的處理,同時結合服務應用層面的設計後,有更多元的使用情境。此外,在設計服務系統架構時,為了能擴展系統並實現分散式架構,往往重複性大於重用性,此時 CQRS 便是其中不可或缺的整合模式。

應用程式層級對 CQRS 的主要需求,在於解決「業務」上的資料需求,例如交易機制、複雜資料的關聯式查詢,這類難以利用資料庫層面的解決方案來實現,只能藉由針對業務改造的服務來達成。

服務層級的 CQRS

從 CDC 輸出事件以驅動分散式的資料處理

CDC 的做法原本是用於資料庫系統的資料同步,但我們可以將 CDC 的事件直接輸出給服務使用。當資料變更的狀態變成事件化,就不再只是資料庫系統內部的複寫機制,應用場景也會變得比較多元,不限於資料庫的同步,也包括應用層面的整合,實現事件驅動(Event-driven)的設計。甚至,整合微服務系統架構的設計時,可以採用「資料部分同步」的做法搭配服務拆分,實現分散式架構的彈性,甚至提升特定需求的存取效能。

這種方式混合了資料庫系統層級和應用程式服務層的實現,對於原本就有導入 CDC 的人來說,可以在不改動事件發起源的應用程式前提之下,實現事件驅動的 CQRS 實作。

事件驅動的分散式資料處理

運用事件驅動的資料流,達成資料同步與資料庫系統的 CDC 做法雷同,每當資料變更時,即進行資料的同步工作。但較為不一樣的是,事件是由「應用程式」所發動,不是由 CDC 機制所發動,而且事件本身與業務通常有直接相關性。換句話說,資料的同步是因為「領域事件」所觸發,而「不是因為資料變更而觸發同步」。

實務上,服務間的資料流存在的目的,多半並不是單純進行資料一對一的複製同步而已,而是可以因應事件需求,去觸發資料處理或相關任務機制,甚至是忽略並丟棄不需要的資料。

應用程式拋出領域事件跟其他服務同步資料

因為同一筆資料或一則命令,可以因應需求而觸發產生多個不同的事件,因此,使用服務層級的事件驅動來實現 CQRS,通常會比資料庫系統的做法更為彈性。

這種服務層級的實現,大概可以用於三種場景:

  1. 解決資料一致性問題,同步資料庫(與 CDC 同)
  2. 優化關聯式查詢的效能
  3. 在即時資料流架構下,同時實現資料的分類和處理

若要自己實作事件驅動,可以在命令(Command) 發動源拋出事件,讓所有對該事件有興趣的服務,接手後續任務,例如儲存資料、分類資料。通常這樣的實現,會搭配訊息佇列系統(Message Queuing System),若要更嚴謹穩定,則會落實 Event Sourcing 來搭配支援日誌機制(Log-based) 的佇列系統,如:Kafka、NATS 等。

後記

CQRS 帶來的議題相當多,其中技術涉及到資料庫設計、訊息佇列,若更深入研究,則會和微服務拆分息息相關,有更多的內容可以討論。本文只是就一些簡單的場景,說明 CQRS 在實務上的一些應用方法。

此外,CQRS 本質上就是空間換取時間,利用重複性來減少重用性,以提升整體系統的效能。而分散式設計的缺點當然會使系統架構看起來變得複雜、零碎,唯一能串起整個系統的就是依靠事件(Event),所以事件驅動(Event-driven)便是另一個需要深度討論的議題。

最後,如果您對「微服務架構設計」以及「CQRS」的議題仍感到疑惑,可以與我們 寬橋(Brobridge) 聯絡,取得專業的教育訓練和顧問服務。

--

--