当业务逻辑变得臃肿时,解耦通常是开发者的第一选择。在一次订单系统重构中,我们遇到了一个典型场景:订单创建成功后,需要依次执行扣减库存、发送消息、记录日志、更新积分、触发营销活动等一系列连锁操作。最初,这些逻辑被紧密耦合在同一个服务方法中,导致了代码冗长、职责不清、测试困难等问题。
领导提出解耦需求后,我立刻想到了观察者模式(发布-订阅模式),并决定使用Spring框架内置的@EventListener来实现。解耦的目的确实达到了,但随之而来的是一个棘手的数据一致性问题:当订单创建事务回滚时,与之关联的库存扣减等操作却未能回滚,造成了数据不一致。本文将详细复盘这一过程,并分享如何正确使用@TransactionalEventListener来规避事务陷阱。
一、原始场景:高度耦合的订单创建流程
最初的OrderService实现将所有后续逻辑都堆砌在createOrder方法内部:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockService stockService;
@Autowired
private MessageService messageService;
// ... 其他多个Service依赖
@Transactional
public void createOrder(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 扣库存
stockService.decreaseStock(order.getItems());
// 3. 发消息
messageService.sendOrderSuccessMessage(order.getUserId());
// 4. 记日志
logService.logOrderOperation(order, "CREATE");
// 5. 更新用户积分
userPointsService.addPoints(order.getUserId(), order.getAmount());
// 6. 触发营销活动
promotionService.triggerFirstOrderPromotion(order);
// 7. 通知仓库
warehouseService.notifyPrepare(order);
}
}
这种方式存在明显弊端:代码臃肿,违反单一职责原则,测试时需要Mock大量依赖。更严重的是,如果“扣库存”操作失败导致事务回滚,那么在此之前已执行的“发消息”等操作将无法撤销,造成业务逻辑不一致。
二、初次解耦尝试:引入Spring事件机制
为了解决耦合问题,我们引入了Spring的事件发布/订阅机制。首先,定义一个订单创建事件:
public class OrderCreatedEvent extends ApplicationEvent {
private Order order;
public OrderCreatedEvent(Object source, Order order) {
super(source);
this.order = order;
}
public Order getOrder() { return order; }
}
接着,改造OrderService,使其职责单一化,仅负责创建订单并发布事件:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 发布事件,通知所有监听者
eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
}
}
然后,为每个后续操作创建独立的事件监听器,例如库存扣减监听器:
@Component
public class StockEventListener {
@Autowired
private StockService stockService;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
Order order = event.getOrder();
stockService.decreaseStock(order.getItems());
}
}
同理,可以创建MessageEventListener、PointsEventListener等。这种设计成功实现了业务逻辑的解耦,每个监听器可以独立开发、测试和扩展,符合开闭原则。
三、关键陷阱:@EventListener与事务的边界
然而,上述实现很快暴露了新问题。考虑以下场景:订单创建成功后,在同一个事务方法内又发生了其他业务异常。
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
// 模拟其他逻辑出错
throw new RuntimeException("其他业务异常");
}
此时,我们希望整个操作回滚。但实际结果是:订单记录因事务回滚而未能入库,而库存却已被扣减。
问题根源在于@EventListener的默认行为:它在发布事件的同一线程中同步执行,但执行时并不在原有事务上下文中。当事件发布后,监听器方法立即被调用并执行其逻辑(如扣减库存)。如果主事务随后因异常回滚,监听器内已完成的操作无法被自动回滚,因为它们处于不同的事务单元。这直接导致了核心业务数据(订单与库存)的不一致。
四、正确解决方案:使用@TransactionalEventListener
Spring提供了@TransactionalEventListener注解专门处理此类需求,它允许开发者指定监听器在关联事务的哪个阶段执行。
@Component
public class StockEventListener {
@Autowired
private StockService stockService;
// 仅在主事务成功提交后才执行
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
Order order = event.getOrder();
stockService.decreaseStock(order.getItems());
}
}
TransactionPhase提供了多个阶段选项:
BEFORE_COMMIT: 事务提交前执行。
AFTER_COMMIT (推荐): 事务提交后执行。确保主事务成功后才执行业务,完美解决数据一致性问题。
AFTER_ROLLBACK: 事务回滚后执行,适用于补偿操作(如释放预占库存)。
AFTER_COMPLETION: 事务完成后(无论提交或回滚)执行,适用于日志记录等场景。
通过将关键的业务操作(如扣库存、加积分)监听器设置为AFTER_COMMIT,可以保证只有订单事务成功提交后,这些操作才会执行,从根本上避免了数据不一致。
五、进阶优化:异步执行以提升性能
当某些监听器操作耗时较长(如发送短信、调用外部API)时,同步执行会阻塞主流程,影响订单创建的响应速度。此时可以结合@Async实现异步处理。
@Component
public class MessageEventListener {
@Autowired
private MessageService messageService;
@Async // 声明为异步方法
@EventListener // 注意:此处仍使用@EventListener,因为异步方法与原事务分离
public void onOrderCreated(OrderCreatedEvent event) {
Order order = event.getOrder();
messageService.sendOrderSuccessMessage(order.getUserId());
}
}
需要在配置类中启用异步支持:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-executor-");
executor.initialize();
return executor;
}
}
重要提示:异步监听器与原事务完全脱离。它不应再使用@TransactionalEventListener,因为事务上下文不会传递给异步线程。同时,异步监听器内的失败不会导致主事务回滚,适用于允许最终一致性的非核心操作。
六、模式总结与选型建议
回顾整个过程,观察者模式的本质在于解耦与通知。发布者不关心订阅者是谁,订阅者也不关心事件来源,两者通过事件媒介进行通信。Spring Event是其在Spring生态中的一种优雅实现。
在选择解耦方案时,可以遵循以下准则:
- 单机应用,强一致性要求:首选
@TransactionalEventListener(phase = AFTER_COMMIT),在保证解耦的同时,确保事务一致性。
- 单机应用,允许异步/最终一致:结合
@Async 与 @EventListener,提升系统响应速度。
- 分布式系统,复杂场景:当服务需要跨JVM或系统边界时,应引入消息中间件(如RocketMQ, Kafka)来实现更健壮的发布-订阅模式。
- 简单业务逻辑:如果逻辑简单且稳定,直接调用也未尝不可,避免过度设计。
总之,观察者模式是解耦业务逻辑的利器,但在涉及数据库事务的场景下,必须谨慎处理事件监听的执行时机。正确理解并使用@TransactionalEventListener,是规避数据一致性陷阱、构建可靠Spring应用的关键。