文章目录
-
- 一、引言
- 二、前置知识:Spring 事务原理速览
- 三、八大失效场景逐一拆解
-
- 3.1 场景一:数据库引擎不支持事务
- 3.2 场景二:类内部方法调用(最常见!⭐⭐⭐⭐⭐)
- 3.3 场景三:非 public 方法
- 3.4 场景四:异常类型不匹配
- 3.5 场景五:异常被 catch 吞掉(常见!⭐⭐⭐⭐)
- 3.6 场景六:多线程环境
- 3.7 场景七:事务传播行为配置错误
- 3.8 场景八:非 Spring 管理的 Bean 中使用
- 四、快速排查指南
- 五、最佳实践总结
-
- 推荐的事务注解模板
- 代码组织建议
- 单元测试中验证事务
- 六、总结
- 参考

一、引言
在 Spring Boot 项目中使用事务非常简单——加个 @Transactional 就搞定。但"简单"往往意味着"容易出错"。在实际项目中,事务不回滚的 bug 屡见不鲜,究其原因,大多数是对 Spring 事务的底层实现机制理解不够。
Spring 声明式事务的核心依赖是 AOP 动态代理。你用 @Transactional 标记的方法,Spring 会为它生成一个代理对象,在方法执行前后自动开启和提交/回滚事务。理解了这个前提,就能推导出一个关键结论:
只有通过 Spring 代理对象调用的方法,事务注解才会生效。
本文将从这一核心原理出发,梳理 8 大常见失效场景,每个场景附带可运行的代码示例和正确的修复方式。
二、前置知识:Spring 事务原理速览
在深入失效场景之前,先用一张图理解 Spring 事务的工作链路:

核心流程如下:
事务状态(Connection)绑定在 ThreadLocal 上,这解释了为什么多线程场景下事务会失效。
了解这个流程后,下面逐一剖析失效场景。
三、八大失效场景逐一拆解
3.1 场景一:数据库引擎不支持事务
这是最容易被忽略的"低级错误"。MySQL 的 MyISAM 引擎不支持事务,如果你的表是 MyISAM,加再多 @Transactional 也无济于事。
// 错误示例:表使用 MyISAM 引擎
// CREATE TABLE user (id BIGINT, name VARCHAR(50)) ENGINE=MyISAM;
@Service
public class UserService {
@Transactional
public void createUser() {
userMapper.insert(new User(1L, "张三")); // 执行成功
int i = 1 / 0; // 抛出异常
// 预期回滚,但 MyISAM 不支持事务,第一条数据已持久化!
}
}
排查方式:执行 SHOW TABLE STATUS FROM 数据库名 查看 Engine 列,确认是 InnoDB。
修复方式:将表引擎改为 InnoDB。
ALTER TABLE user ENGINE = InnoDB;
3.2 场景二:类内部方法调用(最常见!⭐⭐⭐⭐⭐)
这是实际项目中出现频率最高的事务失效场景。直接看代码:
@Service
public class OrderService {
// ❌ 错误写法:外部调用 createOrder(),事务不生效
public void createOrder(Order order) {
// 非事务方法直接调用同类中的事务方法
this.saveOrder(order); // ← this 调用,绕过了代理!
sendNotify(order);
}
@Transactional
public void saveOrder(Order order) {
orderMapper.insert(order);
// 假设这里抛异常,数据不会回滚!
}
private void sendNotify(Order order) {
// 发送通知…
}
}
原理分析:外部调用 orderService.createOrder() 时,拿到的是 CGLIB 代理对象。createOrder() 没有事务注解,代理对象直接调用目标对象的方法。而目标对象内部的 this.saveOrder() 是 Java 原生的方法调用,它根本不知道代理对象的存在,@Transactional 自然不生效。
- 自调用机制:当类中的方法调用同一个类中的另一个@Transactional 方法时,事务可能不会生效。这是因为事务注解是通过 AOP 实现的,而 Spring 的 AOP 代理机制在这种情况下不会被触发。

三种正确的修复方式:
// ✅ 方式一:调用方也标记 @Transactional(推荐,最简单)
@Service
public class OrderService {
@Transactional // 让外层方法也加入事务管理
public void createOrder(Order order) {
saveOrder(order); // 此时事务已在外层开启,内层复用同一事务
sendNotify(order);
}
public void saveOrder(Order order) {
orderMapper.insert(order);
// 异常会回滚,因为事务在外层已开启
}
private void sendNotify(Order order) { /* … */ }
}
// ✅ 方式二:将事务方法提取到独立的 Service(推荐,职责清晰)
@Service
public class OrderService {
@Autowired
private OrderPersistenceService persistenceService; // 注入另一个 Bean
public void createOrder(Order order) {
persistenceService.saveOrder(order); // ← 跨 Bean 调用,走代理,事务生效
sendNotify(order);
}
}
@Service
public class OrderPersistenceService {
@Transactional
public void saveOrder(Order order) {
orderMapper.insert(order);
// 异常会回滚 ✓
}
}
// ✅ 方式三:通过 AopContext.currentProxy() 获取代理对象
@Service
public class OrderService {
public void createOrder(Order order) {
// 拿到当前类的代理对象,再调用事务方法
((OrderService) AopContext.currentProxy()).saveOrder(order);
sendNotify(order);
}
@Transactional
public void saveOrder(Order order) {
orderMapper.insert(order);
}
}
// 需要在启动类或配置类中开启 exposeProxy:
// @EnableAspectJAutoProxy(exposeProxy = true)
建议:优先使用方法二(提取独立 Service),职责单一、可测试性强。方法一简单但会让外层方法变重,适合逻辑不复杂的场景。
3.3 场景三:非 public 方法
Spring 事务注解默认只对 public 方法生效。如果把 @Transactional 放在 private 或 protected 方法上,不会有任何事务。
@Service
public class UserService {
// ❌ 错误:private 方法上的事务不生效
@Transactional
private void savePrivate(User user) {
userMapper.insert(user);
}
// ❌ 错误:protected 方法上的事务默认也不生效
@Transactional
protected void saveProtected(User user) {
userMapper.insert(user);
}
// ✅ 正确:只能放在 public 方法上
@Transactional
public void save(User user) {
userMapper.insert(user);
}
}
源码依据:Spring 的 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute() 方法中有这样一段逻辑:
// org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// 如果方法不是 public 的,且 allowPublicMethodsOnly() 返回 true(默认为 true)
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null; // ← 直接返回 null,不应用事务
}
// …
}
Spring 团队的设计理由是:非 public 方法不应该暴露为事务边界。如果你确实有特殊需求,可以自定义 TransactionAttributeSource,但强烈不推荐。
3.4 场景四:异常类型不匹配
@Transactional 默认只对 RuntimeException 和 Error 进行回滚。当你抛出一个 checked exception(如 IOException、自定义受检异常)时,事务不会回滚。
@Service
public class UserService {
// ❌ 错误:抛出 checked exception,事务不会回滚
@Transactional
public void batchImport(InputStream input) throws IOException {
userMapper.insert(new User(1L, "张三"));
// 模拟读取异常
if (input == null) {
throw new IOException("文件读取失败"); // ← checked exception,默认不回滚!
}
}
// ✅ 正确:显式指定 rollbackFor
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void batchImportCorrect(InputStream input) throws IOException {
userMapper.insert(new User(2L, "李四"));
if (input == null) {
throw new IOException("文件读取失败"); // 这次会回滚 ✓
}
}
// ✅ 更精确的写法:只回滚你关心的特定异常
@Transactional(rollbackFor = {IOException.class, BusinessException.class})
public void batchImportPrecise(InputStream input) throws IOException {
// …
}
}
最佳实践:项目中统一使用 @Transactional(rollbackFor = Exception.class),或者在全局配置中设置默认回滚策略,避免遗漏。
3.5 场景五:异常被 catch 吞掉(常见!⭐⭐⭐⭐)
这是仅次于"内部方法调用"的高频失效场景。你把异常 catch 住了但没重新抛出,事务管理器根本不知道发生了异常,自然就不会回滚。
@Service
public class OrderService {
// ❌ 错误:catch 后吞掉异常,事务不回滚
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0; // 抛出 ArithmeticException
// 其他业务逻辑…
} catch (Exception e) {
log.error("创建订单失败", e);
// ← 异常被吞了,方法正常返回,Spring 提交事务!
}
}
}
为什么会这样:Spring 的事务拦截器在目标方法执行后,检查是否有未捕获的异常从方法中抛出。try-catch 兜底后异常不再往外抛,拦截器认为方法正常结束,于是 commit。
三种正确的修复方式:
@Slf4j
@Service
public class OrderService {
// ✅ 方式一:catch 之后重新抛出(运行时异常)
@Transactional
public void createOrderV1(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0;
} catch (Exception e) {
log.error("创建订单失败", e);
throw new RuntimeException("创建订单失败", e); // 重新抛出
}
}
// ✅ 方式二:手动标记回滚(适合需要返回错误码而非抛异常的场景)
@Transactional
public void createOrderV2(Order order) {
try {
orderMapper.insert(order);
int i = 1 / 0;
} catch (Exception e) {
log.error("创建订单失败", e);
// 手动标记当前事务需要回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 此时可以返回业务错误码,不需要抛异常
}
}
// ✅ 方式三:精确 catch,只处理能处理的异常(推荐)
@Transactional
public void createOrderV3(Order order) {
try {
orderMapper.insert(order);
// 调用外部服务,允许失败
remoteNotifyService.send(order);
} catch (RemoteNotifyException e) {
// 通知服务失败不应该回滚订单事务
log.warn("通知发送失败,但订单正常创建", e);
// 不重新抛出 = 通知失败不影响订单持久化
}
// RuntimeException 不要 catch,让它自然抛出触发回滚
}
}
核心原则:只有你明确知道某个异常不应该导致事务回滚时,才 catch 它。如果你不确定,不要 catch,让它自然抛到代理层。
3.6 场景六:多线程环境
@Transactional 的事务状态通过 ThreadLocal 绑定到当前线程。当你开启新线程执行数据库操作时,新线程里没有事务上下文,自然也享受不到事务保护。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// ❌ 错误:新线程中的方法没有事务
@Transactional
public void createOrderAsync(Order order) {
orderMapper.insert(order); // 这条在当前线程的事务中
// 在子线程中执行数据库操作——事务不传递!
new Thread(() -> {
orderMapper.insertDetail(order.getDetail()); // ← 子线程,事务不生效!
}).start();
// 模拟主线程异常
int i = 1 / 0;
// 结果:order 会回滚,但 orderDetail 已经写入了(在子线程的独立连接中)
}
}
为什么会这样:主线程的事务是把数据库连接(Connection)存在 ThreadLocal 中。子线程是一个全新的 Thread,它的 ThreadLocal 是空的,所以 orderMapper.insertDetail() 会从连接池拿一个新的 Connection,以非事务模式(auto-commit)执行。即使主线程回滚了,子线程写入的数据早已持久化。
更隐蔽的变体(使用 @Async):
@Slf4j
@Service
public class OrderService {
@Autowired
private OrderDetailService detailService;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// @Async 方法在独立线程池中执行,事务不传递!
detailService.saveDetailAsync(order.getDetail());
int i = 1 / 0;
// 同样:order 回滚,detail 不回滚
}
}
@Service
class OrderDetailService {
@Async // ← 异步 = 新线程,事务不传递
@Transactional // ← 这个事务在新的独立线程中,与主线程事务无关
public void saveDetailAsync(OrderDetail detail) {
detailMapper.insert(detail);
}
}
正确做法:
// ✅ 方式一:如果需要保证原子性,不要用多线程
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
orderMapper.insertDetail(order.getDetail()); // 同步执行,共享同一事务
// 两条数据同时成功或同时回滚 ✓
}
// ✅ 方式二:使用分布式事务方案(Seata、RocketMQ 事务消息等)
// ✅ 方式三:改成最终一致性方案——主事务提交后,通过消息队列异步处理
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
orderMapper.insertDetail(order.getDetail());
// 事务提交成功后再发消息
// TransactionSynchronizationManager.registerSynchronization(…)
}
3.7 场景七:事务传播行为配置错误
Spring 提供了 7 种传播行为(Propagation),配置不当会让事务"看起来失效"。
@Service
public class UserService {
@Autowired
private LogService logService;
// ❌ 错误:外部事务存在时,logService 挂起事务再执行,插入的日志不回滚
@Transactional
public void deleteUser(Long userId) {
userMapper.delete(userId);
logService.recordDeleteLog(userId); // 调用另一个事务方法
int i = 1 / 0; // 异常!user 删除会回滚,但日志已写入
}
}
@Service
class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // ← 挂起当前事务,新建独立事务
public void recordDeleteLog(Long userId) {
logMapper.insert(new OperationLog(userId, "DELETE"));
// 这个事务已提交,外部回滚不影响它
}
}
这不是"失效",而是"设计如此"。Propagation.REQUIRES_NEW 的语义就是新建一个独立的物理事务。你需要根据业务需求选择合适的传播行为:
| REQUIRED(默认) | 有事务则加入,没有则新建 | 绝大多数场景 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 | 日志记录、审计等不应因业务回滚而撤销的操作 |
| NESTED | 嵌套事务,内部回滚不影响外部 | 部分回滚时,需要保存点(savepoint) |
| SUPPORTS | 有事务就加入,没有就以非事务运行 | 查询为主的只读操作 |
| NOT_SUPPORTED | 以非事务方式运行,挂起当前事务 | 不需要事务的操作 |
| MANDATORY | 必须在事务中运行,否则抛异常 | 强制调用方提供事务 |
| NEVER | 必须在非事务中运行,否则抛异常 | 强制无事务执行 |
排查技巧:开启 Spring 事务日志,观察事务的创建和提交/回滚行为:
# application.yml 或 application.properties
logging:
level:
org.springframework.transaction.interceptor: DEBUG
org.springframework.orm.jpa: DEBUG
3.8 场景八:非 Spring 管理的 Bean 中使用
@Transactional 只在 Spring 容器管理的 Bean 中生效。以下几种情况注解会"静默失效":
// ❌ 情况1:直接 new 出来的对象
public class SomeController {
@Autowired
private ApplicationContext context;
public void handle() {
// 直接 new,不在 Spring 容器中,@Transactional 无效
OrderService service = new OrderService();
service.createOrder(order); // 事务不生效!
}
}
// ❌ 情况2:在 @Configuration 类的方法上加 @Transactional
@Configuration
public class AppConfig {
@Transactional // ← 无效!配置类的 Bean 初始化方法不是业务调用入口
public DataSource dataSource() {
return new HikariDataSource();
}
}
// ❌ 情况3:工具类的 static 方法
public class OrderUtils {
@Transactional // ← 无效!static 方法不能被 AOP 代理拦截
public static void saveOrder(Order order) {
// …
}
}
根本原因:Spring AOP 代理只能拦截 Spring 容器中 Bean 的实例方法调用。new 出来的对象、static 方法、配置类的 @Bean 方法都不满足这个前提。
四、快速排查指南
遇到事务不生效时,按以下清单逐项自查:
| ① 数据库引擎 | SHOW TABLE STATUS 确认 InnoDB |
| ② 调用链路 | 是否跨 Bean 调用?是否用了 this.? |
| ③ 方法修饰符 | 方法是否为 public? |
| ④ 异常类型 | 是否 catch 吞了?抛的是否 RuntimeException? |
| ⑤ 线程环境 | 是否涉及 new Thread() 或 @Async? |
| ⑥ 传播行为 | @Transactional 的 propagation 属性是否正确? |
| ⑦ Bean 管理 | 类是否由 Spring 容器管理?是否是 new 出来的? |
| ⑧ 事务日志 | 开启 DEBUG 日志观察事务行为 |
最直接的诊断手段——开启日志:
logging:
level:
org.springframework.transaction.interceptor: DEBUG
你会看到类似这样的输出:
Creating new transaction with name [com.example.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Opened new Connection [conn1] for JDBC transaction
Initiating transaction commit
如果加了 @Transactional 但日志里完全没有 Creating new transaction,说明注解没生效,按清单排查。
五、最佳实践总结
推荐的事务注解模板
@Service
@Slf4j
public class OrderService {
/**
* 标准事务方法模板
* – rollbackFor = Exception.class:确保任何异常都回滚
* – timeout:防止长事务锁表
* – readOnly = true:只读操作标记,提升性能
*/
@Transactional(
rollbackFor = Exception.class,
timeout = 30,
readOnly = true)
public Order getOrder(Long orderId) {
return orderMapper.selectById(orderId);
}
@Transactional(
rollbackFor = Exception.class,
timeout = 30)
public void createOrder(Order order) {
// 写操作…
}
}
代码组织建议
单元测试中验证事务
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderMapper orderMapper;
@Test
@DisplayName("异常时应回滚订单数据")
void shouldRollbackOnException() {
// 使用 assertThrows 确认异常被抛出
assertThrows(RuntimeException.class, () -> {
orderService.createOrderWithException(); // 内部会抛异常
});
// 事务回滚后,数据库中应查不到数据
List<Order> orders = orderMapper.selectAll();
assertTrue(orders.isEmpty(), "事务应回滚,表应为空");
}
}
六、总结
Spring 声明式事务"失效"的根本原因只有一句话:事务的方法没有被 Spring 的代理对象拦截到。围绕这条主线,本文梳理了 8 个典型的失效场景:
下次遇到事务不回滚的问题,对照这份清单逐一排查,配合 DEBUG 级别日志,通常几分钟就能定位到根因。
参考
- Spring 官方文档 – Transaction Management
- Spring Transaction 源码分析(AbstractPlatformTransactionManager)
- https://juejin.cn/post/7482620993283768361
- https://www.cnblogs.com/haof31/p/19164274
- Spring 声明式事务:原理、使用及失效场景详解_spring声明式事务-CSDN博客




