3. 事件溯源核心原理:以事件为唯一真相源、事件存储、事件流与聚合重建

事件溯源(Event Sourcing, ES)颠覆了传统以“当前状态”为中心的持久化范式。在 ES 中,事件是唯一的真相源(Source of Truth),系统的当前状态仅是通过重放事件流计算得出的副产品。本节将深入剖析 ES 的三大核心支柱:事件作为唯一真相源、事件存储与事件流、以及聚合重建机制。

3.1 以事件为唯一真相源

传统 CRUD 系统直接存储聚合的当前快照,丢失了所有变更的历史上下文。事件溯源则记录每一个已发生的事实,这些事实是不可变、可追加的。

维度 传统 CRUD 事件溯源
真相源 当前数据库行(状态快照) 事件流(不可变日志)
数据操作 UPDATE / DELETE(覆盖历史) APPEND(仅追加)
审计能力 弱,需额外审计表 原生完整审计,可回溯任意时间点
时间旅行 不支持 支持,通过重放到指定版本
并发控制 乐观锁(版本号) 事件流版本 + 幂等性
核心原则:事件是事实,不可修改。如果需要修正错误,应追加一个“补偿事件”(如 OrderCorrected),而非修改已有事件。

3.2 事件存储:不可变日志的物理实现

事件存储(Event Store)是 ES 的持久化基础设施,它本质上是一个仅追加的日志。每个事件都包含以下关键字段:

  • EventId:全局唯一标识(UUID)。
  • AggregateId:所属聚合的标识。
  • AggregateType:聚合类型(如 Order)。
  • EventType:事件类型(如 OrderPlaced)。
  • Data:事件载荷(JSON 或二进制序列化)。
  • Metadata:元数据(时间戳、用户ID、关联ID等)。
  • Version:聚合内的版本号(单调递增)。
-- 事件存储表结构示例(PostgreSQL)
CREATE TABLE events (
    event_id        UUID PRIMARY KEY,
    aggregate_id    UUID NOT NULL,
    aggregate_type  VARCHAR(100) NOT NULL,
    event_type      VARCHAR(100) NOT NULL,
    data            JSONB NOT NULL,
    metadata        JSONB,
    version         INTEGER NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (aggregate_id, version)
);

CREATE INDEX idx_events_aggregate
    ON events (aggregate_type, aggregate_id, version);
关键约束:同一聚合内的事件版本必须严格递增且无空洞。这保证了事件流的有序性和完整性,是聚合重建正确性的基础。

3.3 事件流:聚合的生命线

事件流(Event Stream)是按时间顺序排列的、属于同一个聚合实例的所有事件的序列。每个聚合实例对应一个独立的事件流。

下图展示了一个订单聚合的事件流生命周期:

事件流示例:订单聚合(Order-123) OrderPlaced PaymentReceived OrderShipped OrderDelivered v1 v2 v3 v4 说明: 每个事件代表一个已发生的业务事实。事件流按版本号严格排序, 重放整个流即可重建聚合的当前状态。 时间 →

3.4 聚合重建:从事件流到当前状态

聚合重建(Aggregate Reconstruction)是 ES 的核心操作:通过重放聚合的完整事件流,依次调用每个事件的 apply 方法,逐步构建出聚合的当前状态。

3.4.1 重建流程

  1. 加载事件流:从事件存储中读取指定聚合 ID 的所有事件,按版本升序排列。
  2. 创建空聚合:实例化聚合对象,初始状态为默认值(如空集合、零值)。
  3. 重放事件:遍历事件流,对每个事件调用聚合的 apply(event) 方法,该方法内部更新聚合状态。
  4. 返回就绪聚合:重放完成后,聚合处于最新状态,可接受新的命令。
// 聚合重建核心逻辑(TypeScript 伪代码)
class OrderAggregate {
  private state: OrderState = { items: [], status: 'pending', total: 0 };

  // 重放事件流
  static loadFromHistory(events: DomainEvent[]): OrderAggregate {
    const aggregate = new OrderAggregate();
    for (const event of events) {
      aggregate.apply(event);
    }
    return aggregate;
  }

  // 应用单个事件
  private apply(event: DomainEvent): void {
    switch (event.type) {
      case 'OrderPlaced':
        this.state.items = event.data.items;
        this.state.status = 'placed';
        break;
      case 'PaymentReceived':
        this.state.status = 'paid';
        break;
      case 'OrderShipped':
        this.state.status = 'shipped';
        break;
      // ... 其他事件处理
    }
  }

  // 当前状态只读访问
  getState(): Readonly<OrderState> {
    return this.state;
  }
}

3.4.2 快照优化

当事件流非常长(数万甚至数百万个事件)时,每次重建都从头重放会导致性能问题。解决方案是引入快照(Snapshot):

  • 定期(如每 100 个事件)保存聚合状态的快照。
  • 重建时,先加载最近的快照,然后仅重放快照之后的事件。
场景 无快照 有快照(每 100 事件)
事件总数 10,000 10,000
重建需重放事件数 10,000 ≤ 100(最近快照后的事件)
重建延迟 高(秒级) 低(毫秒级)
存储开销 仅事件 事件 + 快照(可接受)
最佳实践:快照应作为缓存层,事件存储始终是权威真相源。快照可以异步生成,且允许丢失(丢失后只需从更早的快照或从头重建)。

3.5 事件溯源架构总览

下图展示了事件溯源在典型后端发布拓扑中的完整数据流:

事件溯源架构:命令 → 事件 → 状态 命令 PlaceOrder 聚合 OrderAggregate 事件存储 仅追加日志 处理 追加事件 事件流读取 loadFromHistory() 查询事件流 重建 投影/读模型 物化视图 订阅事件 架构要点: 1. 命令进入聚合,聚合验证业务规则后追加事件到事件存储。 2. 聚合重建时从事件存储读取完整事件流,重放后获得当前状态。 3. 投影/读模型异步订阅事件,构建面向查询的物化视图。

3.6 关键设计考量

  • 事件版本兼容性:事件结构会随时间演化。需设计事件版本策略(如 EventUpcaster),确保旧版本事件能被当前聚合代码正确重放。
  • 并发控制:使用乐观锁(基于聚合版本号)防止并发命令导致的事件流冲突。
  • 事件幂等性:事件存储应保证同一事件不会被重复追加。通常通过 event_id 唯一约束实现。
  • 性能与存储:事件存储是写优化的,读(重建)可通过快照和缓存优化。长期运行的系统需考虑事件归档策略。
总结:事件溯源的核心在于“事件即事实”。事件存储保证了事实的不可变性和可追溯性;事件流提供了聚合的完整生命周期视图;聚合重建则通过重放事件流,将历史事实转化为当前可用的业务状态。这三者共同构成了 ES 的坚实基石,为后端发布拓扑中的状态恢复提供了理论保障和工程实践路径。