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 框架时,踩过哪些快照或序列化的坑?欢迎在评论区聊聊你的解法 👇

