3、事件建模方法:事件风暴工作坊、事件溯源模式、事件版本管理

事件建模这件事,说白了就是给业务画「动作轨迹」。你想想看,一个订单从创建到完成,中间经历了什么?每一步都是一个事件。我做了这么多年数据治理,最深的体会就是:事件建模做得好,后面的治理工作能省一半力气。

这一章咱们就聊三个核心方法:事件风暴工作坊怎么开、事件溯源模式怎么落地、事件版本怎么管。嗯,都是实战中绕不开的硬骨头。

3.1 事件风暴工作坊:让业务和技术「吵」出共识

事件风暴(Event Storming)是 Alberto Brandolini 提出的一种工作坊形式。说白了,就是把业务专家、产品经理、开发、测试全拉到一个屋子里,用便利贴把业务事件「贴」出来。

我个人习惯把工作坊分成三个阶段:

  1. 混乱期——大家随便贴,想到什么贴什么。别管对错,先让信息涌出来。
  2. 整理期——按时间线排序,把重复的合并,把模糊的澄清。
  3. 建模期——识别出核心事件、命令、聚合,画出边界。
小技巧: 我在项目中遇到过业务方和技术方因为「支付成功」这个事件吵了半小时。业务说「支付成功就是钱到了」,技术说「还有回调、对账、风控」。后来我们约定:事件命名必须包含「已完成」状态,比如「支付已完成」「风控已通过」。这样歧义就少多了。

事件风暴的产出物通常包括:

  • 领域事件(橙色便利贴):已经发生的事实,比如「订单已创建」
  • 命令(蓝色便利贴):触发事件的动作,比如「提交订单」
  • 聚合(黄色便利贴):一组强关联的数据,比如「订单聚合」
  • 外部系统(粉色便利贴):第三方服务,比如「支付网关」

下面这张图是我常用的工作坊流程,你可以参考:

事件风暴工作坊流程 事件风暴工作坊流程 阶段1:混乱期 自由贴便利贴 阶段2:整理期 按时间线排序 阶段3:建模期 识别核心事件 领域事件(橙色) 命令(蓝色) 聚合(黄色) 外部系统(粉色) 💡 关键产出物说明: • 领域事件:已发生的事实,命名用过去时(如「订单已支付」) • 命令:触发事件的动作,由用户或系统发起 • 聚合:一组强关联的数据实体,是事务一致性的边界
避坑指南: 我曾经带过一个工作坊,业务方贴了 200 多张便利贴,结果整理时发现 60% 是重复的。后来我定了个规矩:每张便利贴必须写清楚「谁+做了什么+结果」,比如「用户提交订单→订单已创建」。这样重复率直接降到 20% 以下。

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 字段。这样消费者拿到事件,第一件事就是检查版本号,决定怎么处理。别问我为什么强调这个——我见过太多因为版本号缺失导致的线上事故了。