注解已经加上,但是事务没有回滚?拆解 AOP 代理底层原因 + 代码示例

做业务开发,@Transactional几乎每天都在用。 看上去一个简单注解,就可以自动帮我们完成事务提交、异常回滚,很多同学以为只要把注解贴上去就万事大吉。

但线上不止一次遇到诡异故障:代码抛异常了,数据库数据却已经写入,没有回滚。排查半天,注解明明写在代码上,看起来完全没问题。

根源大多不是数据库故障,而是 Spring AOP 代理机制、异常捕获、方法权限、事务传播行为这些细节没有理解到位。 今天结合线上真实踩坑案例,把 7 个高频失效场景、错误 Demo、修复方案、生产编码规范全部整理出来。

前置说明:Spring 声明式事务底层依赖AOP 动态代理 。只有通过 Spring 代理对象调用方法,事务才会生效;一旦绕过代理,@Transactional仅仅只是普通注释。


场景 1:@Transactional 写在非 public 方法上

JDK/CGLIB 代理不会拦截 private /protected/default 方法,注解完全无效。

❌错误 Demo

java 复制代码
@Service
public class OrderService {

    // private,事务注解无效
    @Transactional(rollbackFor = Exception.class)
    private void saveOrder() {
        orderMapper.insert(new Order());
        throw new RuntimeException("业务异常");
    }
}

✅修复:把方法改为public


场景 2:本类内部 this 调用事务方法(最高频)

问题:同一个 Service 内部,使用this调用被@Transactional修饰的方法,不走代理,事务直接失效。

❌错误 Demo

typescript 复制代码
@Service
public class OrderService {

    public void createOrder() {
        // this调用,绕过Spring代理,事务注解失效
        this.saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // 插入订单
        orderMapper.insert(new Order());
        // 抛出运行时异常,期望回滚,实际不会回滚
        throw new RuntimeException("模拟业务异常");
    }
}

✅修复方案一:自我注入(推荐,改动最小)

typescript 复制代码
@Service
public class OrderService {

    // 自注入,拿到代理对象
    @Autowired
    private OrderService self;

    public void createOrder() {
        // 使用代理对象调用
        self.saveOrder();
    }

    @Transactional
    public void saveOrder() {
        orderMapper.insert(new Order());
        throw new RuntimeException("模拟业务异常");
    }
}

✅修复方案二:拆分方法到另外一个 Service; ✅修复方案三:开启expose‑proxy = true,使用AopContext.currentProxy()获取代理。


场景 3:没有配置 rollbackFor,受检异常不触发回滚

Spring 事务默认仅对RuntimeExceptionError回滚;普通受检 Exception(IOException 等)不会自动回滚。

❌错误 Demo

java 复制代码
@Service
public class OrderService {

    // 没有配置 rollbackFor,抛出受检异常不会回滚
    @Transactional
    public void saveOrder() throws IOException {
        orderMapper.insert(new Order());
        throw new IOException("IO受检异常");
    }
}

✅修复,指定 rollbackFor

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void saveOrder() throws IOException {
    orderMapper.insert(new Order());
    throw new IOException("IO受检异常");
}

业务项目建议统一习惯写 rollbackFor = Exception.class


场景 4:多线程,子线程无法继承主线程事务

Spring 事务信息保存在ThreadLocal,子线程是全新线程,拿不到父线程事务上下文,子线程数据库操作是独立事务,不会跟随主线程回滚。

❌错误 Demo

java 复制代码
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void saveOrder() {
        orderMapper.insert(new Order());

        // 子线程执行DB操作,不属于当前事务
        new Thread(() -> {
            orderMapper.insert(new Order());
        }).start();

        throw new RuntimeException("主线程异常");
    }
}

现象:主线程异常回滚主线程 insert;子线程插入的数据不会回滚

✅说明:不要指望子线程参与父事务;子线程需要数据库操作要自己开启独立事务。


场景 5:try‑catch 捕获吃掉异常,没有向外抛出

Spring 事务拦截器只有感知到方法抛出异常,才会触发回滚;如果异常被 catch 吞掉,直接提交事务。

❌错误 Demo

java 复制代码
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void saveOrder() {
        try {
            orderMapper.insert(new Order());
            int i = 1 / 0; // 抛出算术异常
        } catch (Exception e) {
            // 异常被吃掉,没有抛出,事务不会回滚
            log.error("保存订单异常", e);
        }
    }
}

✅方案 A:catch 之后重新抛出异常

php 复制代码
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
    try {
        orderMapper.insert(new Order());
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("保存订单异常", e);
        throw new RuntimeException(e); // 重新抛出
    }
}

✅方案 B:catch 住不想抛出异常,手动标记事务回滚

java 复制代码
import org.springframework.transaction.support.TransactionAspectSupport;

@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
    try {
        orderMapper.insert(new Order());
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("保存订单异常", e);
        // 手动设置回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

场景 6:数据表引擎 MyISAM,不支持事务

MyISAM 不支持事务,无论注解怎么写,不会回滚。只发生在测试环境、老项目。

sql 复制代码
-- 查看表引擎
show create table `order`;

-- 修改为InnoDB
alter table `order` engine = InnoDB;

这个属于数据库层面问题,Java 代码看不出任何问题,抛异常数据依旧落库。


场景 7:事务传播行为配置错误,嵌套事务回滚冲突

典型:REQUIRES_NEWNESTEDREQUIRED混用,出现Transaction rolled‑back because it has been marked as rollback‑only

❌错误 Demo:内层异常,外层 catch,出现 rollback‑only

typescript 复制代码
@Service
public class OrderService {

    @Autowired
    private ItemService itemService;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder() {
        orderMapper.insert(new Order());
        try {
            // REQUIRED,加入当前外层事务
            itemService.saveItem();
        } catch (Exception e) {
            log.error("保存商品失败", e);
            // 内层抛出异常标记整个事务回滚,外层catch也无法提交
        }
    }
}

@Service
class ItemService{
    @Transactional
    public void saveItem(){
        itemMapper.insert(new Item());
        throw new RuntimeException("商品异常");
    }
}

现象:内层异常把整个全局事务标记 rollbackOnly;外层即使 catch,最终整个事务依旧回滚。

✅修复方案:如果希望子方法失败不影响主事务,子方法使用 propagation = Propagation.REQUIRES_NEW,开启全新独立事务。

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveItem(){
    itemMapper.insert(new Item());
    throw new RuntimeException("商品异常");
}

额外:长事务问题(事务生效,但带来线上灾难)

即使事务没有失效,如果事务方法内部包含 RPC、HTTP 调用、耗时循环,会造成长事务:数据库连接占用、行锁长期持有,引发数据库阻塞、接口大面积超时。

❌反面示例

arduino 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(){
    orderMapper.insert(order);
    // 远程RPC调用,网络慢会拉长事务时间,严禁写在事务内部
    remoteUserFeign.getUserInfo(order.getUserId());
    // http请求
    restTemplate.getForObject("xxx",String.class);
}

✅正确:把远程调用挪到事务方法外面。


✅生产环境 @Transactional 编码 CheckList

  1. @Transactional 只加在 public 方法;
  2. 禁止本类this调用事务方法,使用代理对象;
  3. 业务统一配置 @Transactional(rollbackFor = Exception.class)
  4. 事务方法内部不要放 RPC/HTTP/ 外部接口等耗时调用;
  5. 事务内部避免大循环,防止长事务;
  6. 确认业务表使用 InnoDB 引擎;
  7. 子线程不会继承父线程事务,不能共用事务;
  8. catch 异常场景,要么重新抛出,要么手动 setRollbackOnly()
  9. 嵌套事务理清传播行为,区分 REQUIRED / REQUIRES_NEW。

AI 局限性

AI 可以快速生成带@Transactional的 demo,但是经常产出:本类 this 调用、缺少 rollbackFor 的隐患代码。Demo 跑通,上线发生数据不回滚故障。AI 很难识别 AOP 代理带来的隐性陷阱。

@Transactional不是数据一致性的护身符。 注解生效前提是经过 Spring 代理对象调用。很多线上脏数据 bug,不是数据库故障,而是编码破坏 AOP 代理规则。写业务代码,不能只看功能跑通,要充分考虑异常场景。

你在线上遇到过事务不回滚的坑吗?踩过哪一类?欢迎评论区交流。

我是十年 Java 后端程序媛,分享 JDK 迁移、业务踩坑实录,完整文章合集同步更新公众号:Java 后端程序媛手记。

相关推荐
mldong7 小时前
一个 App,十三套后端:手机审批端 uni-jeeflow-app 开源了
java·架构
tqs_123458 小时前
MySQL RR隔离级别死锁|Gap间隙锁、临键锁,订单并发范围查询死锁根因方案
java
逆境不可逃8 小时前
Pi Agent 学习笔记:多个工具怎样并行执行
java
Flynt9 小时前
Java 27 升级实测:默认值动得比新特性多,有个老参数会让 JVM 直接起不来
java·jvm·后端
Bs_MoneyMagnet9 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
星云API技术支持9 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
niucloud-admin9 小时前
JAVA V6 多商户商城 开发文档——job 计划任务开发
java·python·github
海宇AI10 小时前
零信任架构实战:基于海宇柠檬查出险-登记证构建自动化车抵贷核保网关
java·人工智能·架构·自动化
cfm_291412 小时前
单例模式详解
java
我叫张土豆12 小时前
本体论、DDD、SDD、TDD:AI 编程时代的四层认知栈
java·人工智能