📸Axon Event Sourcing 快照踩坑记:从空 JSON 到自定义 Snapshotter

2026-4-24|2026-4-24
WU
WU
type
Post
status
Published
date
Apr 24, 2026
slug
axon-event-sourcing-snapshot-custom-snapshotter
summary
记录 Axon Framework Event Sourcing 项目中快照机制完整踩坑过程:默认序列化为空 JSON、触发时机误解、到自定义 Snapshotter + DTO 的解决方案,附并发安全分析。
tags
开发
工具
docker
category
技术分享
icon
password
📸
在使用 Axon Framework 构建 Event Sourcing 订单系统时,我们遭遇了快照机制的三重陷阱:默认快照序列化出空 JSON {}、事件数超过阈值却不触发快照、以及对触发时机的根本性误解。这篇文章记录完整的排查过程和最终解决方案,让后来者少踩这些坑。

📝 主旨内容

为什么需要快照

Event Sourcing 的本质是:aggregate 当前状态 = 所有历史事件的回放结果。每次加载 aggregate(有命令到来时),Axon 都要从头读取 DomainEventEntry 表中该 aggregate 的所有事件,逐条回调 @EventSourcingHandler
对于一个活跃交易对(如 BTC-USDT),随着挂单、撤单、成交事件不断累积,这个重放过程会越来越慢。快照的作用:只需从最近一次快照恢复状态,再补放快照之后的增量事件即可。
Axon 提供 EventCountSnapshotTriggerDefinition 来按事件数量自动触发快照,配置非常简单:
看起来很美好,但坑就从这里开始。

第一坑:默认快照序列化为空 JSON {}

开启快照后,查看 MySQL SnapshotEventEntry 表:
结果:payloadBytes = 2。打开 payload,只有一个 {}
根本原因:Axon 默认的 SpringAggregateSnapshotter 直接用 Jackson 序列化整个 aggregate 对象。但 OrderBookAggregate 的所有字段均为 private,且没有 getter 方法——Jackson 遇到这种情况,序列化结果就是 {}
更糟糕的是,即使加上 getter:
  • OrderBook 内含 TreeMap(Comparator.reverseOrder())Comparator 实例无法被 Jackson 序列化
  • 改用 XStream 也会报 No converter available for java.time.Instant
结论:默认 Snapshotter 对包含复杂数据结构的 aggregate 基本不可用。

第二坑:事件超过阈值却不触发快照

配置阈值 60 后,DomainEventEntry 总事件数已超过 60,但 SnapshotEventEntry 仍然空表。
排查后发现这是对触发时机的根本误解:
EventCountSnapshotTriggerDefinition 统计的是 aggregate 加载时重放的事件数,而不是全局写入的事件总数。
完整触发流程:
步骤
说明
命令到来
Axon 从 EventStore 加载 aggregate
逐条重放
每回放一条历史事件,计数 +1
计数 >= 阈值
异步调用 scheduleSnapshot()
快照写入
下次加载从快照恢复,只补放增量事件
常见误区对照:
误区
实际行为
写了 60 条事件就触发
加载时重放了 60 条事件才触发
全局事件总数超 60 就触发
每个 symbol 的 aggregate 独立计数
重启后自动生成快照
重启后无命令 = aggregate 不加载 = 不触发
节点 A 快照节点 B 也会触发
只有承载该 symbol 命令的节点才会触发

解决方案:自定义 Snapshotter + 可序列化 DTO

DTO 设计

创建专门的 OrderBookSnapshot,规避所有序列化问题:

自定义 Snapshotter

Aggregate 侧恢复

Axon 加载时若存在快照,会将快照 payload 作为"事件"触发 @EventSourcingHandler

并发安全:快照期间新事件写入怎么办

Snapshot worker 在独立线程创建全新 aggregate 实例,与 Axon 管理的线上 aggregate 没有任何共享可变状态,不存在竞争问题。
快照期间若有新事件写入:
  • MySQL REPEATABLE READ 确保 readEvents() 读到一致性视图,期间新写事件不在视图内
  • 快照存储的 sequenceNumber = lastSeq(worker 读到的最后一条事件序号)
  • 下次加载:先恢复快照到 lastSeq,再从 lastSeq + 1 开始补放增量事件
新事件不会丢失,状态始终正确。

🤗 总结归纳

Axon 默认快照对复杂 aggregate 基本不可用,根因是 Jackson 序列化私有字段的限制。通过自定义 Snapshotter + 简单可序列化 DTO,可以完全绕过这一限制,同时保持 Axon 快照加载流程不变。触发时机是另一个关键认知:快照在 aggregate 加载时按重放计数触发,而不是写事件时触发——搞清楚这一点能省去大量排查时间。

📎 参考资料

💡
你在使用 Axon、EventStoreDB 或其他 Event Sourcing 框架时,踩过哪些快照或序列化的坑?欢迎在评论区聊聊你的解法 👇
工具箱上新!六大加密工具 + Cron 表达式生成器正式上线2026 AI 编程工具大爆发:4 款正在改变开发者工作方式的热门工具
Loading...