@TransactionalEventListener:事务事件监听的艺术

@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等)。这些操作如果放在事务内,会导致:

  1. 事务时间变长,增加锁竞争
  2. 外部操作失败导致事务回滚
  3. 无法确保数据已持久化就执行后续操作

@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
其他服务消费

为什么这是最佳适用场景?

场景特征:

  1. 数据先持久化,后通知:订单数据必须先确保保存到数据库,才能通知其他系统。
  2. 通知失败不影响主业务:消息发送失败不应导致订单创建失败。
  3. 避免消息幽灵:绝对不能出现订单回滚但消息已发送的情况。
  4. 系统解耦:订单服务不直接依赖消息队列的可用性。

技术需求匹配:

  • 需要事务边界控制:必须在事务提交后发送。
  • 需要错误隔离:消息发送失败不影响主事务。
  • 需要异步处理:消息发送可以异步执行。
  • 需要状态追踪:可以记录消息发送状态。

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中
    // 并设计完善的补偿机制
}

要快乐哦!都好好活啊!

相关推荐
柒.梧.7 小时前
Spring核心知识全解析:从入门实战到进阶
java·后端·spring
全栈独立开发者7 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
super_lzb8 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
我是Superman丶8 小时前
【异常】Spring Ai Alibaba 流式输出卡住无响应的问题
java·后端·spring
2501_941800888 小时前
从服务注册发现到动态调度的互联网工程语法实践与多语言探索
spring
2501_941822759 小时前
从API网关到统一流量治理的互联网工程语法实践与多语言探索
rabbitmq·memcached
她说..9 小时前
Spring 核心工具类 AopUtils 超详细全解
java·后端·spring·springboot·spring aop
oMcLin10 小时前
如何在 Ubuntu 22.04 服务器上实现分布式数据库 Cassandra 集群,优化数据一致性与写入吞吐量
服务器·分布式·ubuntu
雨中飘荡的记忆10 小时前
Spring AOP详解:从原理到实战
spring
alonewolf_9912 小时前
Spring IOC容器扩展点全景:深入探索与实践演练
java·后端·spring