一段越写越长的订单逻辑
订单创建成功后要加积分、发优惠券、发短信、写操作日志。最初的写法通常是把这些逻辑全塞进 createOrder,包在同一个事务里。上线后问题很快出现:短信服务超时,订单跟着回滚;积分表锁等待,下单接口 RT 飙升;后来产品要求“订单成功但短信可以稍后发”,代码已经缠得没法改。
Spring 事件机制就是为这种场景准备的:发布方只负责喊一声“订单创建了”,谁关心谁去监听,发布方不依赖监听方的实现。它不能替代消息队列,但在单体应用内部做解耦足够轻量。
事件的三件套
一个事件流程只有三个角色:事件对象、发布者、监听者。事件对象在 Spring 4.2 之后不强制继承 ApplicationEvent,普通 POJO 甚至 record 都可以。发布者注入 ApplicationEventPublisher,调用 publishEvent。监听者用 @EventListener 标注方法,参数类型就是订阅的事件类型。
public record OrderCreatedEvent(Long orderId, Long userId, LocalDateTime createdAt) {
}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
private final OrderRepository orderRepository;
public OrderService(ApplicationEventPublisher publisher, OrderRepository orderRepository) {
this.publisher = publisher;
this.orderRepository = orderRepository;
}
@Transactional(rollbackFor = Exception.class)
public void create(Long userId) {
Order order = orderRepository.save(new Order(userId, "NEW"));
publisher.publishEvent(new OrderCreatedEvent(order.getId(), userId, LocalDateTime.now()));
}
}
注意默认行为:publishEvent 是同步的,监听方法在发布者线程里执行,和发布方共用同一个事务。也就是说,监听器抛异常,订单照样回滚;监听器很慢,下单接口照样变慢。这一点和很多人对“事件”的直觉相反。
用事务监听控制触发时机
真正做业务解耦时,更常用的是 @TransactionalEventListener。它把监听时机绑定到事务阶段,默认 AFTER_COMMIT:只有订单事务提交成功,监听器才执行;事务回滚,事件不会触发。
@Component
public class OrderCreatedListener {
private static final Logger log = LoggerFactory.getLogger(OrderCreatedListener.class);
private final PointService pointService;
public OrderCreatedListener(PointService pointService) {
this.pointService = pointService;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void addPoint(OrderCreatedEvent event) {
pointService.addPoint(event.userId(), 10);
log.info("订单 {} 积分已发放", event.orderId());
}
}
四个阶段要分清:BEFORE_COMMIT 还在原事务里,适合做最后校验;AFTER_COMMIT 事务已提交,适合发通知、加积分;AFTER_ROLLBACK 用于补偿;AFTER_COMPLETION 无论成败都执行。AFTER_COMMIT 里再写数据库,需要新开事务,否则连接已经提交,写入不会生效。
同步还是异步,要先想清楚
@TransactionalEventListener 只解决“什么时候触发”,不解决“在哪个线程执行”。监听器默认仍在发布线程。如果监听逻辑耗时,要在监听方法上再加 @Async,并指定独立线程池:
@Async("eventExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendSms(OrderCreatedEvent event) {
smsClient.send(event.userId(), "下单成功");
}
这样做的代价是异常不再回传给发布方,短信发失败订单也不会知道。关键业务要么落库重试,要么交给 MQ;不能既想异步,又指望调用方感知失败。另外,异步监听器要在配置类上开启 @EnableAsync,并确保 eventExecutor 这个 Bean 真实存在,否则会退回默认执行器。
常见坑
1. 事件发了,监听器没反应
先看监听方法所在类有没有被 Spring 管理,再看事件类型是否完全匹配(泛型事件尤其容易写错)。如果用的是 @TransactionalEventListener,而发布方根本不在事务里,事件默认会被静默丢弃,可以加 fallbackExecution = true 让它退化执行。
2. 在事务提交前查询刚插入的数据
默认 @EventListener 同步执行,监听器能看到同一事务里未提交的数据;一旦改成 @Async 或 REQUIRES_NEW,就查不到了。这不是事件丢了,而是事务可见性问题,排查时先确认监听器的线程和事务传播。
3. 监听器异常把主流程带崩
同步监听器抛异常会一路冒到发布方,导致订单回滚。非核心逻辑要么异步,要么在监听器内部捕获并记录,别让“送积分失败”阻断下单。
4. 把事件当成 MQ 用
ApplicationEvent 只在当前 JVM 内传播,应用重启期间的事件会丢,多实例之间也收不到。需要跨服务、需要削峰、需要消息重投时,应该上 RabbitMQ 或 RocketMQ,而不是继续加监听器。
小结
事件机制适合“同一进程内、允许最终一致、监听方不反向依赖发布方”的场景。用之前先确认三件事:触发时机、执行线程、失败后的补偿方式。这三件事想清楚,事件才是解耦工具,否则只是把调用链藏得更深。
评论