事件溯源模式:让系统拥有“时光机”
事件溯源(Event Sourcing),说白了就是——不存当前状态,只存发生过的事。
我第一次接触这个模式是在一个金融交易系统里。当时有个需求:要能回溯任意时间点的账户余额。用传统方式?你得建一堆快照表,还得考虑数据一致性,麻烦得很。后来用了事件溯源,嗯,整个世界清静了。
核心思想:状态 = 事件的累积
传统CRUD模式下,我们直接保存对象的当前状态:
// 传统方式:直接更新余额
UPDATE account SET balance = 100 WHERE id = 1;
事件溯源的做法完全不同:
// 事件溯源:记录发生了什么
INSERT INTO events (aggregate_id, event_type, data, version)
VALUES (1, 'AccountCreated', '{"owner":"张三"}', 1);
INSERT INTO events (aggregate_id, event_type, data, version)
VALUES (1, 'MoneyDeposited', '{"amount":100}', 2);
你看,我们不直接改余额,而是记录「张三开了账户」和「存了100块」。要查余额?把事件从头到尾回放一遍就行。
三个关键组件
事件溯源系统里,有三个东西你得搞明白:
1. 事件存储(Event Store)
这不是普通的数据库。它是个只追加(append-only)的存储。我习惯用专门的Event Store数据库,或者用PostgreSQL + 序列化JSON字段也能凑合。
事件存储要保证:
- 事件一旦写入,就不能修改(不可变性)
- 按聚合ID和版本号有序存储
- 支持按聚合ID读取全部事件
2. 事件回放(Event Replay)
回放就是把存储的事件重新执行一遍。你想想看,这就像看电影——把录像带从头播到尾。
// 伪代码:事件回放
Account account = new Account();
for (Event event : eventStore.loadEvents(accountId)) {
account.apply(event); // 逐个应用事件
}
return account;
我在项目中遇到过一个问题:事件量太大,每次都从头回放太慢。后来加了快照(Snapshot)机制——每100个事件存一次中间状态,回放时从最近的快照开始。
3. 聚合重建(Aggregate Reconstruction)
聚合(Aggregate)是DDD里的概念,你可以理解为一个业务实体。重建聚合就是通过事件回放,把对象恢复到某个时间点的状态。
SVG:事件溯源核心流程
Axon框架实践
理论说完了,咱们看看实战。Axon是Java生态里最成熟的事件溯源框架。我用了好几年,确实省了不少事。
定义聚合
@Aggregate
public class AccountAggregate {
@AggregateIdentifier
private String accountId;
private int balance;
// 命令处理器:处理"创建账户"命令
@CommandHandler
public AccountAggregate(CreateAccountCommand cmd) {
// 验证业务规则
if (cmd.getInitialBalance() < 0) {
throw new IllegalArgumentException("初始余额不能为负");
}
// 发布事件
apply(new AccountCreatedEvent(cmd.getAccountId(), cmd.getOwner(), cmd.getInitialBalance()));
}
// 事件源处理器:处理"账户创建"事件
@EventSourcingHandler
public void on(AccountCreatedEvent event) {
this.accountId = event.getAccountId();
this.balance = event.getInitialBalance();
}
// 命令处理器:处理"存款"命令
@CommandHandler
public void handle(DepositMoneyCommand cmd) {
apply(new MoneyDepositedEvent(cmd.getAccountId(), cmd.getAmount()));
}
// 事件源处理器:处理"存款"事件
@EventSourcingHandler
public void on(MoneyDepositedEvent event) {
this.balance += event.getAmount();
}
}
你看,Axon用注解就把聚合、命令、事件串起来了。@CommandHandler处理命令,@EventSourcingHandler更新状态——职责分明。
事件存储配置
// Spring Boot配置
@Configuration
public class AxonConfig {
@Bean
public EventStorageEngine eventStorageEngine(EntityManager entityManager) {
// 使用JPA作为事件存储
return JpaEventStorageEngine.builder()
.entityManagerProvider(new SimpleEntityManagerProvider(entityManager))
.build();
}
@Bean
public SnapshotTriggerDefinition snapshotTriggerDefinition(
Snapshotter snapshotter) {
// 每100个事件生成一次快照
return new EventCountSnapshotTriggerDefinition(snapshotter, 100);
}
}
事件溯源的优缺点
| 优势 | 劣势 |
|---|---|
| 完整的审计日志——谁在什么时候做了什么,一清二楚 | 学习曲线陡峭——团队需要转变思维方式 |
| 时间旅行——可以回溯到任意历史状态 | 事件版本管理复杂——事件结构变了怎么办? |
| 读写分离天然支持——CQRS最佳搭档 | 查询性能依赖投影——不能直接查当前状态 |
| 高并发下无锁——只追加写入,没有更新冲突 | 存储量增长快——事件只增不减 |
避坑指南
我曾经踩过几个坑,分享给你:
- 事件结构变更: 事件一旦发布就不能改。如果要加字段,用新版本事件类,老版本事件保留原样。Axon的@Revision注解可以帮你做版本映射。
- 性能问题: 别把所有事件都放一个表里。按聚合类型分表,或者用专门的Event Store数据库(比如EventStoreDB)。
- 分布式事务: 事件溯源和分布式事务天生不对付。我建议用最终一致性 + 幂等处理,别强求强一致性。
什么时候用事件溯源?
不是所有系统都适合。我个人觉得,以下场景可以考虑:
- 需要完整审计日志的金融系统
- 需要时间旅行调试的复杂业务
- 事件驱动架构中的核心聚合
- 需要高并发写入且无锁的场景
但如果你的系统只是简单的CRUD,用户管理、文章发布这种,别用事件溯源——杀鸡焉用牛刀。
事件溯源这个模式,说白了就是「用空间换时间,用事件换状态」。刚开始可能觉得别扭,但一旦用顺手了,你会发现它带来的审计能力和灵活性,是传统方式给不了的。