2. Spring 事务管理:@Transactional 注解、事务传播行为、事务隔离级别

在分布式系统与消息链路的调试中,事务边界是决定数据一致性的核心。Spring 通过 @Transactional 注解提供了声明式事务管理,让开发者能够以极简的代码控制事务的开启、提交与回滚。本章深入剖析该注解的底层机制、传播行为与隔离级别,为后续 Kafka 消息事务的衔接打下基础。

2.1 @Transactional 注解核心机制

@Transactional 本质上是 Spring AOP 的切面实现。当标注在类或方法上时,Spring 会为该类生成代理对象,在方法调用前后织入事务管理逻辑。

配置属性 默认值 说明
propagation Propagation.REQUIRED 事务传播行为
isolation Isolation.DEFAULT 事务隔离级别
timeout -1(无超时) 事务超时秒数
readOnly false 是否为只读事务
rollbackFor RuntimeException 和 Error 指定触发回滚的异常类型
noRollbackFor 空 指定不触发回滚的异常类型
关键点:默认情况下,@Transactional 只对非受检异常(RuntimeException 及其子类)和 Error 进行回滚。受检异常(如 SQLException)不会触发回滚,除非显式指定 rollbackFor。
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 1. 保存订单
        orderDao.insert(dto);
        // 2. 扣减库存(可能抛出受检异常)
        inventoryService.deduct(dto.getProductId(), dto.getQuantity());
        // 3. 发送消息(事务边界内)
        messageService.sendOrderCreatedEvent(dto);
    }
}

2.2 事务传播行为详解

传播行为定义了当多个事务方法相互调用时,事务如何扩散或隔离。这是调试消息链路时最容易出错的环节。

调试提示:在 Kafka 消息消费端,如果消费方法标注了 @Transactional,而内部又调用了其他事务方法,传播行为决定了这些操作是否属于同一个数据库事务。
事务传播行为调用链示意 调用方方法A @Transactional(REQUIRED) 被调用方法B REQUIRED(加入事务) 被调用方法C REQUIRES_NEW(新事务) 事务边界对比 • 方法B(REQUIRED):复用调用方A的事务,A回滚则B也回滚 • 方法C(REQUIRES_NEW):挂起A的事务,开启独立新事务,C回滚不影响A • 方法D(NESTED):在A的事务内创建保存点,D回滚仅回滚到保存点 被调用方法D NESTED(嵌套事务) 常用传播行为:REQUIRED(默认)| REQUIRES_NEW | NESTED | SUPPORTS | MANDATORY | NEVER | NOT_SUPPORTED
传播行为 行为描述 消息链路调试场景
REQUIRED 支持当前事务,不存在则新建 消费消息时,数据库操作与消息确认在同一事务
REQUIRES_NEW 挂起当前事务,新建独立事务 发送 Kafka 消息前,先独立提交日志记录
NESTED 在当前事务内创建保存点 批量消息处理,单条失败仅回滚该条
MANDATORY 必须存在当前事务,否则抛异常 强制调用方已开启事务,防止无事务执行
NEVER 必须不存在当前事务,否则抛异常 非事务性操作(如缓存更新)

2.3 事务隔离级别

隔离级别决定了事务之间数据可见性的程度。在消息链路中,消费端读取数据库时可能遇到脏读、不可重复读或幻读问题。

隔离级别 脏读 不可重复读 幻读 性能
READ_UNCOMMITTED 可能 可能 可能 最高
READ_COMMITTED 避免 可能 可能 高
REPEATABLE_READ 避免 避免 可能 中
SERIALIZABLE 避免 避免 避免 最低
消息链路陷阱:当 Kafka 消费者在 READ_COMMITTED 级别下读取订单表时,如果另一个事务正在更新订单状态但未提交,消费者可能读到旧数据(不可重复读)。对于要求严格顺序的消息处理,建议使用 REPEATABLE_READ 或结合乐观锁。

2.4 事务失效常见场景

在调试消息链路时,事务失效是最隐蔽的问题。以下场景会导致 @Transactional 不生效:

  • 自调用:同一个类中的方法直接调用另一个 @Transactional 方法,AOP 代理不生效。
  • 非 public 方法:注解只能作用于 public 方法(基于代理机制)。
  • 异常被捕获:方法内部 try-catch 吞掉了异常,事务无法感知。
  • 数据库引擎不支持:如 MySQL 的 MyISAM 引擎不支持事务。
  • 传播行为配置错误:如 SUPPORTS 在没有事务时不会创建新事务。
// 错误示例:自调用导致事务失效
@Service
public class OrderService {

    public void processOrder(OrderDTO dto) {
        // 直接调用内部方法,AOP 代理不拦截
        this.saveOrder(dto);  // @Transactional 不生效!
    }

    @Transactional
    public void saveOrder(OrderDTO dto) {
        orderDao.insert(dto);
    }
}

// 正确做法:注入自身代理
@Service
public class OrderService {

    @Autowired
    private OrderService self;  // 注入代理对象

    public void processOrder(OrderDTO dto) {
        self.saveOrder(dto);  // 通过代理调用,事务生效
    }

    @Transactional
    public void saveOrder(OrderDTO dto) {
        orderDao.insert(dto);
    }
}

2.5 事务边界与消息发送的协调

在 Kafka 消息链路中,事务边界需要与消息发送时机精确对齐。常见模式有两种:

事务边界与消息发送模式 模式一:事务内发送消息 开启数据库事务 执行数据库操作(INSERT/UPDATE) 发送 Kafka 消息(事务内) 风险:事务回滚时消息已发出 → 消息与数据不一致 模式二:事务提交后发送消息 开启数据库事务 执行数据库操作 + 提交事务 事务提交后发送 Kafka 消息 推荐:使用 TransactionSynchronizationManager 注册回调
最佳实践:使用 TransactionSynchronizationManager.registerSynchronization() 注册事务同步回调,在事务成功提交后发送 Kafka 消息,确保数据与消息的最终一致性。
@Transactional
public void createOrderAndSendMessage(OrderDTO dto) {
    // 1. 数据库操作
    orderDao.insert(dto);

    // 2. 注册事务同步回调
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 事务提交后才发送消息
                kafkaTemplate.send("order-topic", dto.getOrderId());
            }
        }
    );
}