@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
也就是运行时异常和错误。
对于 Exception、IOException、SQLException 这种受检异常,默认不会回滚。
错误示例
@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