@Transactional失效的常见场景说明

23 阅读10分钟

@Transactional 失效,本质上通常不是注解没生效,而是 Spring AOP 代理没有拦截到方法调用,或者 事务已经开启了,但异常没有触发回滚。 先记住一句核心原则:

Spring 声明式事务默认基于 AOP 代理 实现,只有通过 Spring 容器代理对象调用的 public 方法,事务才容易正常生效。


一、方法内部自调用导致事务失效

这是最常见的场景。

错误示例

@Service
public class OrderService {

    public void createOrder() {
        saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // 插入订单
        // 插入订单明细
    }
}

表面上 saveOrder()@Transactional,但实际不会生效。

原因是:

this.saveOrder();

属于当前对象内部调用,没有经过 Spring 代理对象,所以事务切面不会执行。

正确写法一:拆到另一个 Service

@Service
public class OrderService {

    @Resource
    private OrderTransactionService orderTransactionService;

    public void createOrder() {
        orderTransactionService.saveOrder();
    }
}
@Service
public class OrderTransactionService {

    @Transactional
    public void saveOrder() {
        // 插入订单
        // 插入订单明细
    }
}

这是最推荐的方式,逻辑也更清晰。


二、@Transactional 标在非 public 方法上

Spring 默认基于代理模式,事务通常只对 public 方法生效。

错误示例

@Service
public class OrderService {

    @Transactional
    private void saveOrder() {
        // ...
    }
}

或者:

@Transactional
protected void saveOrder() {
    // ...
}

这些都容易导致事务不生效。

正确写法

@Transactional
public void saveOrder() {
    // ...
}

三、方法被 final 修饰

如果使用 CGLIB 代理,final 方法不能被子类重写,因此事务增强无法织入。

错误示例

@Service
public class OrderService {

    @Transactional
    public final void saveOrder() {
        // ...
    }
}

正确写法

@Service
public class OrderService {

    @Transactional
    public void saveOrder() {
        // ...
    }
}

四、类被 final 修饰

如果类是 final,CGLIB 无法创建子类代理。

错误示例

@Service
public final class OrderService {

    @Transactional
    public void saveOrder() {
        // ...
    }
}

正确写法

@Service
public class OrderService {

    @Transactional
    public void saveOrder() {
        // ...
    }
}

五、对象不是 Spring 容器管理的 Bean

@Transactional 只能作用于 Spring 管理的 Bean。

错误示例

public class OrderService {

    @Transactional
    public void saveOrder() {
        // ...
    }
}

然后手动 new:

OrderService orderService = new OrderService();
orderService.saveOrder();

这种方式不会有事务。

正确写法

@Service
public class OrderService {

    @Transactional
    public void saveOrder() {
        // ...
    }
}

然后通过 Spring 注入使用:

@Resource
private OrderService orderService;

六、异常被 try-catch 吃掉了

Spring 默认是通过异常判断是否回滚的。
如果异常被你捕获后没有继续抛出,Spring 会认为方法正常执行,事务就会提交。

错误示例

@Transactional
public void createOrder() {
    try {
        orderMapper.insert(order);
        int i = 1 / 0;
        orderItemMapper.insert(item);
    } catch (Exception e) {
        log.error("创建订单失败", e);
    }
}

这个方法最终没有抛异常,所以事务会提交。

正确写法一:继续抛出异常

@Transactional
public void createOrder() {
    try {
        orderMapper.insert(order);
        int i = 1 / 0;
        orderItemMapper.insert(item);
    } catch (Exception e) {
        log.error("创建订单失败", e);
        throw e;
    }
}

正确写法二:手动标记回滚

@Transactional
public void createOrder() {
    try {
        orderMapper.insert(order);
        int i = 1 / 0;
        orderItemMapper.insert(item);
    } catch (Exception e) {
        log.error("创建订单失败", e);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

不过更推荐第一种:抛异常,让事务机制自然回滚


七、抛出的是受检异常,默认不回滚

Spring 默认只对以下异常回滚:

RuntimeException
Error

也就是运行时异常和错误。

对于 ExceptionIOExceptionSQLException 这种受检异常,默认不会回滚。

错误示例

@Transactional
public void createOrder() throws Exception {
    orderMapper.insert(order);
    throw new Exception("业务异常");
}

这个默认不会回滚。

正确写法

@Transactional(rollbackFor = Exception.class)
public void createOrder() throws Exception {
    orderMapper.insert(order);
    throw new Exception("业务异常");
}

企业项目里我一般建议统一写:

@Transactional(rollbackFor = Exception.class)

八、数据库引擎不支持事务

以 MySQL 为例,InnoDB 支持事务,MyISAM 不支持事务。

如果表引擎是 MyISAM,即使 @Transactional 生效,也不会回滚。

查看表引擎

SHOW TABLE STATUS WHERE Name = 'tb_order';

或者:

SHOW CREATE TABLE tb_order;

正确做法

建表时使用 InnoDB:

CREATE TABLE tb_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(64),
    amount DECIMAL(10, 2)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

九、事务方法开启了新线程

事务上下文是绑定在线程上的。
如果你在事务方法里开新线程,新线程不会继承当前事务。

错误示例

@Transactional
public void createOrder() {
    orderMapper.insert(order);

    new Thread(() -> {
        orderItemMapper.insert(item);
    }).start();

    int i = 1 / 0;
}

orderMapper.insert(order) 可能回滚,但新线程里的 orderItemMapper.insert(item) 不受当前事务控制。

@Async 也是类似问题

@Transactional
public void createOrder() {
    orderMapper.insert(order);
    asyncService.saveOrderItem();
    throw new RuntimeException("异常");
}
@Async
public void saveOrderItem() {
    orderItemMapper.insert(item);
}

@Async 会切换线程,事务不会自动传递。


十、事务传播行为配置不符合预期

事务传播行为配置错误,也会让你感觉事务“失效”。

常见传播行为

传播行为含义
REQUIRED默认值,有事务就加入,没有就新建
REQUIRES_NEW挂起当前事务,开启新事务
NESTED嵌套事务,依赖数据库保存点
SUPPORTS有事务就加入,没有事务就非事务执行
NOT_SUPPORTED挂起事务,以非事务方式执行
NEVER当前有事务就抛异常
MANDATORY必须存在事务,否则抛异常

典型坑:REQUIRES_NEW

@Service
public class OrderService {

    @Resource
    private LogService logService;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder() {
        orderMapper.insert(order);

        logService.saveLog();

        throw new RuntimeException("订单创建失败");
    }
}
@Service
public class LogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog() {
        logMapper.insert(log);
    }
}

结果:

订单回滚
日志不回滚

这不是事务失效,而是 REQUIRES_NEW 开启了一个独立事务。


十一、rollbackFor 写错异常类型

错误示例

@Transactional(rollbackFor = RuntimeException.class)
public void createOrder() throws Exception {
    orderMapper.insert(order);
    throw new Exception("普通异常");
}

这里抛的是 Exception,但你只配置了 RuntimeException.class,所以不会回滚。

正确写法

@Transactional(rollbackFor = Exception.class)
public void createOrder() throws Exception {
    orderMapper.insert(order);
    throw new Exception("普通异常");
}

十二、异常类型被转换了

有时候你以为抛的是业务异常,但中间被包装成了别的异常。

例如:

try {
    doSomething();
} catch (Exception e) {
    throw new BusinessException("业务异常");
}

如果你的事务配置是:

@Transactional(rollbackFor = SQLException.class)

但实际抛出的是 BusinessException,那就可能不会回滚。

建议业务异常继承 RuntimeException

public class BusinessException extends RuntimeException {

    public BusinessException(String message) {
        super(message);
    }
}

然后:

@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    throw new BusinessException("订单金额异常");
}

十三、同一个类中,一个有事务方法调用另一个有事务方法

示例

@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        saveOrderItem();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrderItem() {
        // 保存订单明细
    }
}

你可能以为 saveOrderItem() 会开启新事务,但实际上不会。

原因还是:

同类内部调用没有经过代理

所以 REQUIRES_NEW 不会生效。

正确做法

拆到不同 Service:

@Service
public class OrderService {

    @Resource
    private OrderItemService orderItemService;

    @Transactional
    public void createOrder() {
        orderItemService.saveOrderItem();
    }
}
@Service
public class OrderItemService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveOrderItem() {
        // 保存订单明细
    }
}

十四、多数据源事务管理器用错

如果项目有多个数据源,需要指定正确的事务管理器。

示例

@Transactional(transactionManager = "orderTransactionManager")
public void createOrder() {
    orderMapper.insert(order);
}

如果你操作的是订单库,却用了用户库的事务管理器,事务就可能不生效。

多数据源常见配置

@Bean
public DataSourceTransactionManager orderTransactionManager(
        @Qualifier("orderDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}
@Bean
public DataSourceTransactionManager userTransactionManager(
        @Qualifier("userDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

使用时明确指定:

@Transactional(
    transactionManager = "orderTransactionManager",
    rollbackFor = Exception.class
)
public void createOrder() {
    // 操作订单库
}

十五、事务方法中执行了非事务资源操作

事务只能控制数据库连接里的操作,不能自动控制:

Redis
RabbitMQ
Kafka
文件写入
HTTP 调用
本地缓存
第三方支付接口

例如:

@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    orderMapper.insert(order);

    rabbitTemplate.convertAndSend("order.exchange", "order.created", order);

    throw new RuntimeException("异常");
}

数据库回滚了,但 MQ 消息已经发出去了。

这不是 @Transactional 失效,而是事务本来就管不了 MQ。

正确思路

可以用:

本地消息表
事务消息
Outbox Pattern
RocketMQ 事务消息
最终一致性补偿

例如本地消息表:

@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    orderMapper.insert(order);
    messageMapper.insert(message);
}

然后由定时任务或 MQ 发布器异步投递消息。


十六、在构造方法、@PostConstruct 中使用事务

Spring AOP 代理是在 Bean 初始化之后才完整可用的。

所以构造方法、@PostConstruct 中调用事务方法,容易不生效。

错误示例

@Service
public class OrderService {

    @PostConstruct
    public void init() {
        saveOrder();
    }

    @Transactional
    public void saveOrder() {
        // ...
    }
}

原因还是:此时代理机制还没完全参与调用链。


十七、事务注解加在接口上但代理模式不匹配

如果你把 @Transactional 加在接口方法上:

public interface OrderService {

    @Transactional
    void createOrder();
}

在 JDK 动态代理下通常可以生效。

但如果使用 CGLIB 类代理,或者项目配置混乱,可能出现不一致问题。

更稳妥的方式是加在实现类方法上:

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createOrder() {
        // ...
    }
}

十八、readOnly = true 误用

@Transactional(readOnly = true)
public void updateOrder() {
    orderMapper.updateById(order);
}

readOnly = true 表示只读事务优化。

不同数据库、不同驱动、不同 ORM 对它的处理不完全一样。
有些情况下它不会阻止写入,但这是非常危险的用法。

更新、插入、删除不要使用:

@Transactional(readOnly = true)

应该使用:

@Transactional(rollbackFor = Exception.class)

十九、事务嵌套后出现 UnexpectedRollbackException

这种不是事务失效,而是内部事务已经把外层事务标记为回滚了。

示例

@Transactional
public void outer() {
    try {
        inner();
    } catch (Exception e) {
        log.error("inner error", e);
    }
}

@Transactional
public void inner() {
    throw new RuntimeException("异常");
}

如果 inner() 参与了外层事务,它抛异常后事务可能已经被标记成 rollback-only。

外层虽然 catch 了异常,但提交时会报:

UnexpectedRollbackException

解决方案要根据业务决定:

需求方案
内外一起回滚不要 catch,直接抛
内部失败不影响外部内部方法使用 REQUIRES_NEW
只记录失败事务外处理或者独立事务记录日志

二十、常见失效场景总结表

场景是否真的失效原因解决方案
同类方法自调用没经过代理拆 Service / 通过代理调用
private 方法AOP 不增强改 public
final 方法无法代理增强去掉 final
手动 new 对象不归 Spring 管交给 Spring 管理
异常被 catch表现为不回滚Spring 感知不到异常继续抛异常
抛 checked exception表现为不回滚默认不回滚受检异常rollbackFor = Exception.class
多数据源事务管理器错误管错数据源指定 transactionManager
新线程 / @Async事务线程绑定不跨线程使用事务
MQ / Redis / 文件没回滚不是事务只管数据库本地消息表 / 最终一致性
REQUIRES_NEW 不回滚不是独立事务检查传播行为
MyISAM 表数据库不支持事务使用 InnoDB

推荐企业写法

业务 Service 方法建议这样写:

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderMapper orderMapper;
    private final OrderItemMapper orderItemMapper;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        OrderDO order = buildOrder(request);
        orderMapper.insert(order);

        OrderItemDO item = buildOrderItem(order);
        orderItemMapper.insert(item);

        if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException("订单金额必须大于0");
        }
    }
}

业务异常建议继承 RuntimeException

public class BusinessException extends RuntimeException {

    public BusinessException(String message) {
        super(message);
    }
}

最佳实践建议

1. 事务尽量放在 Service 层

不要放在 Controller,也不要放在 Mapper。

推荐结构:

Controller
  ↓
Service:事务边界
  ↓
Mapper / Repository

2. 事务方法保持短小

事务里不要做太多慢操作,比如:

远程 HTTP 调用
大文件上传
复杂计算
MQ 阻塞发送
第三方支付请求

事务时间太长容易造成:

数据库连接占用
锁等待
死锁概率增加
吞吐下降

3. 默认写 rollbackFor = Exception.class

@Transactional(rollbackFor = Exception.class)

这样可以避免受检异常不回滚的问题。


4. 不要在事务里直接发 MQ

尤其是订单、支付、退款这种业务。

推荐:

数据库事务内:
  保存订单
  保存本地消息表

事务提交后:
  异步发送 MQ

5. 避免同类内部事务调用

不要这样:

this.xxx();

涉及事务传播行为时,尽量拆成不同 Service。


一句话总结 🎯

@Transactional 失效主要分两类:

1. 代理没生效:
   自调用、private/final、手动 new、非 Spring Bean、新线程

2. 回滚没触发:
   异常被 catch、checked exception、rollbackFor 配错、传播行为不符合预期

真实项目里最常见的三个坑是:

同类方法自调用
异常被 try-catch 吃掉
没有配置 rollbackFor = Exception.class