跳到内容

第 6 卷 / 第 15 章 / 第 3 课

15.3 CQRS 与 Event Sourcing:分离模型和保存事实 ​

订单系统既要快速查询当前状态,又要解释状态为何变成今天这样,团队因此提出拆分读写模型并保留事件历史。

CQRS 与 Event Sourcing 经常一起出现,但它们解决不同问题:CQRS 分离写入模型和读取模型;Event Sourcing 把领域事件序列作为状态的权威记录。可以只用其中一个,也可以都不用。

CQRS:读写使用不同模型 ​

写模型保护不变量,通常按聚合和命令组织;读模型为具体查询预先组合数据,通常按页面、报表或 API 响应组织。

text
Command → Write Model → Database
                         │
                         └─ events / CDC → Projection → Read Store → Query

CQRS 不要求两个数据库。最轻量的实现可以在同一个数据库中使用不同的对象模型或视图;只有负载、权限、部署或存储特性确有差异时,才分离存储。

上图采用异步投影,需要明确读延迟;若在同一事务中同步维护读模型,则没有这段异步等待。异步情况下,命令成功并不保证查询投影已更新,客户端可能需要:

  • 接受短暂旧数据并显示更新时间;
  • 根据写模型返回关键结果;
  • 使用版本 token 等待投影追上;
  • 对“读己之写”提供专门路径。

Event Sourcing:状态来自事件折叠 ​

传统持久化保存当前状态;Event Sourcing 保存导致当前状态的领域事实:

text
EntryRequested(v1)
SeatReserved(v2)
PaymentAuthorized(v3)
RegistrationConfirmed(v4)

下面是项目中的 Java 方法,依赖领域类型 Registration 和 RegistrationEvent。empty() 返回初始状态;apply() 必须验证事件属于同一报名、版本连续且转换合法,并返回应用后的状态。方法本身只负责按输入顺序折叠,不会替调用者排序或查库:

java
Registration rehydrate(List<RegistrationEvent> events) {
    Registration state = Registration.empty();
    for (RegistrationEvent event : events) {
        state = state.apply(event);
    }
    return state;
}

重放只恢复内存状态,不能重新扣款、发送通知或读取“今天的汇率”。这类历史计算需要的金额、时间、汇率或规则版本应成为事件事实,副作用由独立处理路径执行。修复一张投影,不该让所有参赛者再收到一次报名短信。

追加事件必须带期望版本,防止两个命令基于同一旧状态同时提交:

text
append(streamId, expectedVersion=4, newEvents)

版本检查与整个事件批次的追加必须在事件存储内原子完成,不能先查询版本再无条件插入。若当前版本已不是 4,写入失败,调用者重新读取后重新判断业务命令,而不是盲目重发根据旧状态生成的事件。命令 ID 还可用于判断响应丢失前那次追加是否已经成功。

事件是长期契约 ​

事件一旦写入历史,就可能在多年后重放。事件演进策略包括:

  • 读取时 upcast 旧版本到当前内存表示;
  • 新消费者同时理解多个 schema 版本;
  • 修复事实时追加纠正事件,不篡改已经发生的历史;
  • 保存重放所需的数据或稳定版本引用,不能指望可变外部对象永远保留旧值。

事件名称应表达已发生的业务事实,如 PaymentAuthorized,而不是含糊的 RegistrationUpdated。

快照与投影重建 ​

快照用于缩短长事件流的聚合加载时间,但不是权威事实。快照应记录所覆盖的事件版本,损坏时能够丢弃并从事件重建。

上述丢弃快照的前提是原始事件仍完整保留且可读;若按保留策略裁剪事件,快照就成为恢复所必需的数据,不能再称它随时可丢。

投影处理器也需要幂等和 checkpoint。投影更新与 checkpoint 应在同一目标事务中提交;无法共用事务时,先持久化投影,再推进 checkpoint,并确保重放幂等。先推进位置再更新投影会永久跳过数据。重建大投影时应:

  1. 在新表或新索引中从头构建;
  2. 持续追赶新增事件;
  3. 验证计数和业务不变量;
  4. 原子切换读流量;
  5. 保留可回退窗口。

切换前用明确的事件位置确认新投影已追上,而不是只比较墙上时钟。回退窗口内若旧投影已停止消费,它会逐渐变旧;必须继续更新或补齐到所需位置再切回。

不要把审计日志误当 Event Sourcing ​

记录“谁在何时改了哪一列”有审计价值,但不一定包含重建业务状态所需的完整语义。Event Sourcing 要求事件是状态权威来源、顺序可验证、演进可管理,并有投影与恢复工具链。

采用前的判断 ​

适合考虑 Event Sourcing 的信号:

  • 历史事实本身有核心业务价值;
  • 需要回溯某时刻状态或重新计算新投影;
  • 领域天然由命令和事件表达;
  • 团队能运营版本、重放、快照和修复流程。

不适合的信号:

  • 只是普通 CRUD;
  • 团队希望借它“自动解决审计”;
  • 无法定义稳定事件语义;
  • 没有能力测试重放和灾难恢复。

参考资料 ​

Built with VitePress | Software Systems Atlas