在使用 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);
}
}
✅ 三种解决方案对比:
示例:通过自身代理调用
@Autowired
private OrderService self;
public void createOrder(Order order) {
self.saveOrder(order); // 事务生效
}
四、事务传播机制理解不足
Spring 提供了 7 种事务传播方式,最常见的是 REQUIRED
和 REQUIRES_NEW
。一旦理解不到位,可能出现部分提交、数据错乱等问题。
🌐 常见传播类型说明:
❌ 示例问题:
@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
已占用连接,而连接池资源耗尽,methodB
因 REQUIRES_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
的使用看似简单,实则暗藏玄机。只有理解其背后的原理与注意事项,才能真正驾驭事务,构建稳定、可靠的业务系统。