400-920-5594
173-6014-8050
首页 > 资讯中心 > 技术分享
数据库事务常见陷阱深度剖析:@Transactional失效、脏读、事务嵌套、长事务问题实战处理
2026-08-14 72 技术分享

成都软件开发

  在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后端代码审计、事务问题排查、数据库锁故障定位、业务数据一致性改造等技术服务,帮助企业项目修复各类隐性数据风险,提升系统生产稳定性。

推荐文章查看更多》