2. 生产状态建模基础

多系统生产状态一致性工程的第一道门槛,不是分布式协议,也不是消息中间件,而是“状态”这个词本身。在单系统里,状态往往就是内存里的一组变量;但在多系统环境里,同一个业务对象的状态会同时存在于订单系统、库存系统、支付系统、履约系统和数据仓库中,每个系统对“当前状态”的理解可能都不一样。如果连状态语义都没有统一,后续的对账、补偿、回滚和一致性校验都无从谈起。

本节先把五个基础概念讲清楚:状态、事件、快照、版本、因果序,然后说明如何为多系统建立统一的状态语义模型。

一、状态:不是变量,而是可判定的语义断言

状态(State)描述的是某个业务对象在某一时刻所处的确定情形。它必须满足两个条件:可判定和可比较。可判定意味着给定一组输入,能明确判断对象是否处于该状态;可比较意味着两个系统对同一对象的状态描述可以对齐或发现差异。

以订单为例,一个可用的状态定义不是“订单处理中”这种模糊描述,而应写成:

OrderState = {
  orderId: "O20240517001",
  status: "PAID",              // 枚举:CREATED / PAID / SHIPPED / COMPLETED / CANCELLED
  paidAmount: 199.00,          // 单位:元,精度 2 位
  currency: "CNY",
  version: 7,
  updatedAt: "2024-05-17T10:23:41+08:00"
}

这里的关键是:状态字段必须是有限枚举或可量化数值,不能是自由文本。多系统一致性工程中,最常见的失败原因就是各系统用不同的词表达同一状态,比如“已支付”“支付成功”“PAID”“付款完成”混用,导致对账时无法自动匹配。

二、事件:状态变化的唯一合法来源

事件(Event)是导致状态发生变化的已发生事实。它有两个重要性质:不可变和已发生。事件不能被修改,只能被追加;事件描述的是过去,而不是意图。

例如,订单从 CREATED 变为 PAID,对应的事件是:

Event = {
  eventId: "E-9f3a...",
  type: "OrderPaid",
  orderId: "O20240517001",
  occurredAt: "2024-05-17T10:23:41+08:00",
  payload: { paidAmount: 199.00, currency: "CNY", channel: "WECHAT" },
  causedBy: "E-8b21..."   // 因果前驱事件
}

事件与状态的关系可以概括为:状态是事件序列在某一时刻的投影。只要事件序列完整且顺序确定,状态就可以被重放出来。这也是事件溯源(Event Sourcing)能够支撑多系统一致性的根本原因。

三、快照:状态的物化与加速

如果每次读取状态都要从头重放全部事件,成本会随时间线性增长。快照(Snapshot)就是在某个版本点上对状态做一次物化保存,后续读取从快照开始,只重放快照之后的事件。

快照必须携带三个元数据:版本号、对应的事件位置、生成时间。缺少任何一个,快照都无法与事件流正确对齐。例如:

Snapshot = {
  orderId: "O20240517001",
  version: 7,
  lastEventId: "E-9f3a...",
  state: { status: "PAID", paidAmount: 199.00, currency: "CNY" },
  createdAt: "2024-05-17T10:24:00+08:00"
}

快照不是真相,事件流才是真相。快照只是加速手段,一旦发现快照与事件流不一致,应以事件流为准重建。

四、版本:给状态变化编号

版本(Version)是状态在事件序列中的单调递增序号。它的作用不是“标记新旧”,而是提供并发控制和差异定位的基准。在多系统写入场景中,版本号是判断“谁的状态更新”以及“是否发生冲突”的核心依据。

常见的版本策略有两种:全局单调版本和按对象单调版本。前者便于跨对象排序,后者实现简单、冲突范围小。实际工程中,订单、库存这类对象通常采用按对象单调版本,例如每次状态变更 version 加 1。

版本冲突的典型判断是:系统 A 持有 version=7 的状态,系统 B 也持有 version=7,两者分别产生新事件后都试图写入 version=8。此时必须通过因果序或版本向量判断谁的事件应该排在前面,而不是简单用时间戳决定。

五、因果序:比时间戳更可靠的事件顺序

物理时钟不可靠,这是分布式系统的常识。两台机器的时钟可能相差几十毫秒甚至更多,因此“谁的时间戳小谁先发生”这个判断在多系统环境中经常出错。因果序(Causal Order)用“事件之间的依赖关系”代替物理时间,定义如下:

  • 如果事件 A 是事件 B 的原因,则 A 因果先于 B,记作 A → B;
  • 如果 A 和 B 之间没有因果路径,则它们是并发的,记作 A ∥ B;
  • 并发事件不能强行排序,只能通过冲突解决策略合并。

实现因果序的常用工具是版本向量(Version Vector)。每个系统维护一个向量,记录自己已知的各系统事件计数。比较两个向量即可判断因果先后或并发关系。

多系统状态语义模型:事件 → 状态 → 快照 → 版本 → 因果序 事件流(真相源) E1 OrderCreated v1 · 10:20:00 E2 OrderPaid v2 · 10:23:41 E3 StockLocked v3 · 10:23:42 E4 OrderShipped v4 · 14:05:10 E5 OrderCompleted v5 · 18:30:00 状态投影(可判定) CREATED v1 PAID v2 PAID + 库存锁定 v3 SHIPPED v4 COMPLETED v5 快照(物化加速) Snapshot v3 · lastEventId=E3 state: PAID + 库存锁定 · 10:24:00 Snapshot v5 · lastEventId=E5 state: COMPLETED · 18:30:05 因果序(版本向量) 订单系统 VV = [订单:5, 库存:3, 支付:2] E1 → E2 → E3 → E4 → E5 库存系统 VV = [订单:3, 库存:3, 支付:1] E3 与 E4 并发 → 需冲突合并 并发事件不可用时间戳强行排序

六、统一状态语义模型的建立方法

把上述五个概念落到工程上,需要建立一份跨系统共享的状态语义模型。它不是某张数据库表,而是一组约定和元数据。建议按以下四步推进。

第一步:定义状态枚举字典。把所有业务对象的状态收敛为有限枚举,每个枚举值给出唯一编码、中文名、可进入条件、可离开条件。例如订单状态字典中,PAID 的进入条件是“收到支付成功事件且金额匹配”,离开条件是“发货事件到达”。

第二步:定义事件契约。每个事件必须包含 eventId、type、orderId、occurredAt、payload、causedBy 六个字段。其中 causedBy 用于构建因果图,是后续判断并发与冲突的关键。

第三步:约定版本与快照策略。明确版本是全局单调还是按对象单调,明确快照的生成频率和保留策略。例如订单对象每 100 个事件或每 5 分钟生成一次快照,快照必须记录 lastEventId。

第四步:建立因果序校验规则。规定当两个系统对同一对象产生并发事件时,采用什么合并策略:以业务优先级高者为准、以金额大者为准,还是进入人工仲裁队列。规则必须显式写入模型,不能靠默认行为。

统一状态语义模型的核心不是“让所有系统用同一张表”,而是让所有系统对“状态是什么、事件怎么记、版本怎么比、并发怎么判”达成一致约定。约定越明确,后续一致性校验和自动补偿就越容易实现。

最后需要强调一个常见误区:很多团队把“状态同步”理解为“把 A 系统的状态字段复制到 B 系统”。这种做法在单向上或许可行,但在多系统双向写入时必然产生冲突。正确的做法是让事件流成为唯一真相源,各系统基于事件重建自己的状态投影,再用版本和因果序判断投影是否一致。状态可以不同步,但事件必须可追溯、可重放、可比较。