状态是对事件的折叠
传统模型把订单行更新为 paid;事件溯源模型则追加 OrderPlaced 和 PaymentCaptured,当前状态是按序应用这些事件的结果。对单个聚合而言追加是原子的,事件是不可变事实。
CREATE TABLE events (
aggregate_id text NOT NULL,
version bigint NOT NULL,
event_type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (aggregate_id, version)
);
(aggregate_id, version) 主键实现乐观并发:两个写者在同一预期版本追加会冲突,其中一个必须重试。加载聚合时从最近快照读取事件并向前折叠。快照限制重放时间,它们是优化而非事实来源。
读取需要投影
为每个查询折叠全部事件无法扩展。投影消费事件流并写入反规范化的读模型,每个投影记录自己的位置以便重启后续传。投影可以从日志重建,这在读模型出错或需要新视图时很有价值。
事件流与投影是最终一致的。写入后立即读取的用户可能看到旧投影。界面要么对一致性敏感的操作读取写模型,要么明确告知延迟。不要把投影伪装成同步。
事件是契约
投影、其他服务和分析都会消费事件。重命名字段或改变含义会破坏消费者。要为事件模式做版本管理,保持增量变更兼容,并在读取时用 upcaster 转换旧事件。绝不要原地重写历史事件;应追加更正事件。
何时使用
当审计历史、重放或多个读模型是核心需求,且团队有能力运维投影和模式演进时,使用事件溯源。优点是持久、可查询的历史;代价是复杂度:最终一致性、存储增长、快照和事件版本化。与 CRUD 加审计表相比,事件溯源让历史成为一等公民,但每次读写都需要不同的心智模型。