Files
cs-note/hhs/MQ/08-消息设计模式/29-MQ-CQRS-与-Event-Sourcing.md
T
2026-05-24 20:51:06 +08:00

175 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MQ, 消息队列, CQRS, Event Sourcing, 架构模式]
create time: 2026-05-24 19:52
---
# MQ CQRS 与 Event Sourcing
## 概述
CQRS(Command Query Responsibility Segregation)将读写模型分离,Event Sourcing 用事件流代替状态快照。两者结合并通过 MQ 广播事件,可以构建高性能、可审计、可追溯的系统架构。本文详解这两个模式的原理、组合方式、与传统 CRUD 的对比,以及事件存储的核心设计。
## 正文
### CQRS 概述
传统 CRUD 模式下,同一个数据模型既用于写入也用于查询。这在简单场景下工作良好,但随着业务复杂化,读写的性能需求和模型结构会逐渐分化。
CQRS 的核心思想:**Command(写)和 Query(读)使用不同的模型**。
- **写模型**(Command Side):专注于业务规则校验和状态变更,使用领域模型(Domain Model),可以高度规范化。
- **读模型**(Query Side):专注于查询性能,使用反规范化的视图模型(View Model),可以针对不同查询场景定制。
```mermaid
graph LR
Client["客户端"] --> CmdAPI["Command API"]
Client --> QueryAPI["Query API"]
CmdAPI --> WriteDB["写模型 (Domain)"]
WriteDB -->|"事件通过 MQ 广播"| MQ["Message Queue"]
MQ --> ReadModel1["读模型 A (列表视图)"]
MQ --> ReadModel2["读模型 B (统计视图)"]
ReadModel1 --> QueryAPI
ReadModel2 --> QueryAPI
style Client fill:#4A90D9,color:#fff
style CmdAPI fill:#F5A623,color:#fff
style QueryAPI fill:#6EC1E0,color:#fff
style WriteDB fill:#D0021B,color:#fff
style MQ fill:#F5A623,color:#fff
style ReadModel1 fill:#6EC1E0,color:#fff
style ReadModel2 fill:#6EC1E0,color:#fff
```
### Event Sourcing
传统方式存储数据的"当前状态"——每次更新都是覆盖写入。Event Sourcing 反其道而行:**不存储当前状态,存储所有导致状态变更的事件**。
以电商订单为例:
| 传统 CRUD | Event Sourcing |
|-----------|----------------|
| `UPDATE orders SET status='paid'` | 追加事件 `OrderPaid{orderId, amount, time}` |
| 只保留最终状态 | 保留完整的事件历史 |
| 无法追溯"为什么变成这样" | 可以重放任意时间点的状态 |
Event Sourcing 的优势:
- **完整审计追踪**:每个状态变更都有记录,满足金融、医疗等合规要求。
- **时间旅行**:通过重放事件,可以重建任意历史时刻的状态。
- **调试友好**:出 bug 时可以精确回放复现问题。
Event Sourcing 的挑战:
- **事件版本兼容**:事件 Schema 演进时需要保证新旧版本兼容。
- **查询复杂**:获取当前状态需要重放所有事件(可用 Snapshot 优化)。
- **最终一致性**:读模型通过 MQ 异步更新,存在短暂延迟。
> [!question]
> Event Sourcing 让历史可追溯,但也带来事件版本兼容的挑战。你会如何处理事件 Schema 的演进?
### CQRS + Event Sourcing 的组合
CQRS 和 Event Sourcing 天然互补:
1. **Command Side** 采用 Event Sourcing,将状态变更以事件形式追加到 Event Store。
2. **Event Store** 通过 MQ 将事件广播给所有关心的消费者。
3. **Query Side**(Read Model)消费事件,构建针对查询优化的视图。
这种架构下,MQ 是连接读写两侧的桥梁——写入端产生事件,MQ 负责分发,读取端消费事件并更新视图。
### 与传统 CRUD 的对比
| 维度 | 传统 CRUD | CQRS + Event Sourcing |
|------|-----------|----------------------|
| 数据一致性 | 强一致(同一数据库) | 最终一致(读模型异步更新) |
| 查询性能 | 受限于写模型结构 | 读模型可针对查询优化 |
| 审计追踪 | 需要额外设计审计表 | 天然支持,事件即审计日志 |
| 扩展性 | 读写耦合,难以独立扩展 | 读写独立扩展 |
| 复杂度 | 低 | 高(事件设计、版本管理、最终一致性) |
| 适用场景 | 简单 CRUD 应用 | 复杂业务、高并发读、审计要求高 |
> [!question]
> CQRS 带来最终一致性,用户下单后立刻查询可能看不到最新状态。在你的业务场景中,这种延迟可以接受吗?如何在 UX 层面缓解?
### 事件存储设计
#### 事件版本
事件一旦写入就不可修改(Immutable),但业务会演进。处理方式:
- **Upcast**:读取旧版本事件时,在内存中转换为新版本。
- **多版本共存**:事件中携带版本号,消费者根据版本号分别处理。
#### 快照(Snapshot)
当事件数量很大时,每次重放全部事件来获取当前状态会很慢。Snapshot 在特定时间点保存状态快照,重放时只需从最近的 Snapshot 开始。
#### 事件重放
重放是 Event Sourcing 的核心能力——从头(或从 Snapshot)依次应用事件,重建状态。读模型的重建、bug 修复后的数据修复、新读模型的初始化,都依赖事件重放。
### Go 代码:简化版 Event Sourcing
```go
package main
import "time"
// Event 事件定义:每个事件代表一次状态变更
type Event struct {
ID string
AggregateID string // 聚合根 ID(如订单 ID)
Type string // 事件类型
Data []byte // 事件数据
Version int // 事件版本号
Timestamp time.Time
}
// EventStore 事件存储接口
type EventStore interface {
Save(events []Event) error
Load(aggregateID string) ([]Event, error)
}
// OrderAggregate 订单聚合根
type OrderAggregate struct {
ID string
Status string
Amount float64
}
// Apply 将单个事件应用到聚合根,更新状态
func (o *OrderAggregate) Apply(event Event) {
switch event.Type {
case "OrderCreated":
o.Status = "created"
case "OrderPaid":
o.Status = "paid"
case "OrderShipped":
o.Status = "shipped"
}
}
// ReplayEvents 从事件流重建聚合根状态
func ReplayEvents(aggregateID string, store EventStore) (*OrderAggregate, error) {
events, err := store.Load(aggregateID)
if err != nil {
return nil, err
}
agg := &OrderAggregate{ID: aggregateID}
for _, event := range events {
agg.Apply(event) // 依次应用每个事件
}
return agg, nil
}
```
核心逻辑:`ReplayEvents` 从 Event Store 加载指定聚合根的所有事件,依次调用 `Apply` 重建当前状态。这就是 Event Sourcing 的精髓——状态是事件的函数,`State = f(events)`。
## 关联笔记
- [[02-消息模型/3-MQ-消息模型|MQ 消息模型]]
- [[09-流处理与事件驱动/34-事件驱动架构-EDA|事件驱动架构 EDA]]
- [[12-架构与实战/47-MQ-与微服务|MQ 与微服务]]
- [[06-高级特性/20-MQ-Schema-管理与演进|MQ Schema 管理与演进]]
- [[12-架构与实战/46-MQ-分布式事务实践|MQ 分布式事务实践]]