Spring事务那些隐形陷阱:本地测试一切正常,线上却出现数据错乱、事务不回滚、脏数据
Spring事务那些隐形陷阱:本地测试一切正常,线上却出现数据错乱、事务不回滚、脏数据
Spring事务是Java后端保证数据一致性最常用的手段,@Transactional注解几乎每个业务项目随处可见。很多开发者认为只要方法打上这个注解,就万事大吉,异常发生数据库就会自动回滚。

可线上无数真实案例告诉我们:很多时候注解明明写了,抛出异常事务却没有回滚;部分数据库操作提交成功,部分操作失败,出现数据不一致;还有事务传播行为使用错误,导致大事务、锁等待、数据库死锁。
这些问题绝大多数都不是Spring框架本身bug,而是开发者对事务生效条件、传播机制、异常捕获规则理解不到位。开发环境测试用例简单,调用链路短,隐藏问题不会暴露,一旦上线面对真实业务链路、并发场景,各种诡异故障集中爆发。下面结合生产故障复盘,包含错误代码、故障现象、根因解析、可直接落地修复方案。
坑点1:方法内部try‑catch捕获异常,@Transactional无法触发回滚
故障现象
业务方法添加@Transactional,方法内部抛出数据库异常,被try‑catch捕获处理。程序打印错误日志,但是数据库已经提交,事务没有回滚,产生脏数据。本地单元测试很难复现,很多测试只关注程序是否报错,忽略数据库数据状态。
❌错误代码
java
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto){
try {
orderMapper.insert(dto);
stockMapper.deductStock(dto.getGoodsId());
}catch (Exception e){
log.error("创建订单失败",e);
//异常被捕获,没有向外抛出
}
}
根因:Spring事务回滚原理:只有异常向外抛出,被AOP事务拦截器捕获,才会触发事务回滚。如果在方法内部把异常catch吃掉,AOP接收不到异常信号,会认为业务执行正常,直接执行事务提交。很多开发为了不让接口直接抛出500,习惯内部捕获异常,直接破坏事务回滚能力。
✅修复方案
方案1:catch处理业务日志,捕获之后重新向外抛出异常,触发事务回滚。
java
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto){
try {
orderMapper.insert(dto);
stockMapper.deductStock(dto.getGoodsId());
}catch (Exception e){
log.error("创建订单失败",e);
throw new RuntimeException("订单创建失败",e);
}
}
方案2:把try‑catch放到事务方法外层,事务方法内部不捕获异常,交由AOP处理回滚。
实战提醒:不要盲目在事务方法内部吞噬异常,这是线上事务失效最高发的问题。
坑点2:同类内部调用,@Transactional完全失效
故障现象
同一个类里面,无事务的A方法调用本类标注@Transactional的B方法。B方法抛出异常,数据库数据不会回滚。本地测试直接调用B方法一切正常,走A调用B的业务链路就出现事务失效。
❌错误代码
java
@Service
public class OrderService {
//无事务
public void invokeBiz(OrderDTO dto){
//本类内部调用
this.createOrder(dto);
}
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto){
orderMapper.insert(dto);
stockMapper.deductStock(dto.getGoodsId());
}
}
根因:Spring事务基于AOP动态代理实现。只有外部访问代理对象,AOP拦截逻辑才会生效。
this.createOrder()调用的是原始对象,不走代理,事务注解直接失效。很多新手不知道代理原理,同一个service内部互相调用,线上踩坑。
✅修复方案
方案1:把事务方法抽取到另外一个Service类,外部Bean调用,走代理对象;
方案2:上下文获取自身代理对象,通过代理调用。
java
//获取代理对象调用,触发AOP
((OrderService)AopContext.currentProxy()).createOrder(dto);
注意:使用AopContext需要开启
expose‑proxy=true,不建议大量使用,优先拆分为不同Service,代码可读性更好。
坑点3:错误的传播行为,导致大事务、锁超时
故障现象
业务嵌套调用,外层已经开启事务,内层方法设置Propagation.REQUIRES_NEW。预期内层独立事务,结果出现大事务,数据库行锁持有时间变长,线上频繁出现锁等待、超时、死锁。
❌错误认知:以为同一个service直接调用,REQUIRES_NEW就会新开独立事务。
根因:REQUIRES_NEW想要生效,同样必须经过Spring代理对象调用。本类内部调用的情况下,传播行为完全失效,会合并到外层事务。很多开发者照搬文档配置,忽略代理调用前提,线上出现超长事务,锁长时间占用,引发大量并发问题。
✅修复方案
REQUIRES_NEW场景,必须把内层方法放到另外一个Bean,由外部代理对象调用,才能真正开启新事务。同时评估业务,非必要不要滥用REQUIRES_NEW,会产生数据库连接消耗。
坑点4:rollbackFor配置缺失,遇到受检异常事务不回滚
故障现象
方法上加@Transactional,业务抛出自定义受检异常,事务没有回滚,数据提交。
很多开发者直接写@Transactional,不指定rollbackFor。Spring默认仅在抛出RuntimeException和Error的时候回滚。自定义Exception受检异常不会触发回滚。
❌错误代码
java
//没有配置rollbackFor,自定义业务异常继承Exception,不会回滚
@Transactional
public void payOrder() throws BizException {
orderMapper.updateStatus();
throw new BizException("支付失败");
}
✅修复方案
业务项目统一配置:
java
@Transactional(rollbackFor = Exception.class)
最佳实践:所有业务事务方法统一指定rollbackFor,不要使用框架默认行为。
坑点5:事务方法里面调用远程HTTP接口,造成超大长事务
故障现象
事务内部调用第三方http接口、RPC调用,网络慢的时候,数据库事务长时间不提交。数据库连接被长时间占用,行锁不会释放,线上出现大量锁等待,数据库连接池耗尽。
❌错误代码
java
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto){
orderMapper.insert(dto);
//事务内部调用第三方http接口,网络卡顿直接拉长事务周期
thirdPartyApi.callPay();
stockMapper.deductStock(dto.getGoodsId());
}
根因:事务开启之后,数据库连接一直占用,直到方法执行完毕提交。网络请求存在很大不确定性,一旦第三方接口响应缓慢,事务会被无限拉长。数据库锁持续持有,高并发场景直接引发大量业务阻塞。
✅修复方案
把远程调用移出事务范围。优先执行远程调用,成功之后,再进入事务执行数据库操作。
java
//先调用第三方,不在事务内
thirdPartyApi.callPay();
//再开启事务执行数据库操作
orderService.saveOrderAndStock(dto);
坑点6:事务中使用多线程,子线程数据库操作不参与主事务
故障现象
主线方法开启事务,方法内部开启新线程做数据库写入。主线程抛出异常回滚,子线程已经写入的数据不会回滚,出现部分数据提交。
❌错误代码
java
@Transactional(rollbackFor = Exception.class)
public void batchSave(){
new Thread(()->{
userMapper.insert(new User());
}).start();
int i = 1/0;
}
根因:Spring事务和数据库连接绑定在当前线程ThreadLocal。子线程获取不到主线程的数据库连接,无法加入当前事务。子线程会获取全新数据库连接,独立执行,不受主事务控制。
✅修复方案
不要在事务内部新开线程执行数据库操作。多线程数据库操作需要分布式事务方案,不要依赖Spring本地事务。
线上开发总结:Spring事务编码实战准则
- 事务方法内部不要随意catch吃掉异常,需要捕获日志,记得重新抛出;
- 注意AOP代理限制,本类内部调用,事务注解不会生效;
- 统一配置rollbackFor = Exception.class,不要依赖Spring默认回滚规则;
- 事务内部杜绝http、rpc远程调用,避免长事务,防止锁等待;
- 子线程无法继承主线程事务,多线程数据库操作不能使用本地事务;
- 理解事务传播行为生效前提,REQUIRES_NEW必须跨Bean代理调用;
- 线上排查事务问题,打印事务日志,观察事务创建、提交、回滚完整流程。
很多开发者以为打上注解就万事大吉,忽略底层AOP、线程绑定、异常规则。本地测试场景简单,很多边界条件不会触发,只有线上真实业务流量,才会把隐藏问题暴露出来。遇到数据不一致,优先排查事务是否真正生效,不要盲目怀疑数据库。
友情链接:
凡尘博客、
凡尘影院、
凡尘博客文章|凡尘博客文摘、
凡尘乡音|凡尘街坊、
凡尘博客|雨落凡尘博客|羽落凡尘博客、
凡尘博客|雨落凡尘博客|羽落凡尘博客
凡尘版权
本文原创技术文章,仅供技术学习交流,转载请完整保留版权和全部友链。