事件溯源把状态变化记录为可重放的有序事件流
说明事件流、聚合、乐观并发、投影和快照的工作方式,并分析事件演进、重放副作用和采用成本。
事件溯源把状态变化记录为可重放的有序事件流
事件溯源不把当前状态作为唯一事实,而是保存导致状态变化的一系列不可变事件。对象当前状态由事件流按顺序重放得到,查询则常通过投影生成专用读模型。这种方式能保留意图和历史,但会把事件设计、演进、重放和一致性变成长期架构责任。
事件流是事实来源
一个事件应描述已经发生的业务事实,例如“订单已确认”,而不是要求执行动作的命令。事件通常包含聚合标识、流版本、事件类型、发生时间、负载和追踪信息。事件追加后不原地修改;纠错通过新事件表达,使历史仍可审计。
聚合是命令处理的一致性边界。处理命令时读取某个流的事件,重建状态并检查业务规则,再以期望版本追加新事件。如果其他写入已经改变流版本,追加应失败并由调用方重新读取、重新判断,而不是盲目覆盖。
事件存储需要保证同一流内有序追加和持久性,但不一定提供跨所有流的全局事务。把一个业务规则拆到多个聚合后,就要接受消息协调、补偿或流程管理。事件溯源不会自动消除分布式一致性问题。
投影和读取模型
每次查询都从头重放长事件流成本很高,因此系统通常维护投影。投影消费事件并更新适合查询的表、索引或缓存。一个事件可以支持多个投影,各自按界面、统计或搜索需求组织数据。
投影通常与写入存在延迟。客户端提交命令后立即读取,可能尚未看到新状态。可以返回新版本、等待指定投影位置、在界面表达处理中状态,或在少数场景读取聚合状态,但不能假装读写始终同步。
投影处理必须幂等,并持久化消费位置。处理程序在写入投影后、提交偏移前崩溃会重复事件。按事件 ID 去重或用流版本做条件更新,可以防止重复累加。多个分区的事件进入同一投影时,还需定义局部顺序和冲突处理。
快照不是历史替代品
事件流变长后,可周期性保存聚合快照,从最近快照继续重放。快照是性能优化,不是新的事实来源;它必须标明对应流版本,并能在失效时从事件重新构建。模型代码变化后,旧快照可能无法读取,需要版本化或重新生成。
快照频率取决于流长度、重建成本、写入频率和存储预算。每个事件都生成快照会增加写放大,间隔过长又拖慢命令处理。应通过真实重建延迟决定,而不是统一设定固定数量。
事件模式如何演进
事件不可变并不代表事件结构永远不变。新增字段、拆分类型或修正含义时,旧事件仍需被当前代码读取。常见方法包括容错读取、在加载时升级事件、为处理器保留多个版本,或生成补充事件。直接重写历史会破坏审计和外部消费者位置。
事件名称和负载应表达稳定业务事实,避免直接镜像数据库字段。内部事件与跨服务集成事件也不必相同:内部事件可能细且与聚合实现相关,对外事件需要稳定契约、隐私筛选和兼容策略。
重放时不能重新执行发送邮件、扣款或调用外部服务等副作用。投影处理与副作用处理应分离,并提供重放模式、事件边界和幂等键。生产重放前还要估算吞吐、下游容量和完成时间。
采用条件与代价
事件溯源适合历史意图、时间点重建、审计和多种投影具有明确价值的复杂领域。普通 CRUD 已能可靠满足需求时,引入事件存储会增加概念、工具和运维成本。Microsoft 的架构指导也强调,它不是大多数系统和大多数模块的默认选择。
迁移进事件溯源需要为现有状态生成基线事件,历史细节通常无法恢复;迁出则要选择某个投影作为新事实来源,并决定如何保留历史。两种方向都昂贵,因此应先在边界清晰的聚合验证,而不是一次改造整套数据库。
测试除命令结果外,还要覆盖给定事件历史产生的状态、新命令生成的事件、并发版本冲突、事件升级和投影重建。备份恢复必须验证事件存储与消费位置的一致性。
事件溯源的价值来自可解释的历史和可重建状态。只有团队愿意长期维护事件契约、重放纪律和投影一致性,它才是可持续的架构选择。
删除与合规要求
不可变事件与删除要求可能发生冲突。敏感数据不宜直接复制到每个事件,可保存受控引用、令牌化值或最小必要字段,并让密钥销毁或独立数据存储承担删除。事件保留期限、法律冻结和备份处理应在采用前确认;不能等数据主体提出删除后才发现整个重放链依赖该明文。
运维界面也要避免允许任意编辑历史事件。紧急修正应通过经过审批的补偿事件和可追踪工具执行,并在重建投影后验证状态一致。任何跳过领域规则的管理操作都会削弱事件流作为事实来源的可信度。
资料依据
- Microsoft Azure Architecture Center:Event Sourcing Pattern,https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing
- Apache Kafka:Design,https://kafka.apache.org/41/design/design/