事件溯源(Event Sourcing)中如何记录与实现对象状态
事件溯源模式下文件更新历史的正确落地方式
核心结论先给:File本身就是这个场景下的唯一聚合根,你不需要额外设计FileStatus类承载更新历史,所有关联文件的事件流本身就是完整的更新记录,这是事件溯源范式的标准实现逻辑。
聚合边界定义
- 直接将
File定义为聚合根,用已有的fileId作为聚合全局唯一标识。所有和单个文件强绑定的操作——包括上传、内容编辑、重命名、存储路径移动,全部归属这个聚合的行为边界,不需要拆分额外的聚合。 - 放弃用
FileStatus承载更新历史的思路:事件溯源的核心逻辑就是“状态是事件的折叠结果”,如果单独建状态对象存储历史,本质是又回到了状态持久化的老路,还会引入事件和状态双写的一致性风险,完全没必要。
事件设计调整
你当前用通用FileUpdated事件承载所有更新的思路可以跑,但从长期维护性看更推荐拆分为独立事件类型,避免后续加逻辑时大量判断reason字段:
// 文件上传完成事件 {"fileId": "1234556", "eventTimestamp": "2022-06-08 10:50:00", "eventType": "FileUploaded", "payload": {"initPath": "/docs/a.txt", "fileSize": 1024, "uploader": "user_001"}} // 文件内容修改事件 {"fileId": "1234556", "eventTimestamp": "2022-06-08 11:00:00", "eventType": "FileContentModified", "payload": {"newVersionId": "ver_2", "editType": "online_edit"}} // 文件重命名事件 {"fileId": "1234556", "eventTimestamp": "2022-06-08 11:10:00", "eventType": "FileRenamed", "payload": {"oldName": "a.txt", "newName": "b.txt"}} // 文件移动事件 {"fileId": "1234556", "eventTimestamp": "2022-06-08 11:20:00", "eventType": "FileMoved", "payload": {"oldPath": "/docs/a.txt", "newPath": "/archive/b.txt"}}
如果坚持保留通用FileUpdated结构,必须保证两个原则:
- 事件一旦写入事件存储就不可修改、不可删除
- 每个事件自带的时间戳就是该次更新的时间依据,不要单独在聚合内维护冗余的
LastUpdatedTimestamp字段
更新历史的留存与查询实现
- 事件存储中,同一个
fileId下按时间戳升序排列的所有事件,就是这个文件的完整更新历史,不需要额外设计历史表存储。 - 你需要的最后更新时间、最后更新原因两个字段,不需要作为聚合的持久化状态存储:
- 获取文件最新状态时,重放该
fileId下的所有事件得到当前状态,事件流中最后一个事件的时间戳就是LastUpdatedTimestamp,对应的事件类型(或reason字段)就是最后更新原因。 - 需要查询全量更新历史时,直接按
fileId拉取完整事件流按顺序返回即可,不需要额外做跨表关联。
- 获取文件最新状态时,重放该
- 针对高频查询最新更新信息的场景,可以通过读模型投影做性能优化:单独维护一张读库表,存储
fileId、lastUpdatedTimestamp、lastUpdateReason字段,每次有新的文件相关事件写入后,异步更新这张表的对应记录,既保证查询性能,也不破坏事件存储作为唯一可信源的核心原则。
Event Storming阶段的设计注意点
- 贴事件贴纸时,所有文件变更类事件直接放在File聚合的边界内,不要把更新历史拆成独立的聚合或实体。
- 所有触发文件变更的命令(上传文件、编辑内容、重命名、移动位置)全部直接路由到File聚合处理,聚合完成业务规则校验后生成对应事件写入存储,不需要通过中间状态对象中转。
内容的提问来源于stack exchange,提问作者Cesar Flores
相关产品推荐
相关产品推荐

