3. 事件模型与状态快照设计
历史数据回放系统能否准确重建过去某一时刻的业务状态,取决于两个基础设计:事件如何被定义,以及状态如何被保存。事件是"发生了什么"的最小记录,快照是"当时是什么样"的完整切片。二者配合,才能既保证重建精度,又控制存储与回放成本。
事件类型定义:从业务动作到不可变事实
事件建模的第一步不是写代码,而是把业务动作翻译成不可变的事实陈述。一个合格的事件类型应满足三点:语义单一、字段自洽、时间明确。以订单系统为例,常见事件类型包括 OrderCreated、OrderPaid、OrderShipped、OrderCancelled。每个事件只描述一个已发生的动作,不包含"如果""可能"等推测语义。
事件字段通常分为四类:标识字段(event_id、aggregate_id)、类型字段(event_type)、时间字段(occurred_at、recorded_at)、载荷字段(payload)。其中 occurred_at 表示业务实际发生时间,recorded_at 表示系统写入时间,二者在回放排序中必须区分:重建业务状态用 occurred_at,排查写入延迟用 recorded_at。
{
"event_id": "evt-20240612-000871",
"aggregate_id": "order-100237",
"event_type": "OrderPaid",
"occurred_at": "2024-06-12T10:23:41+08:00",
"recorded_at": "2024-06-12T10:23:43+08:00",
"payload": {
"amount": 268.00,
"currency": "CNY",
"channel": "wechat"
},
"version": 4
}
其中 version 是聚合内事件序号,用于检测并发写入与乱序。若同一聚合出现两个 version=4 的事件,说明存在冲突,回放时必须先做冲突消解,而不是简单按时间排序。
状态机建模:约束事件合法迁移
事件不能任意堆叠,必须由状态机约束其合法迁移路径。订单聚合的状态可定义为 Created → Paid → Shipped → Completed,并允许 Created → Cancelled、Paid → Refunded 等分支。状态机的作用是:回放时若遇到非法迁移(如未支付直接发货),系统应标记为异常事件而非静默接受。
状态机建模建议采用显式迁移表,而不是散落在代码中的 if-else。迁移表至少包含:当前状态、事件类型、目标状态、是否允许。这样回放引擎可以统一校验,也便于生成状态图与测试用例。
快照策略:全量、增量与混合
如果只存事件,重建状态需要从第一个事件开始重放。对于长生命周期聚合(如运行数年的账户),重放上千个事件会显著拖慢回放。快照的作用是"截断重放链":在某个版本点保存完整状态,之后只需重放该点之后的事件。
快照策略主要有三种:
- 全量快照:每次保存聚合的完整状态。恢复最快,但存储开销大,适合状态小、变更频繁的聚合。
- 增量快照:只保存自上次快照以来的变化量。存储省,但恢复时需要先加载基线再叠加增量,逻辑更复杂。
- 混合策略:按事件数量或时间间隔触发全量快照,中间辅以增量。工程上最常用。
触发条件通常设为"每 N 个事件"或"每 T 时间"。例如订单聚合每 50 个事件做一次全量快照,则最坏情况下重放 49 个事件即可恢复。N 的取值需要权衡:N 越小恢复越快但快照越多;N 越大存储越省但恢复越慢。经验上,若单事件重放耗时约 0.5ms,目标恢复时间 100ms,则 N 不宜超过 200。
增量重建与全量重建的权衡
重建方式的选择取决于回放目标。全量重建从零开始重放所有事件,结果最精确,适合审计、对账、故障复盘等对一致性要求极高的场景。增量重建从最近快照出发,只重放后续事件,速度快,适合交互式回放、状态预览、实时看板等场景。
两者并非互斥。一个实用的回放系统通常提供"快照 + 增量"的默认路径,同时保留"强制全量"开关。当增量重建结果与全量重建结果不一致时,说明快照损坏或事件丢失,此时应以全量结果为准并触发快照修复。
设计检查清单
| 检查项 | 合格标准 | 常见问题 |
|---|---|---|
| 事件语义 | 每个事件只描述一个已发生动作 | 事件名含"更新""处理"等模糊动词 |
| 时间字段 | 区分 occurred_at 与 recorded_at | 只存一个时间,回放排序错误 |
| 版本号 | 聚合内单调递增,用于乱序检测 | 缺失 version,无法发现并发冲突 |
| 状态机 | 显式迁移表,非法迁移可识别 | 状态迁移散落在业务代码中 |
| 快照触发 | 按事件数或时间间隔,有明确阈值 | 从不快照或每次事件都快照 |
| 重建校验 | 增量与全量结果可比对 | 只信增量,快照损坏无感知 |
事件模型与快照设计不是一次性工作。随着业务演进,事件类型会增加,状态机会扩展,快照格式也可能变化。因此事件载荷应保持向后兼容,新增字段尽量可选;快照应带 schema 版本号,升级时提供迁移脚本。这样,历史数据回放系统才能在长期运行中保持可重建、可验证、可演进。