Synchronizing the Commands and Queries Databases#
本模組討論 Commands 與 Queries 資料庫之間的同步機制。當我們在前一個模組中引入了獨立的 Read 資料庫後,兩個資料庫之間就需要一個同步策略,這種同步也稱為 Projection。
同步策略依觸發方式可分為兩大類:
- State-driven Projection – 由資料庫狀態驅動
- Synchronous(同步)
- Asynchronous(非同步)
- Event-driven Projection – 由 Domain Events 驅動
State-driven Projections#
State-driven Projection 的核心概念是在資料表中加入一個 flag(如 IsSyncRequired 或 IsDirty),每個 Aggregate 對應一個 flag。當 Aggregate 的任何資料被修改時,就將 flag 設為 true,背景同步程序會偵測到 flag 的變化,將資料同步到 Read 資料庫,然後重置 flag。
運作流程#
- 當 Student 或 Course 等 Aggregate 的資料被更新時,設定
IsSyncRequired = true - 背景同步程序偵測到 flag 變化
- 將變更同步到 Read 資料庫(新增或更新記錄)
- 重置 flag

每個 Aggregate(Student、Course)各自擁有 IsSyncRequired flag,用於追蹤同步狀態

State-driven Projection 流程:Commands 端設定 IsSyncRequired → 同步到 Queries 端 → 重置 flag
Synchronization 表的優化#
可以額外建立一張小型的 Synchronization 表,只包含一個 IsSyncRequired flag。當任何 Aggregate 的 flag 被設定時,這張表的 flag 也會被設定。這樣背景程序只需要輪詢這張小表,而不用頻繁查詢所有 Aggregate 的表,減輕資料庫壓力。
兩種實作 Flag 的方式#
方式一:Database Triggers
- 建立 Database Trigger 監控所有資料表的變更
- 自動更新對應的
IsSyncRequiredflag - 需要實作 Soft Delete(使用
IsDeletedflag 取代實際刪除)

Database Triggers 方式:Trigger 監控資料表變更,自動更新 IsSyncRequired flag,需實作 Soft Delete
方式二:在 Domain Model 中明確加入 Flag
- 在 Student 和 Course 等 Entity 中加入
isSyncRequired屬性 - 當任何屬性被修改時,顯式地設定 flag
public class Student implements Entity {
private String name;
private String email;
private boolean isSyncRequired;
public void removeEnrollment(Enrollment enrollment, String comment) {
enrollments.remove(enrollment);
Disenrollment disenrollment = new Disenrollment(
enrollment.getStudent(), enrollment.getCourse(), comment);
disenrollments.add(disenrollment);
isSyncRequired = true;
}
}可以使用 Hibernate 的 Event Listeners 或 Entity Framework 的 Change Tracker 來自動偵測帶有
IsSyncRequiredflag 的 Entity,並同步更新 Synchronization 表。
優先選擇在 Domain Model 中明確實作 flag 的方式。顯式的實作大幅提升程式碼的可維護性。只有在無法控制 Domain Model 原始碼時,才選擇 Database Trigger 的方式。
State-driven Projection 的優點#
- 直觀且容易使用
- 若需要重建 Read 資料庫,只需將所有記錄的 flag 設為 true
- 也可以選擇性地只重建部分記錄
Synchronous State-driven Projections#
前面描述的 State-driven Projection 是非同步的:應用程式更新 Commands 資料庫後,不等待同步完成就繼續運作,由背景程序負責同步。
Synchronous(同步)版本則是由應用程式本身執行同步,或等待背景程序完成同步後,才回應客戶端。
同步版本的特性#
- 應用程式自行完成 Projection
- 增加處理時間
- Read 與 Write Model 立即一致,消除使用者的困惑
同步版本的缺點#
- 很少在有兩個獨立資料庫時使用,因為它會顯著增加延遲
- 不具備擴展性:每新增一個 Read 資料庫副本,延遲就會增加,因為主應用程式必須更新所有 Read 資料庫
一般不建議使用同步版本。若需要立即一致性,考慮將 Read 和 Write Model 放在同一個資料庫中以減少延遲,或者使用非同步 Projection。
實際例子#
- Synchronous:Indexed Views – 關聯式資料庫的 Indexed View 會與來源表同步更新
- Asynchronous:Database Replication – 資料庫副本是非同步更新的
Event-driven Projections#
Event-driven Projection 的主要差異在於它不是由資料庫狀態驅動,而是由 Domain Events 驅動。同步程序訂閱 Command 端產生的 Domain Events,並利用這些事件中的資訊來更新 Read 資料庫。
優點#
- 擴展性極佳:應用程式只需將 Domain Events 發布到 Message Bus 上,可以有任意數量的 Projection 程序訂閱這些事件,不會對 Commands 資料庫產生額外壓力
缺點#
- 無法輕易重建 Read 資料庫:如果沒有儲存 Domain Events,就無法從 State 反推出 Events;一旦遺漏某個事件,Read 資料庫就會失去同步
- 若要將 Domain Events 與 State 一起儲存在 Commands 資料庫中,複雜度會大幅增加,此時可能應該直接轉向 Event Sourcing 架構
如何選擇 Projection 策略#
將 Projection 策略與持久化機制對齊:
- 沒有 Event Sourcing(只儲存最新狀態):使用 State-driven Projection
- 有 Event Sourcing(Domain Events 是一等公民):使用 Event-driven Projection,可透過重播所有事件來重建 Read 資料庫
Consistency#
擁有兩個資料庫而非一個,會在 Write 與 Read Model 之間引入延遲,可能導致使用者困惑。例如,註冊學生後,使用者可能無法立即在列表中看到該學生,誤以為操作失敗而重複註冊。
緩解一致性問題的方法#
- Uniqueness Constraints:在 Commands 資料庫上設定唯一性約束,即使使用者嘗試重複操作也會收到錯誤訊息
- Commands 資料庫始終立即一致:一致性問題只影響 Queries 資料庫
在 Command 執行期間如何查詢資料#
當需要在 Command 執行期間驗證資料(例如 Email 唯一性檢查)時:
- 不應使用 Query:因為 Queries 使用的資料庫可能尚未與 Commands 資料庫同步
- 應直接查詢 Commands 資料庫:使用 Repository 或 UnitOfWork 來查詢,因為 Commands 資料庫始終是最新的
Command 端的讀取操作(如
getStudentById、getCourseByName)是 Command 處理流程的一部分,其結果不會跨越應用程式邊界。這與 Query 的用途不同,不要將兩者混淆。
如果使用 Event Sourcing,則無法有效率地從 Commands 資料庫查詢當前狀態(因為儲存的是 Events 而非 State),此時不得不查詢 Read 資料庫,並處理由此帶來的一致性問題。
Eventual Consistency#
Eventual Consistency 是一種一致性模型,保證在沒有新的更新發生時,最終所有對該資料的存取都會回傳最後更新的值。
即時一致性是反直覺的#
現實世界本質上就是非同步且最終一致的。例如搬家後,駕照上的地址不會立即更新,需要去 DMV 換發。在此期間,駕照上的資訊是過時的。使用者在現實生活中已經習慣了 Eventual Consistency,因此在軟體中接受它也不會太困難。
提升使用者接受度的方法#
- 顯示友善訊息:例如「學生註冊已提交,將在短時間內處理完成」,設定正確的期望
- 本地更新 UI:提交後立即在使用者端更新畫面,等到下次重新整理時,Read 資料庫已經同步完成
- 雙向通訊:Read Model 收到更新後主動通知 UI 更新搜尋結果(需要維持連線,複雜度較高)
在大多數情況下,一個簡單的訊息提示或本地資料更新就足夠了。

緩解 Eventual Consistency 的 UI 策略:顯示提示訊息、本地更新記錄、雙向通訊
Versioning#
當基於過時資料做決策的成本很高時,Eventual Consistency 可能會有問題。但這種情況極少發生,典型例外是高頻股票交易。
對於其他大多數場景,可以透過 Versioning 搭配 Optimistic Concurrency Control 來緩解:
運作方式#
- 在 Read 和 Write 資料庫中都維護 Aggregate 的版本號
- UI 從 Read 資料庫查詢資料時,取得版本號(例如 Alice v1)
- UI 發送 Command 時,附帶該版本號
- Write Model 比對 Command 中的版本號與 Aggregate 當前版本號
- 若版本不同,拋出 Concurrency Error 並拒絕變更
- 通知使用者重新整理畫面以取得最新資料

Versioning 流程:UI 從 Queries 取得 Alice v1,發送 Command 時附帶版本號

Optimistic Concurrency Control:Commands 端檢查版本號,若不一致則拒絕變更
將版本號納入 UI、Command 和 Read Model 之間所有通訊的一部分,即可實現 Optimistic Concurrency Control。
CQRS 與 CAP Theorem#
CAP Theorem 指出分散式系統無法同時滿足以下三項保證中的兩項以上:
- Consistency:每次讀取都取得最新寫入的值或錯誤
- Availability:每個請求都會收到回應(不受部分節點故障影響)
- Partition Tolerance:即使節點間的訊息遺失或延遲,系統仍能繼續運作
CAP 的三種組合#
| 組合 | 特性 | 範例 |
|---|---|---|
| Consistency + Availability | 無 Partition Tolerance,單一伺服器 | 傳統關聯式資料庫 |
| Consistency + Partition Tolerance | 犧牲 Availability,需要鎖定整個系統等待同步 | Distributed Monolith(反模式) |
| Availability + Partition Tolerance | 犧牲 Consistency,節點間可能暫時不同步 | 大型分散式系統 |

CAP Theorem:Consistency、Availability、Partition Tolerance 三者最多只能同時滿足兩項
CQRS 如何在 CAP 中取得平衡#
CQRS 的優勢在於能對 Read 和 Write 做出不同的取捨:
- Write 端:選擇 Consistency + Availability,犧牲 Partition Tolerance
- 所有 Command Handler 在單一機器上運作,確保一致性
- Read 端:選擇 Availability + Partition Tolerance,犧牲完全一致性
- 可將 Read 擴展到多台機器,實現可擴展性
- 接受 Eventual Consistency
小結#
- Commands 與 Queries 資料庫之間的同步稱為 Projection,分為 State-driven 和 Event-driven 兩種策略
- State-driven Projection 透過在 Aggregate 表中加入
IsSyncRequiredflag 來觸發同步,實作方式包括 Database Trigger 和在 Domain Model 中顯式設定(優先選擇後者) - State-driven Projection 可以是同步或非同步的;同步版本不具擴展性,預設應選擇非同步
- Event-driven Projection 透過訂閱 Domain Events 來驅動同步,擴展性更好,但無法輕易重建 Read 資料庫
- 選擇 Projection 策略應與持久化機制對齊:無 Event Sourcing 用 State-driven,有 Event Sourcing 用 Event-driven
- Eventual Consistency 在實務中通常可被接受,可透過友善提示、本地 UI 更新和 Versioning(Optimistic Concurrency Control)來緩解
- CQRS 允許在 CAP Theorem 中對 Read 和 Write 做出不同取捨:Write 端重視一致性,Read 端重視可擴展性