3. 事件源与生产者:事件源识别方法、事件生产者职责、事件发布策略(同步/异步)
好,咱们进入第三章。前两章我们把事件治理的「为什么」和「是什么」讲清楚了。这一章,得动手了。
事件从哪来?谁负责把它发出去?怎么发?这三个问题,是落地事件驱动架构的第一道坎。我见过不少团队,上来就搞 Kafka、搞 RabbitMQ,结果发现事件要么发不出来,要么发出来没人认领。说白了,就是没搞清楚「事件源」和「生产者」的区别。
3.1 事件源识别方法
事件源,就是事件产生的「根因」。它不是技术上的消息发送者,而是业务上那个「发生了什么」的源头。
我个人习惯用「三问法」来识别事件源:
- 这个动作改变了什么核心数据?——比如订单状态从「待支付」变成「已支付」。
- 这个改变对谁有影响?——库存系统要扣减,财务系统要记账,用户要收到通知。
- 这个影响需要实时还是准实时?——决定了事件是同步还是异步。
举个例子。我在做电商平台重构时,团队一开始把「用户点击了购买按钮」当作事件源。这其实是错的。点击按钮只是操作,真正的源是「订单创建成功」这个事实。你想想看,如果用户点了按钮但支付失败,订单根本没生成,那这个「点击」事件就没有任何业务意义。
事件源识别的核心原则:
- 事件源必须是不可变的事实(已经发生的、不可撤销的)
- 事件源必须对应业务状态的变更(不是操作行为)
- 事件源必须有明确的消费者(没人消费的事件,别发)
常见的业务事件源类型,我整理了一张表:
| 业务域 | 典型事件源 | 触发条件 |
|---|---|---|
| 订单 | 订单已创建、订单已支付、订单已取消 | 数据库状态变更 + 业务校验通过 |
| 库存 | 库存已扣减、库存已释放、库存预警 | 库存操作完成 + 阈值检查 |
| 用户 | 用户已注册、用户已实名、用户已注销 | 用户信息持久化完成 |
| 支付 | 支付成功、支付失败、退款完成 | 支付网关回调 + 对账确认 |
一个小技巧:事件源的命名,我建议用「过去时态」。比如 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 图来总结。你可以把它当作事件源与生产者的「全景地图」。
这张图把事件源识别、生产者职责、发布策略串在了一起。你从上往下看,就是一条完整的事件生产链路:先识别源,再明确谁负责,最后决定怎么发。