175 lines
6.7 KiB
Markdown
175 lines
6.7 KiB
Markdown
---
|
||
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 分布式事务实践]]
|