Skip to content

事件溯源(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 领域驱动设计》。