@TransactionalEventListener:事务事件监听的艺术
- [1. 核心概念:为什么需要它?](#1. 核心概念:为什么需要它?)
-
- [1.1 与 @EventListener 的区别](#1.1 与 @EventListener 的区别)
- [1.2 使用决策树](#1.2 使用决策树)
- [2. 案例:订单创建后的消息通知](#2. 案例:订单创建后的消息通知)
-
- [2.1 事件定义:订单创建事件](#2.1 事件定义:订单创建事件)
- [2.2 事件发布者:订单服务](#2.2 事件发布者:订单服务)
- [2.3 事件监听器:消息队列发送](#2.3 事件监听器:消息队列发送)
- [2.4 测试控制器,演示完整流程](#2.4 测试控制器,演示完整流程)
- [3. 适用场景分析](#3. 适用场景分析)
-
- [3.1 消息队列发送的核心适用场景](#3.1 消息队列发送的核心适用场景)
- [3.2 其他适用场景对比](#3.2 其他适用场景对比)
- [4. 注意事项:事务传播行为的问题](#4. 注意事项:事务传播行为的问题)
- [5. 使用原则](#5. 使用原则)
1. 核心概念:为什么需要它?
在分布式系统和微服务架构中,我们经常遇到这样的问题:数据库事务提交后,还需要执行一些额外操作(如发送消息、更新缓存、调用外部API等)。这些操作如果放在事务内,会导致:
- 事务时间变长,增加锁竞争
- 外部操作失败导致事务回滚
- 无法确保数据已持久化就执行后续操作
@TransactionalEventListener 正是为解决这些问题而生。它允许我们定义在 事务的特定阶段 执行的事件监听器,而不是立即执行。
1.1 与 @EventListener 的区别
| 特性 | @EventListener | @TransactionalEventListener |
|---|---|---|
| 执行时机 | 事件发布后立即执行 | 事务的特定阶段执行 |
| 事务保证 | 无事务上下文 | 与发布事件的事务关联 |
| 数据一致性 | 可能读取到未提交数据 | 确保数据已提交/回滚 |
| 适用场景 | 即时通知、日志记录 | 数据同步、外部调用、消息发送 |
1.2 使用决策树
- 是否需要事务控制?
- 否 → 使用 @EventListener
- 是 → 需要哪个阶段?
- 事务提交前 → @TransactionalEventListener(BEFORE_COMMIT)
- 事务提交后 → @TransactionalEventListener(AFTER_COMMIT) [默认]
- 事务回滚后 → @TransactionalEventListener(AFTER_ROLLBACK)
- 事务完成后 → @TransactionalEventListener(AFTER_COMPLETION)
2. 案例:订单创建后的消息通知
2.1 事件定义:订单创建事件
java
/**
* 订单创建成功事件
* 注意:事件对象应是不可变的(immutable)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderCreatedEvent {
/**
* 订单ID(唯一标识)
*/
private String orderId;
/**
* 用户ID
*/
private String userId;
/**
* 订单金额
*/
private BigDecimal amount;
/**
* 订单创建时间
*/
private LocalDateTime createTime;
/**
* 订单项列表
*/
private List<OrderItem> items;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class OrderItem {
private String productId;
private String productName;
private Integer quantity;
private BigDecimal price;
}
}
2.2 事件发布者:订单服务
java
/**
* 订单服务 - 事件发布者
*/
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional // 使用Spring声明式事务
public class OrderService {
private final OrderRepository orderRepository;
private final OrderItemRepository orderItemRepository;
private final ApplicationEventPublisher eventPublisher;
/**
* 创建订单(带事务)
* 核心:订单数据持久化到数据库后,发布事件
*/
public Order createOrder(CreateOrderRequest request) {
log.info("开始创建订单,用户: {}", request.getUserId());
// 1. 创建订单主表
Order order = Order.builder()
.orderId(generateOrderId())
.userId(request.getUserId())
.amount(calculateTotalAmount(request.getItems()))
.status(OrderStatus.CREATED)
.createTime(LocalDateTime.now())
.build();
orderRepository.save(order);
log.info("订单主表保存成功,订单ID: {}", order.getOrderId());
// 2. 创建订单项(批量插入)
List<OrderItem> orderItems = createOrderItems(order.getOrderId(), request.getItems());
orderItemRepository.saveAll(orderItems);
log.info("订单项保存成功,共{}个商品", orderItems.size());
// 3. 发布订单创建事件
// 注意:此时事件已发布,但监听器不会立即执行
// 要等到当前方法执行完毕(事务提交)后才会触发
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(order.getOrderId())
.userId(order.getUserId())
.amount(order.getAmount())
.createTime(order.getCreateTime())
.items(convertToEventItems(orderItems))
.build();
eventPublisher.publishEvent(event);
log.info("订单创建事件已发布,等待事务提交...");
// 4. 模拟其他数据库操作(仍在事务中)
updateUserOrderCount(request.getUserId());
log.info("用户订单计数更新完成");
// 5. 方法返回,Spring事务管理提交事务
// 事务提交后,@TransactionalEventListener监听器开始执行
log.info("订单创建事务即将提交");
return order;
}
private List<OrderCreatedEvent.OrderItem> convertToEventItems(List<OrderItem> items) {
return items.stream()
.map(item -> OrderCreatedEvent.OrderItem.builder()
.productId(item.getProductId())
.productName(item.getProductName())
.quantity(item.getQuantity())
.price(item.getPrice())
.build())
.collect(Collectors.toList());
}
}
2.3 事件监听器:消息队列发送
java
/**
* 1. 消息队列发送监听器
* 监听订单创建事件,在事务提交后发送到消息队列
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class OrderMessageListener {
private final RabbitTemplate rabbitTemplate;
private final KafkaTemplate<String, Object> kafkaTemplate;
private final MessageLogRepository messageLogRepository;
/**
* 发送到RabbitMQ(默认在事务提交后执行)
* 使用 @Async 让监听器异步执行
* 使用 @TransactionalEventListener 确保:
* 1. 订单数据已持久化到数据库
* 2. 避免消息已发送但订单回滚的数据不一致
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async("mqTaskExecutor")
public void sendToRabbitMQ(OrderCreatedEvent event) {
log.info("事务已提交,开始发送订单消息到RabbitMQ,订单ID: {}", event.getOrderId());
try {
// 1. 构建消息
OrderMessage message = OrderMessage.builder()
.orderId(event.getOrderId())
.userId(event.getUserId())
.amount(event.getAmount())
.timestamp(System.currentTimeMillis())
.build();
// 2. 发送消息
rabbitTemplate.convertAndSend(
"order.exchange",
"order.created",
message,
m -> {
m.getMessageProperties().setMessageId(UUID.randomUUID().toString());
m.getMessageProperties().setTimestamp(new Date());
return m;
}
);
log.info("RabbitMQ消息发送成功,订单ID: {}", event.getOrderId());
// 3. 记录发送日志(这里在新事务中,独立于订单创建事务)
recordMessageLog(event.getOrderId(), "RABBITMQ", "SUCCESS", null);
} catch (Exception e) {
log.error("RabbitMQ消息发送失败,订单ID: {}", event.getOrderId(), e);
recordMessageLog(event.getOrderId(), "RABBITMQ", "FAILED", e.getMessage());
// 注意:这里异常不会导致订单事务回滚
// 可以触发重试、告警等补偿机制
}
}
// 记录发送日志
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void recordMessageLog(String orderId, String mqType, String status, String error) {
MessageLog log = MessageLog.builder()
.id(UUID.randomUUID().toString())
.orderId(orderId)
.mqType(mqType)
.status(status)
.errorMsg(error)
.sendTime(LocalDateTime.now())
.build();
messageLogRepository.save(log);
}
/**
* 事务回滚后执行补偿操作
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleOrderRollback(OrderCreatedEvent event) {
log.warn("订单创建事务回滚,清理相关消息,订单ID: {}", event.getOrderId());
// 清理可能已发送的消息(如果有的话)
// 或者记录回滚日志,用于人工核查
}
}
/**
* 2. 异步任务配置
*/
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("mqTaskExecutor")
public Executor mqTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("mq-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
2.4 测试控制器,演示完整流程
java
/**
* 测试控制器,演示完整流程
*/
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
log.info("收到创建订单请求,用户: {}", request.getUserId());
try {
// 1. 调用订单服务(带事务)
Order order = orderService.createOrder(request);
log.info("订单创建API返回,订单ID: {}", order.getOrderId());
// 2. 注意:此时消息队列发送可能在后台执行中
// 因为 @TransactionalEventListener 在事务提交后异步执行
return ResponseEntity.ok(order);
} catch (Exception e) {
log.error("订单创建失败", e);
return ResponseEntity.status(500).build();
}
}
}
3. 适用场景分析
3.1 消息队列发送的核心适用场景
提交成功
回滚
订单创建
事务中保存到数据库
发布OrderCreatedEvent
其他数据库操作
事务提交
事务结果
监听器执行
清理/补偿
发送到RabbitMQ
发送到Kafka
其他服务消费
为什么这是最佳适用场景?
场景特征:
- 数据先持久化,后通知:订单数据必须先确保保存到数据库,才能通知其他系统。
- 通知失败不影响主业务:消息发送失败不应导致订单创建失败。
- 避免消息幽灵:绝对不能出现订单回滚但消息已发送的情况。
- 系统解耦:订单服务不直接依赖消息队列的可用性。
技术需求匹配:
- ✅ 需要事务边界控制:必须在事务提交后发送。
- ✅ 需要错误隔离:消息发送失败不影响主事务。
- ✅ 需要异步处理:消息发送可以异步执行。
- ✅ 需要状态追踪:可以记录消息发送状态。
3.2 其他适用场景对比
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 订单支付后发短信 | ✅ 非常适用 | 必须确保支付成功后才发短信 |
| 用户注册后发欢迎邮件 | ✅ 适用 | 确保用户数据已保存 |
| 商品价格更新同步缓存 | ✅ 适用 | 确保数据库更新成功 |
| 库存扣减 | ❌ 不适用 | 应在事务内完成,确保一致性 |
| 日志记录 | ⚠️ 可能适用 | 看是否需要事务保证 |
4. 注意事项:事务传播行为的问题
1. 推荐的传播行为配置:
java
@Component
public class RecommendedListeners {
/**
* 推荐1:默认无事务传播
* 适合:简单的非数据库操作,如发送消息、HTTP调用
*/
@TransactionalEventListener
public void sendNotification(OrderCreatedEvent event) {
// 发送消息队列、HTTP请求等
// 不需要数据库事务
notificationService.send(event);
}
/**
* 推荐2:只读事务
* 适合:需要查询数据的场景
*/
@TransactionalEventListener
@Transactional(readOnly = true)
public void queryAndProcess(OrderCreatedEvent event) {
// 只查询,不修改
Order order = orderRepository.findById(event.getOrderId());
// 基于查询结果的处理
analyticsService.recordOrderAnalytics(order);
}
/**
* 推荐3:需要写操作时,使用独立服务
* 适合:需要数据库写入的场景
*/
@TransactionalEventListener
public void handleWithService(OrderCreatedEvent event) {
// 委托给专门的服务,由服务管理事务
orderPostProcessService.process(event.getOrderId());
}
}
2. 事务传播行为决策树:
- 监听器中需要数据库操作吗?
- 不需要 → 不加@Transactional(默认)
- 只需要读 → @Transactional(readOnly = true)
- 需要写 →
- 简单操作 → 可以加@Transactional(默认REQUIRED)
- 复杂操作 → 委托给Service,让Service管理事务
5. 使用原则
- 原则1:监听器只做一件事,避免在监听器中执行复杂业务逻辑。
- 原则2:保持监听器无状态。
- 原则3:优先使用异步处理。
- 原则4:设计完善的错误处理,监控监听器执行情况,设置失败告警。
- 原则5:监听器可能被重复调用,确保业务幂等。
总结 :@TransactionalEventListener 在消息队列发送场景中是完美的选择,但必须谨慎处理事务传播行为。记住:监听器应该是轻量的、幂等的、可补偿的。对于复杂的业务逻辑,应该委托给专门的Service管理,而不是在监听器内处理复杂的事务。
java
// 保持监听器简单
@TransactionalEventListener
public void simpleHandler(OrderCreatedEvent event) {
// 只做一件事:发送消息
mqService.sendOrderCreated(event);
// 其他复杂业务放到专门的Service中
// 并设计完善的补偿机制
}
要快乐哦!都好好活啊!
