3. 事件源与生产者:事件源识别方法、事件生产者职责、事件发布策略(同步/异步)

好,咱们进入第三章。前两章我们把事件治理的「为什么」和「是什么」讲清楚了。这一章,得动手了。

事件从哪来?谁负责把它发出去?怎么发?这三个问题,是落地事件驱动架构的第一道坎。我见过不少团队,上来就搞 Kafka、搞 RabbitMQ,结果发现事件要么发不出来,要么发出来没人认领。说白了,就是没搞清楚「事件源」和「生产者」的区别。

3.1 事件源识别方法

事件源,就是事件产生的「根因」。它不是技术上的消息发送者,而是业务上那个「发生了什么」的源头。

我个人习惯用「三问法」来识别事件源:

  1. 这个动作改变了什么核心数据?——比如订单状态从「待支付」变成「已支付」。
  2. 这个改变对谁有影响?——库存系统要扣减,财务系统要记账,用户要收到通知。
  3. 这个影响需要实时还是准实时?——决定了事件是同步还是异步。

举个例子。我在做电商平台重构时,团队一开始把「用户点击了购买按钮」当作事件源。这其实是错的。点击按钮只是操作,真正的源是「订单创建成功」这个事实。你想想看,如果用户点了按钮但支付失败,订单根本没生成,那这个「点击」事件就没有任何业务意义。

事件源识别的核心原则:

  • 事件源必须是不可变的事实(已经发生的、不可撤销的)
  • 事件源必须对应业务状态的变更(不是操作行为)
  • 事件源必须有明确的消费者(没人消费的事件,别发)

常见的业务事件源类型,我整理了一张表:

业务域 典型事件源 触发条件
订单 订单已创建、订单已支付、订单已取消 数据库状态变更 + 业务校验通过
库存 库存已扣减、库存已释放、库存预警 库存操作完成 + 阈值检查
用户 用户已注册、用户已实名、用户已注销 用户信息持久化完成
支付 支付成功、支付失败、退款完成 支付网关回调 + 对账确认

一个小技巧:事件源的命名,我建议用「过去时态」。比如 OrderCreated 而不是 CreateOrder。前者是事实,后者是命令。这个区别,在事件溯源(Event Sourcing)里尤其重要。

3.2 事件生产者职责

事件生产者,就是负责把事件源「包装」成事件消息,并发送出去的那个角色。它可以是微服务、可以是模块、甚至可以是一个函数。

生产者的职责,我总结为四点:

  • 事件构建:把业务数据转换成标准的事件格式(包含事件ID、时间戳、类型、载荷等)
  • 事件校验:确保事件数据完整、符合契约(比如必填字段不能为空)
  • 事件发布:选择合适的策略(同步/异步)把事件发到消息中间件或事件总线
  • 失败处理:发布失败时的重试、补偿、日志记录

我曾经在一个项目中踩过坑。当时团队把事件生产者的职责和业务逻辑混在一起。一个订单服务里,既有「创建订单」的业务代码,又有「发送OrderCreated事件」的代码,还夹杂着「更新用户积分」的逻辑。结果呢?订单创建失败时,事件已经发出去了,下游系统收到了一个「假事件」。

避坑指南:事件生产者应该只负责「发布事实」,不负责「处理后果」。后果是消费者的事。生产者如果越界去处理后果,事件驱动就退化成「事件耦合」了。

我推荐的生产者设计模式是这样的:

// 伪代码:事件生产者职责分离
public class OrderService {
    
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        // 1. 业务逻辑:创建订单
        Order order = orderRepository.save(buildOrder(request));
        
        // 2. 构建事件
        OrderCreatedEvent event = OrderCreatedEvent.builder()
            .eventId(UUID.randomUUID().toString())
            .timestamp(Instant.now())
            .orderId(order.getId())
            .userId(order.getUserId())
            .amount(order.getTotalAmount())
            .build();
        
        // 3. 发布事件(通过事件发布器,而不是直接发消息)
        eventPublisher.publish(event);
        
        return order;
    }
}

// 事件发布器:专门负责发布策略和失败处理
@Component
public class EventPublisher {
    
    private final KafkaTemplate<String, Object> kafkaTemplate;
    
    public void publish(DomainEvent event) {
        try {
            // 同步发布(事务内)
            kafkaTemplate.send(event.getTopic(), event.getEventId(), event).get(5, TimeUnit.SECONDS);
        } catch (Exception e) {
            // 异步补偿:写入本地事件表,由后台任务重试
            eventRepository.save(new FailedEvent(event));
            log.error("事件发布失败,已写入本地事件表: {}", event.getEventId(), e);
        }
    }
}

你看,这样职责就清晰了。OrderService 只关心业务和事件构建,EventPublisher 只关心发布策略和失败处理。各司其职。

3.3 事件发布策略:同步 vs 异步

这是最让架构师头疼的选择。同步还是异步?没有银弹,只有权衡。

先看同步发布:

同步发布,就是生产者发完事件后,必须等到消费者处理完(或至少确认收到)才继续往下走。

  • 优点:强一致性,实时性高,出错能立即回滚
  • 缺点:耦合度高,性能受限于最慢的消费者,容易导致级联故障

再看异步发布:

异步发布,就是生产者把事件丢到消息队列里就完事,消费者自己来拉。

  • 优点:解耦,削峰填谷,生产者不受消费者影响
  • 缺点:最终一致性,调试困难,需要处理消息丢失和重复消费

我个人的经验是:核心链路用同步,非核心链路用异步。

举个例子。在支付场景里,「支付成功」这个事件,库存系统必须同步扣减,否则会出现超卖。这时候用同步发布,确保库存扣减成功后再返回给用户。而「支付成功」后给用户发短信、更新推荐算法这些,完全可以用异步,晚几秒甚至几分钟都没关系。

我的决策矩阵:

场景 推荐策略 理由
强一致性要求(如支付、库存) 同步 需要立即确认结果,失败可回滚
最终一致性可接受(如通知、日志) 异步 解耦,提升主链路吞吐量
跨系统事件(如第三方回调) 异步 避免外部系统拖垮内部服务
高并发场景(如秒杀) 异步 削峰填谷,保护下游系统

还有一种混合策略,我称之为「半同步半异步」。生产者同步等待消息中间件确认收到(不等待消费者处理),然后异步让消费者去消费。这样既保证了事件不丢失,又解耦了生产者和消费者。Kafka 的 acks=all 配置就是这种思路。

嗯,这里要注意:同步发布不等于同步调用。同步发布是「生产者等消息中间件确认」,同步调用是「生产者等消费者处理完」。前者是合理的,后者通常意味着架构设计有问题。

3.4 知识体系总览

这一章的内容,我用一张 SVG 图来总结。你可以把它当作事件源与生产者的「全景地图」。

事件源与生产者知识体系 事件源与生产者知识体系 事件源识别 三问法 数据变更?影响谁?实时性? 不可变事实 已发生、不可撤销 业务状态变更 非操作行为 事件生产者职责 事件构建 事件校验 事件发布 失败处理 发布策略 同步发布 强一致、高耦合 异步发布 最终一致、低耦合 💡 核心原则 生产者只发布事实,不处理后果 核心链路同步,非核心链路异步

这张图把事件源识别、生产者职责、发布策略串在了一起。你从上往下看,就是一条完整的事件生产链路:先识别源,再明确谁负责,最后决定怎么发。