事件溯源(Event Sourcing)是《CQRS》的常见搭档,核心思想一句话:不存"当前状态",存"发生了什么"的事件序列,当前状态靠重放事件得到。这篇讲清它解决什么问题、怎么工作、以及它为什么和 CQRS 一起出现。
一、解决什么问题:当前状态"丢失了历史"
传统做法只存"当前状态":
sql
-- 订单表只存当前状态
UPDATE orders SET status = 'PAID' WHERE id = 123;问题:订单"怎么一步步变成已支付的"这个过程丢了——你只知道现在是"已支付",但不知道什么时候下的单、谁支付的、中间有没有改过地址。
这在很多场景是致命的:
- 审计合规:金融系统必须知道"每一笔钱是怎么变的"
- 排查 bug:想知道"这个状态是怎么变成这样的",传统只能猜
- 回溯:想看"订单昨天的状态是什么",传统查不到
事件溯源解决的就是这个:把每个状态变化都当成一个"事件"记录下来。
二、事件溯源怎么工作
核心:不存状态,存事件序列;当前状态 = 重放事件得到。
java
// 传统:只存当前状态
orders 表:id=123, status=已支付
// 事件溯源:存"发生了什么"的事件流
order_events 表:
123 | ORDER_PLACED | {customerId, items, ...} // 下单
123 | ORDER_PAID | {amount, paidAt, ...} // 支付
123 | ORDER_SHIPPED | {logisticsNo, shippedAt, ...} // 发货
// 当前状态 = 依次重放所有事件
Order order = Order.replay(events); // 应用 下单→支付→发货,得到"已发货"关键点:
- 事件是不可变的:记录下来的事件永不修改,只追加(append-only)
- 状态是可推导的:当前状态 = 重放所有事件得到,不需要单独存
- 任何时刻的状态都能重建:想查"支付后、发货前"的状态,重放到"支付"事件就行
三、事件溯源 + CQRS(为什么总一起)
事件溯源和 CQRS 是黄金搭档,因为它们各管一半:
- 事件溯源管"写":写操作不直接改状态,而是追加事件
- CQRS 管"读":读的时候,从事件重放出状态(或读投影)
命令(下单/支付)→ 追加事件(Event Sourcing)→ 事件存储
↓ 重放/投影
查询 → 读模型(CQRS 的读库,由事件投影得到)→ 展示两者结合后:写 = 追加事件(不可变、可审计),读 = 从事件投影出状态(可优化)。这也解释了为什么 CQRS 和事件溯源总是一起出现。
四、优缺点
| 优点 | 说明 |
|---|---|
| 完整审计 | 每个变化都有记录,天然满足审计合规 |
| 可回溯 | 任意时间点的状态都能重建(重放到某事件) |
| 可调试 | 重放事件流,复现 bug 是怎么发生的 |
| 天然时序 | 事件有序,天然支持时序分析 |
| 缺点 | 说明 |
|---|---|
| 复杂度高 | 要处理事件版本、快照、投影重建 |
| 存储增长 | 事件比状态多,存储量大 |
| 学习成本 | 团队要理解"重放"思维,不是"改状态"思维 |
五、什么时候用
事件溯源不是银弹,适用场景很明确:
- 金融、交易、订单:必须审计、可回溯
- 需要完整历史:想查"任意时刻的状态"
- 配合 DDD/CQRS:复杂领域建模
不适合:简单 CRUD、不需要历史、纯配置类数据——存状态就够了,上事件溯源是杀鸡用牛刀。
小结
- 事件溯源不存"当前状态",存"发生了什么"的事件序列,状态靠重放得到
- 事件不可变、只追加,任何时刻的状态都能重建
- 和 CQRS 是搭档:事件溯源管写(追加事件),CQRS 管读(投影)
- 金融、审计、需回溯的场景才用,简单 CRUD 别硬上
到这里,"领域设计"这条线就完整了:DDD(怎么建模领域)→ CQRS(读写分离)→ 事件溯源(怎么记录变化),三者结合就是复杂业务系统的完整方案。想了解建模的起点,回头看《DDD 领域驱动设计》。
