在Spring中进行事务管理非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。这导致许多人将Spring事务与@Transactional划上了等号,只要有数据库相关操作就直接给方法加上@Transactional注解。
不瞒你说,我之前也一直是这样,直到一次@Transactional的错误使用导致了生产事故,那次事故还直接影响了我的绩效。
@Transactional导致的生产事故回顾
2019年,我负责公司的一个内部报销项目。其中一个核心业务逻辑是:
- 员工加班可通过滴滴出行企业版打车,次日费用会自动同步到报销平台。
- 员工在平台勾选费用并创建报销单,系统会同步创建一条审批流(调用统一的流程平台)。
当时,创建报销单的核心代码如下:
/**
* 保存报销单并创建工作流
*/
@Transactional(rollbackFor = Exception.class)
public void save(RequestBillDTO requestBillDTO){
//调用流程HTTP接口创建工作流
workflowUtil.createFlow(“BILL”,requestBillDTO);
//转换DTO对象
RequestBill requestBill = JkMappingUtils.convert(requestBillDTO, RequestBill.class);
requestBillDao.save(requestBill);
//保存明细表
requestDetailDao.save(requestBill.getDetail())
}
代码看起来简洁“优雅”,先通过HTTP接口调用工作流引擎创建审批流,然后保存报销单。为了保证操作的一致性,在整个方法上加上了@Transactional注解。但你想过吗,这样真的能保证事务吗?
项目属于内部系统,并发不高,一直稳定运行。直到年末的一天下午(恰逢大雪后打车需求激增),公司发邮件通知年度报销窗口即将关闭。与此同时,工作流引擎正在进行安全加固。
收到邮件后,报销申请量在接近下班时达到顶峰,系统随即出现故障:数据库监控平台频繁告警,提示连接不足与大量死锁;日志显示调用流程引擎接口大量超时;同时持续抛出CannotGetJdbcConnectionException,数据库连接池被占满。
我们尝试过清理死锁进程,也进行过重启,但不到10分钟故障复现,投诉电话不断。最终,只能向全员发送停机维护通知。事后复盘,这次事故的根源正是save()方法,而罪魁祸首就是那个看似无害的@Transactional注解。
事故原因深度分析
@Transactional 注解基于AOP实现,本质是在目标方法执行前后进行拦截:在执行前开启或加入一个事务,在执行后根据情况提交或回滚。
当Spring处理该注解时,会从数据库连接池中获取一个connection,并开启事务绑定到当前线程的ThreadLocal上。这意味着,被@Transactional包裹的整个方法执行期间,都使用同一个数据库连接。如果方法中包含了耗时操作,比如调用外部HTTP接口、处理复杂业务逻辑或大批量数据,就会导致这个连接被长时间占用而不释放。一旦此类操作密集发生,数据库连接池很快就会被耗尽。
在这个案例中,事务内进行RPC调用(HTTP调用工作流引擎)导致数据库连接池被撑爆,是一个典型的长事务问题。类似的情况还包括在事务中进行大量数据查询、复杂的业务规则处理等。
什么是长事务?
顾名思义,就是运行时间长、长时间未提交的事务,也可以称之为大事务。
长事务会引发哪些问题?
常见危害包括:
- 数据库连接池被占满,应用无法获取新的连接资源。
- 容易引发数据库死锁。
- 数据库回滚时间长。
- 在主从架构中会导致主从延迟变大。
如何有效避免长事务?
解决长事务的核心宗旨是:对事务方法进行拆分,尽量让事务变小、变快,减小事务的粒度。
首先,我们需要回顾Spring的两种事务管理方式。
声明式事务
通过在方法上使用@Transactional注解进行事务管理的方式称为声明式事务。
- 优点:使用极其简单,开发者无需关心事务的开启、提交、回滚等底层细节,只需关注业务逻辑。
- 缺点:事务的粒度是整个方法,无法进行精细化控制。
编程式事务
开发者基于底层API,在代码中手动管理事务的开启、提交、回滚等操作。在Spring项目中,可以使用TransactionTemplate。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(RequestBill requestBill) {
transactionTemplate.execute(transactionStatus -> {
requestBillDao.save(requestBill);
//保存明细表
requestDetailDao.save(requestBill.getDetail());
return Boolean.TRUE;
});
}
使用编程式事务最大的好处就是可以精细化控制事务范围。因此,避免长事务最直接有效的方法之一,就是放弃使用声明式事务@Transactional,转而使用编程式事务手动控制事务范围。
那么,如果既想享受@Transactional的简便,又想避免长事务,有没有办法呢?

答案是:对方法进行拆分,将非事务操作与事务操作分离开。例如:
@Service
public class OrderService{
public void createOrder(OrderCreateDTO createDTO){
query();
validate();
saveData(createDTO);
}
//事务操作
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO){
orderDao.insert(createDTO);
}
}
这里将query()和validate()这两个不需要事务的操作,与事务方法saveData()拆分开。然而,这种简单的拆分会掉入@Transactional失效的经典陷阱——同一个类内的方法调用。因为@Transactional通过Spring AOP生成的代理对象生效,而类内部方法调用使用的是原始对象,导致事务失效。其他常见的事务失效场景还包括:
@Transactional 应用在非 public 修饰的方法上。
@Transactional 注解属性 propagation 设置错误。
@Transactional 注解属性 rollbackFor 设置错误。
- 异常被catch捕获导致
@Transactional失效。
正确的拆分方式有以下两种:
1. 抽取到独立的Service/Manager层
将事务方法移动到另一个Bean中,通过Spring注入调用,符合代理对象调用的条件。
@Service
public class OrderService{
@Autowired
private OrderManager orderManager;
public void createOrder(OrderCreateDTO createDTO){
query();
validate();
orderManager.saveData(createDTO);
}
}
@Service
public class OrderManager{
@Autowired
private OrderDao orderDao;
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO){
orderDao.saveData(createDTO);
}
}
2. 使用 AopContext.currentProxy()
在启动类开启代理暴露,在方法内通过工具类获取当前代理对象来调用事务方法。
// SpringBootApplication.java
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
// OrderService.java
public void createOrder(OrderCreateDTO createDTO){
OrderService orderService = (OrderService)AopContext.currentProxy();
orderService.saveData(createDTO);
}
总结
@Transactional注解在开发中提供了极大的便利,但缺乏精细控制,稍有不慎就可能引发长事务,进而导致数据库连接池耗尽、死锁等严重生产问题。对于业务逻辑复杂,特别是包含远程调用、耗时操作的方法,更推荐使用编程式事务(TransactionTemplate)进行手动控制。如果坚持使用@Transactional,则务必按照上述方案对方法进行合理拆分,确保事务粒度尽可能小。
在云栈社区的Java和架构板块中,有更多关于事务管理、性能调优及复杂系统设计的深度讨论与实战案例,欢迎一起交流学习,共同规避这些“优雅”的陷阱。