3、事件建模方法:事件风暴工作坊、事件溯源模式、事件版本管理
事件建模这件事,说白了就是给业务画「动作轨迹」。你想想看,一个订单从创建到完成,中间经历了什么?每一步都是一个事件。我做了这么多年数据治理,最深的体会就是:事件建模做得好,后面的治理工作能省一半力气。
这一章咱们就聊三个核心方法:事件风暴工作坊怎么开、事件溯源模式怎么落地、事件版本怎么管。嗯,都是实战中绕不开的硬骨头。
3.1 事件风暴工作坊:让业务和技术「吵」出共识
事件风暴(Event Storming)是 Alberto Brandolini 提出的一种工作坊形式。说白了,就是把业务专家、产品经理、开发、测试全拉到一个屋子里,用便利贴把业务事件「贴」出来。
我个人习惯把工作坊分成三个阶段:
- 混乱期——大家随便贴,想到什么贴什么。别管对错,先让信息涌出来。
- 整理期——按时间线排序,把重复的合并,把模糊的澄清。
- 建模期——识别出核心事件、命令、聚合,画出边界。
事件风暴的产出物通常包括:
- 领域事件(橙色便利贴):已经发生的事实,比如「订单已创建」
- 命令(蓝色便利贴):触发事件的动作,比如「提交订单」
- 聚合(黄色便利贴):一组强关联的数据,比如「订单聚合」
- 外部系统(粉色便利贴):第三方服务,比如「支付网关」
下面这张图是我常用的工作坊流程,你可以参考:
3.2 事件溯源模式:把状态「存」成事件流
事件溯源(Event Sourcing)这个模式,我刚开始接触时觉得挺反直觉的。你想想,传统做法是存当前状态——比如订单表里存「已支付」,查一下就知道。但事件溯源不这么干,它只存事件:「订单已创建」「支付已完成」「商品已发货」。当前状态?从事件流里算出来。
为什么要这么折腾?原因有三:
- 审计追踪——每一步都有记录,谁在什么时候干了什么,一清二楚
- 时间旅行——可以回到任意时间点,看当时的系统状态
- 事件驱动——每个事件都可以触发下游动作,天然适合微服务
来看一个简单的代码示例:
// 事件基类
public interface Event {
String getEventId();
long getTimestamp();
String getAggregateId();
}
// 具体事件
public class OrderCreatedEvent implements Event {
private String eventId;
private long timestamp;
private String aggregateId;
private String orderId;
private String customerId;
private BigDecimal amount;
// 构造函数、getter/setter 省略
}
// 事件存储接口
public interface EventStore {
void append(String aggregateId, List<Event> events);
List<Event> readEvents(String aggregateId);
List<Event> readEventsSince(String aggregateId, long sinceTimestamp);
}
// 从事件流重建聚合状态
public class Order {
private String orderId;
private String status;
private BigDecimal amount;
public static Order reconstruct(List<Event> events) {
Order order = new Order();
for (Event event : events) {
if (event instanceof OrderCreatedEvent) {
OrderCreatedEvent e = (OrderCreatedEvent) event;
order.orderId = e.getOrderId();
order.amount = e.getAmount();
order.status = "CREATED";
} else if (event instanceof OrderPaidEvent) {
order.status = "PAID";
} else if (event instanceof OrderShippedEvent) {
order.status = "SHIPPED";
}
}
return order;
}
}
核心要点: 事件溯源不是银弹。我见过团队把所有数据都改成事件溯源,结果查询性能惨不忍睹。我的建议是:只在需要审计、时间旅行、事件驱动的核心业务上使用。比如订单、账户、合同这些,别用在用户头像、系统日志上。
事件溯源还有个常见问题:事件多了怎么办?嗯,这里有个概念叫快照(Snapshot)。每积累 N 个事件,就存一份当前状态的快照。重建时先加载最近的快照,再回放之后的事件。我一般设 N=100,具体看事件大小和业务频率。
3.3 事件版本管理:别让旧事件「炸」了你的系统
事件一旦发布,就可能被多个消费者消费。你改了事件结构,旧消费者怎么办?这就是事件版本管理的由来。
我个人习惯用语义化版本来管理事件:
| 版本号 | 变更类型 | 示例 | 兼容性 |
|---|---|---|---|
| v1.0.0 | 初始版本 | 第一个发布版本 | - |
| v1.1.0 | 新增可选字段 | 增加 customerPhone(可选) | 向前兼容 |
| v1.2.0 | 新增必填字段 | 增加 customerEmail(必填) | 需要消费者升级 |
| v2.0.0 | 破坏性变更 | 删除 oldField,重构结构 | 不兼容 |
事件版本管理的常见策略:
- 双写策略——新旧版本同时写入,消费者按需选择。适合迁移期
- 版本转换器——写一个转换层,把旧版本转成新版本。适合消费者不统一的情况
- 事件升级器——在事件存储层做升级,旧事件读取时自动转成新版本。适合事件溯源场景
事件版本管理的核心原则就一条:生产者要向前兼容,消费者要向后兼容。什么意思?生产者不能随便删字段,消费者不能假设字段一定存在。嗯,说白了就是大家都留一手。
最后,我建议你在事件 Schema 里加一个 eventVersion 字段。这样消费者拿到事件,第一件事就是检查版本号,决定怎么处理。别问我为什么强调这个——我见过太多因为版本号缺失导致的线上事故了。