网站Logo Ilren 小记

Spring Boot事务管理:@Transactional的六个隐藏陷阱

jack
2
2023-12-23

在使用 Spring Boot 构建业务系统时,@Transactional 是最常见的事务控制注解。然而,你是否遇到过这样的情况:

  • 明明加了 @Transactional,却没有回滚?

  • 异常发生了,数据还是被提交了?

  • 数据源没问题,事务却像失效了一样?

这些问题往往源于我们对 @Transactional 的理解不够深入。本文总结了六个“看起来简单,实则致命”的陷阱,帮你避坑踩雷。

一、异常被吞导致事务不回滚

❌ 错误示例:

@Transactional
public void process(Order order) {
    try {
        orderRepository.save(order);
        stockService.reduce(order.getItems()); // 这里可能抛出异常
    } catch (Exception e) {
        log.error("处理订单异常", e); // 捕获后事务不会回滚!
    }
}

📌 问题分析:

Spring 默认只对未捕获的运行时异常RuntimeException)或 Error 触发事务回滚。被捕获的异常不再往外抛,Spring 认为是“正常逻辑”,因此不会回滚事务。

✅ 正确做法:

  • 方式一:继续抛出异常

catch (Exception e) {
    throw new BusinessException("业务出错", e); // BusinessException 是 RuntimeException 子类
}
  • 方式二:显式设置回滚标志

catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

二、非 public 方法上的事务注解无效

❌ 示例:

@Transactional
void internalMethod() {
    // 事务不会生效
}

📌 原因解析:

Spring 的事务功能是通过 AOP 动态代理实现的,只有 public 方法才能被代理增强。非 public 方法调用时不会触发事务逻辑,等于注解白加了。

✅ 推荐做法:

  • 把事务逻辑放在 public 方法中。

  • 不推荐在 private/protected/default 方法上使用 @Transactional

三、类内部方法调用绕过事务代理

❌ 问题场景:

public class OrderService {

    public void createOrder(Order order) {
        validate(order);
        saveOrder(order); // 自调用,不会走事务代理
    }

    @Transactional
    public void saveOrder(Order order) {
        orderRepository.save(order);
    }
}

✅ 三种解决方案对比:

方式

优点

缺点

注入自身代理

快速修复

有可能引起循环依赖

抽出到独立 Service 类

清晰的职责划分

增加类数量

使用编程式事务控制

控制更灵活

代码侵入性更强,增加复杂度

示例:通过自身代理调用

@Autowired
private OrderService self;

public void createOrder(Order order) {
    self.saveOrder(order); // 事务生效
}

四、事务传播机制理解不足

Spring 提供了 7 种事务传播方式,最常见的是 REQUIREDREQUIRES_NEW。一旦理解不到位,可能出现部分提交、数据错乱等问题。

🌐 常见传播类型说明:

传播类型

行为简述

使用场景

REQUIRED

默认值,存在事务就加入,没有就新建

普通业务流程

REQUIRES_NEW

新建事务,当前事务挂起

日志写入、发送通知等独立操作

NESTED

嵌套事务,底层通过保存点控制回滚

子操作失败时需回滚,主事务不回滚

❌ 示例问题:

@Transactional
public void updateUser(User user) {
    userRepository.update(user);
    logUpdate(user); // 抛出异常,日志仍被提交
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logUpdate(User user) {
    logRepository.insert(new Log("修改用户:" + user.getId()));
}

日志操作是独立事务,即使主流程抛出异常,日志仍然提交了。

五、事务超时设置不当或失效

示例:

@Transactional(timeout = 5) // 单位:秒
public void importData() {
    // 可能执行很久的操作
}

🚨 注意事项:

  • 默认超时时间为 -1,表示无超时限制。

  • 超时时间只对数据库连接有效,对非 JDBC 操作(如文件IO、HTTP 调用)无效。

  • 嵌套事务中,以最外层的超时时间为准

✅ 建议:

  • 对批量导入、报表等慢操作设定合理超时。

  • 避免将大量非数据库操作放在事务中执行。

六、连接池耗尽导致事务死锁

⚠ 死锁典型场景:

@Transactional
public void methodA() {
    doSomething();
    methodB(); // 需要新事务
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 数据库操作
}

如果 methodA 已占用连接,而连接池资源耗尽,methodBREQUIRES_NEW 需要新的连接,将陷入等待,形成互相阻塞。

✅ 解法建议:

  • 避免在主事务中嵌套新事务,改为复用连接。

  • 增加连接池大小(如 HikariCP 的 maximumPoolSize)。

  • 考虑将子方法通过 @Async 异步执行,隔离资源。

🔧 附录:事务管理最佳实践清单

🛠 配置建议

spring:
  transaction:
    default-timeout: 30  # 默认超时(单位:秒)
    rollback-on-commit-failure: true

📊 事务监控指标

@Bean
public TransactionMetrics transactionMetrics(PlatformTransactionManager tm) {
    return new TransactionMetrics(tm);
}

🧪 测试验证代理是否生效

@SpringBootTest
public class TxTest {

    @Autowired
    private DataSource dataSource;

    @Test
    public void testProxy() {
        assertThat(AopUtils.isAopProxy(dataSource)).isTrue();
    }
}

✅ 写在最后

@Transactional 的使用看似简单,实则暗藏玄机。只有理解其背后的原理与注意事项,才能真正驾驭事务,构建稳定、可靠的业务系统。

动物装饰