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。迁移表至少包含:当前状态、事件类型、目标状态、是否允许。这样回放引擎可以统一校验,也便于生成状态图与测试用例。

订单聚合状态机与快照点 Created Paid Shipped Completed Cancelled Refunded OrderPaid OrderShipped OrderCompleted OrderCancelled OrderCancelled OrderRefunded 快照点 S1 快照点 S2 快照点:保存完整状态,回放从此处开始 合法迁移 终止/异常分支 回放时按 version 顺序重放事件,遇非法迁移标记异常

快照策略:全量、增量与混合

如果只存事件,重建状态需要从第一个事件开始重放。对于长生命周期聚合(如运行数年的账户),重放上千个事件会显著拖慢回放。快照的作用是"截断重放链":在某个版本点保存完整状态,之后只需重放该点之后的事件。

快照策略主要有三种:

  • 全量快照:每次保存聚合的完整状态。恢复最快,但存储开销大,适合状态小、变更频繁的聚合。
  • 增量快照:只保存自上次快照以来的变化量。存储省,但恢复时需要先加载基线再叠加增量,逻辑更复杂。
  • 混合策略:按事件数量或时间间隔触发全量快照,中间辅以增量。工程上最常用。

触发条件通常设为"每 N 个事件"或"每 T 时间"。例如订单聚合每 50 个事件做一次全量快照,则最坏情况下重放 49 个事件即可恢复。N 的取值需要权衡:N 越小恢复越快但快照越多;N 越大存储越省但恢复越慢。经验上,若单事件重放耗时约 0.5ms,目标恢复时间 100ms,则 N 不宜超过 200。

增量重建与全量重建的权衡

重建方式的选择取决于回放目标。全量重建从零开始重放所有事件,结果最精确,适合审计、对账、故障复盘等对一致性要求极高的场景。增量重建从最近快照出发,只重放后续事件,速度快,适合交互式回放、状态预览、实时看板等场景。

两者并非互斥。一个实用的回放系统通常提供"快照 + 增量"的默认路径,同时保留"强制全量"开关。当增量重建结果与全量重建结果不一致时,说明快照损坏或事件丢失,此时应以全量结果为准并触发快照修复。

判断快照是否可信的一个简单方法:在快照点之后重放少量事件,与全量重建到同一版本的状态做字段级比对。若关键字段(如金额、状态、数量)一致,可认为快照有效;若不一致,应标记该快照为可疑并回退到更早的快照。

设计检查清单

检查项合格标准常见问题
事件语义每个事件只描述一个已发生动作事件名含"更新""处理"等模糊动词
时间字段区分 occurred_at 与 recorded_at只存一个时间,回放排序错误
版本号聚合内单调递增,用于乱序检测缺失 version,无法发现并发冲突
状态机显式迁移表,非法迁移可识别状态迁移散落在业务代码中
快照触发按事件数或时间间隔,有明确阈值从不快照或每次事件都快照
重建校验增量与全量结果可比对只信增量,快照损坏无感知

事件模型与快照设计不是一次性工作。随着业务演进,事件类型会增加,状态机会扩展,快照格式也可能变化。因此事件载荷应保持向后兼容,新增字段尽量可选;快照应带 schema 版本号,升级时提供迁移脚本。这样,历史数据回放系统才能在长期运行中保持可重建、可验证、可演进。