
在Java后端开发中📚,@Transactional注解几乎是业务开发的标配,依靠Spring声明式事务,可以简单实现数据库操作的提交与回滚,保障业务数据一致性。
但线上经常出现非常迷惑的现象:明明已经加上事务注解,发生异常却没有回滚,部分数据已经写入数据库;嵌套调用事务方法时,回滚行为和预期完全不一样;还有长事务导致数据库锁等待、死锁、连接池被打满,引发线上故障。
很多开发者遇到事务异常,第一反应怀疑是数据库问题,实际上绝大多数问题来源于Spring AOP代理限制、传播机制理解错误、代码编写不规范、事务范围控制不当。
本文结合大量生产踩坑案例,梳理事务高频陷阱,拆解底层原因,提供修复代码与开发规范,帮助避开事务各类隐形坑,保障业务数据安全。
一、@Transactional高频失效场景⚠️

声明式事务基于Spring AOP动态代理实现,一旦破坏代理调用逻辑,事务就直接失效,这是生产环境最高频问题。
1.同类类内部方法直接调用
同一个class里面,非事务方法直接调用本类加了@Transactional的方法,不会走代理对象,事务完全不生效。
错误示例代码:
@Service
public class OrderServiceImpl implements OrderService {
public void createOrderBiz(){
// 本类内部直接调用,不走AOP代理,事务失效❗
saveOrder();
}
@Transactional(rollbackFor = Exception.class)
public void saveOrder(){
orderMapper.insert(new Order());
int i = 1/0; // 抛出异常,不会回滚
}
}解决方案:把事务方法抽取到另外Service;或者从Spring上下文获取自身代理对象再调用。
2.异常被catch捕获,没有向外抛出
Spring事务只有捕获到方法抛出的异常才会触发回滚,如果try‑catch把异常吃掉,框架感知不到异常,不会执行回滚。
@Transactional(rollbackFor = Exception.class)
public void testCatch(){
try {
orderMapper.insert(new Order());
int i = 1/0;
}catch (Exception e){
log.error("异常",e);
// 没有throw抛出异常 → 事务不会回滚❗
}
}处理方式:catch之后重新throw异常;或者手动使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()标记回滚。
3.rollbackFor配置遗漏,只捕获RuntimeException
@Transactional默认只对运行时异常RuntimeException回滚,如果抛出普通Checked受检异常,默认不会回滚。很多开发忘记配置rollbackFor,出现业务异常数据错乱。
标准写法:@Transactional(rollbackFor = Exception.class)
4.方法不是public修饰
private、protected、default权限的方法,Spring AOP无法生成代理,注解直接无效。事务注解只能加在public方法上。
二、事务传播机制带来的嵌套坑点🔧

传播机制是嵌套事务行为的核心,很多业务bug根源就是对PROPAGATION_REQUIRED、REQUIRES_NEW、NESTED理解混淆。
REQUIRED(默认):有就加入当前事务,没有就新建事务。子方法异常会导致外层整个事务标记回滚,外层抛异常子方法一起回滚。
REQUIRES_NEW:总是开启全新独立事务,挂起外层事务。子事务回滚不影响外层事务,外层异常也不会回滚已经提交的子事务,适合记录日志、保存操作记录场景。
NESTED:嵌套事务,基于savepoint保存点。子事务回滚只回滚到保存点,不影响外层;外层回滚全部一起回滚。
常见踩坑:外层大事务,调用子方法希望子失败不影响主业务,直接用默认REQUIRED做不到,此时需要REQUIRES_NEW。
@Service
public class LogRecordService {
// 独立新事务,就算主业务失败,日志依旧可以保存入库
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveOperateLog(OperateLog log){
logMapper.insert(log);
}
}三、长事务:线上隐形杀手😱

长事务指事务开启之后,中间夹杂大量业务逻辑、网络调用、文件IO、第三方http请求,事务迟迟不提交。
长事务带来的严重后果:
数据库行锁、表锁长时间持有,引发大量锁等待、死锁,其他业务接口大面积阻塞
数据库连接池被占用耗尽,新请求拿不到连接,服务整体雪崩
undo log堆积,数据库性能下降
错误写法:把http远程调用放到@Transactional方法内部。
@Transactional(rollbackFor = Exception.class)
public void badLongTx(){
orderMapper.insert(order);
// ❌事务内部执行第三方http,网络慢会拉长整个事务时长
httpApi.callThirdParty();
accountMapper.updateBalance(account);
}优化原则:事务方法内部只保留数据库操作;网络请求、文件处理、复杂计算全部移到事务外面执行。事务尽量短、快。
四、隔离级别引发的业务问题📖
MySQL InnoDB默认隔离级别REPEATABLE‑READ(可重复读),大部分业务不需要修改隔离级别,但要理解现象:
脏读:读到其他事务未提交的数据,MySQL默认级别不会出现脏读。
不可重复读:同一个事务内,多次读取同一行,中间被其他事务修改提交,读取结果发生变化。
幻读:同一范围查询,其他事务插入新数据,再次查询出现多出的数据。MySQL RR级别通过MVCC+间隙锁解决大部分幻读场景。
不要随意修改全局隔离级别,如需调整,仅针对少数特殊业务方法设置。
五、生产环境事务编码最佳实践✅

事务注解只写在public方法;避免本类内部直接调用事务方法。
统一配置
@Transactional(rollbackFor = Exception.class),不要使用默认配置。不要在事务方法内部写http请求、RPC调用、大循环复杂计算,缩小事务边界。
catch异常如果需要回滚,要么重新throw,要么手动标记回滚。
嵌套事务分清传播机制,记录日志、审计记录优先使用REQUIRES_NEW。
线上开启监控,统计事务执行耗时,告警超时长事务。
尽量避免大事务打包大量数据库操作,大业务拆分为多个小事务。
六、总结📌
Spring声明式事务看着简单,底层依赖AOP代理、事务传播、数据库锁机制,稍有不注意就出现数据异常。很多业务线上数据错乱问题,根源不是SQL写错,而是事务使用方式踩坑。
开发中重点规避:事务失效、异常吞噬、嵌套传播混淆、长事务四大类问题。坚持小事务原则,事务内只做数据库操作,尽可能缩短事务生命周期,才能保证数据一致性与数据库性能。
我们提供Java后端代码审计、事务问题排查、数据库锁故障定位、业务数据一致性改造等技术服务,帮助企业项目修复各类隐性数据风险,提升系统生产稳定性。
在线
电话
微信
需求
TOP