CQRS模式學習

大雄45發表於2022-12-22
導讀 由於存在增刪改與查詢邏輯有差異的這個問題,為了更好的針對差異進行抽象,我們可以將它們分開進行設計。也就是我們的CQRS模式,即 查詢的責任分離Command Query Responsibility Segregation模式。
簡單的需求

當我們系統中的資料模型層級較少時,資料模型足夠簡單時,模型與資料庫可以直接進行對映。這種簡單資料模型使我們不需要針對其相互關係進行復雜的建模設計,直接在工程中使用經典的三層模型就足以支撐專案需求。

對於這種簡單系統,過度設計會增加後續維護、重構的成本(並不能保證預設計能完美符合後續需求)。同時,對於簡單系統,我們大部分的需求都只涉及其中的少量資料模型邏輯處理。

而我們直接對資料模型進行CURD就能滿足需求,進而的結論就是:

針對簡單需求,我們不需要特別區別查詢和增刪改的程式結構。

複雜的需求

如果我們的系統具有一定複雜性,這種複雜性可能是源於訪問頻次、資料量或者是資料模型數量。這時候我們遇到的問題是資料在查詢和更新的需求差距逐漸變大。

  1. 頻次:資料的查詢頻次會遠高於新增、更新、刪除頻次。
  1. 資料量:資料量變大後會增加對資料進行分庫分表的設計訴求,從而導致資料查詢變得的複雜性(涉及分表關鍵字)。
  1. 資料模型數量:資料模型數量的增大,會導致在進行新增、更新與刪除操作時同時影響的資料模型變多,而在查詢時同時跨多模型的查詢條件會讓查詢的效能具有極大的挑戰性。

根據以上舉例我們可以發現,當我們的需求具有一定的複雜性後,根據引入複雜性的不同,會導致系統功能上需要用更加複雜的設計來對需求的複雜性進行支撐。同時我們也可以發現,引入的不同複雜性在增刪改和查詢方面的帶來的功能需求差別很大。

所以:

需求的複雜性會放大程式中查詢和增刪改的設計差異。

DDD的需求

如果我們對系統整體的構建與設計有了更高的可維護性與可擴充套件性要求,以至於我們需要使用DDD來設計整個系統。

在這種情況下往往模型中具有相對複雜的模型關係,在增刪改時我們需要將所有請求封裝為領域物件,以便程式可以基於領域模型完成大量複雜的校驗、業務邏輯。而在查詢需求時,我們常常需要組織跨領域資料來完成一個列表中資料內容的展示。所以:

在DDD設計中,增刪改操作便於應用領域模型執行,而查詢操作往往無法直接透過領域模型執行。

CQRS模式

根據第一節中的內容我們可以發現,在進行系統架構設計時,當系統出現複雜性後存在一個核心問題:

增刪改型別的功能與查詢型別的功能,在功能需求上具有較大的差異。
這種差異帶來的直接結果就是在系統開發的過程中,針對增刪改和查詢操作的業務設計上差異會比較大。如果舉幾個例子來說的話,比如:

  1. 針對增刪改系統我們需要事務來保證多領域模型的更新原子性;針對查詢我們需要增加快取來提高熱點資料的查詢效能。
  1. 資料讀取和寫入的模型通常是不匹配的,他們維護和查詢的列或者屬性坑沒有交集。
  1. 在更新的時候查詢資料可能會產生衝突。
  1. 使用統一模型進行儲存可能會導致複雜查詢時的效能降低。
CQRS本質

由於存在增刪改與查詢邏輯有差異的這個問題,為了更好的針對差異進行抽象,我們可以將它們分開進行設計。也就是我們的CQRS模式,即 查詢的責任分離Command Query Responsibility Segregation模式。其中我們稱增刪改為命令型操作。

CQRS本質上是一種讀寫分離設計思想,這種框架設計模式將命令型業務和查詢型業務分開單獨處理。透過這種方式,CQRS可以針對命令和查詢單獨進行業務模型上的設計,從而用更加適合各自場景的方案與元件來提供能力。

查詢

查詢操作並不會修改資料庫中的內容,所以查詢本身是一種冪等操作,以同一個查詢條件在系統不改變的情況下反覆執行會返回相同的結果,我們可以針對這種特性提供資料快取來提高系統效能;同時因為不影響資料庫,查詢邏輯是不會產生資料一致性問題。查詢往往會存在較高的使用頻率。

命令操作會直接修改資料庫,並針對多個領域模型的情況下我們需要增加來保證操作的原子性。而對於一個命令操作,我們往往是不直接依賴命令的返回值的,所以通常可以非同步執行命令操作。對於一般系統來說,往往命令操作的使用頻次會較低。

簡單實用

由於CQRS的本質是對於讀寫操作的分離,所以比較簡單的CQRS的做法是:

CQ兩端資料庫表共享,CQ兩端只是在上層程式碼上分離。
這種做法在不對資料庫進行分離設計的情況下,CQ兩端在上層程式碼進行分離個字單獨維護,例如命令型的都用xxxManagerController、xxxManagerService來定義,而查詢則直接用xxxController、xxxService定義。

CQRS模式學習CQRS模式學習

因為使用同一個資料庫,所以沒有CQ兩端的資料一致性問題。但因為已經對上層程式碼進行了抽離,所以可以滿足一些設計特性如:

  1. 命令應基於任務,而不是以資料為中心。
  1. 命令可以放置在佇列上進行非同步處理,而不是同步處理。
  1. 查詢從不修改資料庫。 查詢返回的 DTO 不封裝任何域知識。

這種方案可以滿足程式碼邏輯上的分離維護,但由於是使用同一資料庫表,所以無法根據CQ兩種業務的特點單獨進行模型設計。

關注效能

在程式碼分離的基礎上,我們可以再將資料儲存的模型進行物理分離,讀取儲存可以是寫入儲存的只讀副本,使用多個只讀副本可以提高查詢效能;也可能為讀取模型單獨設計庫表。單獨對查詢和更新進行模型設計可以減小設計和實現的難度。並且此時讀取資料庫可使用自己的已針對查詢進行最佳化的資料架構。比如讀資料庫可以直接儲存查詢資料寬表從而避免進行join操作或者複雜的查詢對映。甚至可以針對讀取操作使用mongo或者es等nosql資料庫對查詢邏輯進行增強。

CQRS模式學習CQRS模式學習

分離後的資料將存在在不同的資料庫中,Q的資料由C端同步過來。通常,這是透過在每次更新資料庫時使寫入模型釋出事件來實現的。 而說到資料同步則就有同步執行和非同步執行兩種方案:

  1. 同步:導致效能降低,但是可以保證資料的強一致性。
  1. 非同步:擁有較高的效能,但需要系統接受最終一致性的。

同樣的,這種同步也可以解釋為對快取進行的更新,即:查詢資料庫是使用快取,而寫入資料庫使用普通MySQL,兩者之間資料同步透過領域事件實現最終一致性。

進一步強化

進一步的,由於命令操作實際上是對“操作”進行的記錄,而只有查詢才需要將所有的操作進行彙總展示。基於這種思想,可以使用事件溯源EventSourcing模式來進行命令操作的記錄。在這種方案下,儲存記錄時更新的不是當前的記錄,而是會導致狀態變化的事件日誌,每個事件表示對資料所作的一系列更改,而我們可以透過重播事件構造資料當前的狀態(可以參考Mysql的Binlog設計)。這種記錄的優點是可以根據回放,重現每一次狀態變更的時間點以及變更軌跡。而查詢則可以根據當前狀態的快照來為查詢提速。來自於網路的架構圖:

CQRS模式學習CQRS模式學習

這種設計模式聽起來就比較複雜,但是卻有很多好處,例如:實現透明的分散式處理,當使用事件作為狀態改變的引擎時,你可以透過實現多工併發處理,比如透過JVM平行計算或事件訊息匯流排機制,事件能夠很容易序列化,並在多個伺服器之間傳送。同時因為是保留的操作記錄,可以在回放的時候對於異常運算元據進行過濾,從而增加了資料的魯棒性。

使用挑戰

如果希望使用CQRS,根據你希望實現的系統效能,你需要評估當前系統架構以及個人經驗是否有以下能力:

  1. 複雜性設計:儘管CQRS基礎理念較為容易理解,但是這種模式會導致系統的構建複雜度上升,尤其是進一步使用事件溯源模式時。
  1. 訊息佇列處理:在進行高效能設計的時候,通常會使用訊息處理命令和釋出更新事件。在此情況下,應用程式必須處理訊息失敗或重複的訊息。
  1. 最終一致性:如果分離讀取和寫入資料庫,讀取資料可能會過時。 必須更新讀取模型儲存,以反映對寫入模型儲存區所做的更改,並且在使用者根據過時的讀取資料發出請求時,可能很難檢測到這種情況。
選型建議

對於以下場景不建議引入CQRS:

  1. 領域或者業務十分簡單。
  1. 基本的CRUD就可以支撐完整的系統資料訪問需求。

如果系統存在一定的複雜性,並且有以下的特點,則可以根據特點,選擇適合的CQRS實現方式。

  1. 在使用者操作中,需要在使用者介面中進行一系列的複雜操作來最終定義、組裝、修改領域模型。寫模型需要有完成的命令處理堆疊,包括:輸入驗證、業務處理、業務驗證。而讀模型只需要返回檢視中所用到的DTO資料。讀模型與寫模型只需要最終一致性關係。
  1. 對於使用者的操作訪問,需要以較小的粒度定義命令,並透過合併命令的方式避免命令衝突。
  1. 資料寫入和資料讀取之前存在比較大的效能區別,需要分開進行資料最佳化。尤其是讀取次數遠大於寫入次數的場景,可以對讀模型進行水平擴充套件。
  1. 當團隊人員可以分拆分,組成專門針對複雜業務寫場景的組,以及專門針對高頻查詢和使用者介面的組。
  1. 當系統隨時間不斷演進,不斷包含多個版本的模型,或者業務規則會定期修改。可以在寫模式中包含多個版本的模型,而讀模式中使用統一的檢視模型。
  1. 與其他系統整合時,希望不會受到其他系統故障的影響(讀寫庫表分離)。
最後

總的來說,CQRS是處理複雜問題的一種具體實現方案,常用於配合DDD使用。

總結CQRS 的主要優點包括:

  1. 獨立縮放:CQRS 允許讀取和寫入工作負載獨立縮放,這可能會減少鎖爭用。
  1. 最佳化的資料架構: 讀取端可使用針對查詢最佳化的架構,寫入端可使用針對更新最佳化的架構。
  1. 安全性:更輕鬆地確保僅正確的域實體對資料執行寫入操作。
  1. 關注點分離:分離讀取和寫入端可使模型更易維護且更靈活。 大多數複雜的業務邏輯被分到寫模型。 讀模型會變得相對簡單。
  1. 查詢更簡單:透過將具體化檢視儲存在讀取資料庫中,應用程式可在查詢時避免複雜聯接。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2883741/,如需轉載,請註明出處,否則將追究法律責任。

相關文章