注解已经加上,但是事务没有回滚?拆解 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 后端程序媛手记。

相关推荐
μθημα1 小时前
K8s Pod 管理实战:虚拟机环境下的具体命令与步骤
java·docker·kubernetes
数据狐(Datafox)1 小时前
1688.item_search工程实战:1688商品列表API分页采集与B2B货源数据分析落地
java·前端·数据分析
省长1 小时前
不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现!
java·后端·开源
得物技术1 小时前
企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术
java·人工智能·后端
撩得Android一次心动1 小时前
Kotlin 语言【知识点整理3】
java·开发语言·笔记·学习·kotlin
朝发如雪1 小时前
快速掌握Linux(4)(进程)
java·linux·服务器
JackSparrow4141 小时前
Kafka(七)集成Apache Avro+Apicurio Schema Registry以保障生产者与消费者的消息兼容性
java·中间件·rpc·kafka·apache·avro·schema-registry
林森lsjs1 小时前
零基础吃透二叉树:定义、遍历与高频算法 —数据结构柒
java·开发语言·数据结构·算法·二叉树
MacroZheng1 小时前
又一个神级画图Skill开源,再见draw.io!
java·人工智能·后端