Java实用注解篇:@Transactional 事务失效的场景深度解析
前言
在使用 @Transactional
时,很多开发者都会遇到一个常见困惑:明明加了事务注解,但事务却没有生效,数据库操作仍然被提交了!
这是因为事务机制的触发有一些 前提条件,只要触碰到事务失效的“雷区”,就会让事务变成“摆设”。接下来,我们来详细剖析几种常见的 事务失效场景 以及 解决方案!
✅ 1️⃣ 同一个类中方法直接调用,事务失效
问题复现:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void methodA() {
userRepository.save(new User("UserA"));
methodB(); // 内部调用,不触发事务
}
@Transactional
public void methodB() {
userRepository.save(new User("UserB"));
throw new RuntimeException("Test rollback"); // 期望回滚
}
}
执行结果:
- UserA 会被插入数据库
- UserB 没有插入,因为抛出了异常
- 🚨 事务没有回滚!
为什么会失效?
Spring 的事务是基于 AOP 代理机制 实现的,而代理对象只有在 外部调用 时才会触发拦截器逻辑,从而开启事务。
同一个类内的方法调用不会经过代理对象,而是通过 this.methodB()
调用的,因此不会触发事务机制!
✅ 解决方案:
方案一:通过代理对象调用事务方法
@Autowired
private UserService userService;
@Transactional
public void methodA() {
userRepository.save(new User("UserA"));
userService.methodB(); // 通过代理对象调用,触发事务
}
方案二:使用 AopContext
获取当前代理对象
Spring 提供了 AopContext
工具类,可以获取当前代理对象:
import org.springframework.aop.framework.AopContext;
@Transactional
public void methodA() {
userRepository.save(new User("UserA"));
((UserService) AopContext.currentProxy()).methodB();
}
注意:
- 需要在配置类中开启 暴露代理对象功能 才能使用
AopContext
:
@EnableAspectJAutoProxy(exposeProxy = true)
✅ 2️⃣ 非 public
方法上的 @Transactional 注解无效
问题复现:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
private void addUser() {
userRepository.save(new User("PrivateUser"));
throw new RuntimeException("Test rollback");
}
}
执行结果:
- 数据成功插入,事务 没有回滚!
为什么会失效?
Spring 的事务是基于 动态代理机制,只有 public
方法 才会被代理,private
、protected
、default
修饰的方法不会被代理,因此不会触发事务机制。
✅ 解决方案:
改为 public
方法:
@Transactional
public void addUser() {
userRepository.save(new User("PublicUser"));
throw new RuntimeException("Test rollback");
}
✅ 3️⃣ 数据库引擎不支持事务
问题复现:
如果你的数据库表使用的是 MySQL 的 MyISAM 引擎 而不是 InnoDB,事务机制是不会生效的!
检查表引擎:
SHOW TABLE STATUS WHERE Name = 'your_table';
如果看到 Engine=MyISAM
:
+------------+--------+ ...
| Name | Engine | ...
+------------+--------+ ...
| user_table | MyISAM | ...
+------------+--------+ ...
为什么会失效?
- MyISAM 是 MySQL 的早期存储引擎,不支持事务回滚、外键等特性。
- InnoDB 才是支持事务、行级锁等功能的存储引擎。
✅ 解决方案:
把表引擎改为 InnoDB:
ALTER TABLE your_table ENGINE=InnoDB;
查看所有 MyISAM 表:
SELECT table_schema, table_name, engine
FROM information_schema.tables
WHERE engine = 'MyISAM';
✅ 4️⃣ 捕获了异常,导致事务无法回滚
问题复现:
@Transactional
public void addUser() {
try {
userRepository.save(new User("TryCatchUser"));
int result = 1 / 0; // 抛出 ArithmeticException
} catch (Exception e) {
// 异常被捕获
System.out.println("Exception caught: " + e.getMessage());
}
}
执行结果:
- 数据被插入
- 🚨 事务没有回滚!
为什么会失效?
- Spring 默认只有 未捕获的运行时异常(继承自
RuntimeException
)才会触发回滚。 - 受检异常(如
IOException
)或者 被捕获的异常 不会触发事务回滚!
✅ 解决方案:
方案一:手动抛出异常,让 Spring 感知到事务需要回滚:
@Transactional
public void addUser() {
try {
userRepository.save(new User("User"));
int result = 1 / 0;
} catch (Exception e) {
throw new RuntimeException(e); // 抛出运行时异常触发回滚
}
}
方案二:使用 rollbackFor
明确指定哪些异常触发回滚:
@Transactional(rollbackFor = Exception.class)
public void addUser() {
try {
userRepository.save(new User("User"));
int result = 1 / 0;
} catch (Exception e) {
// 异常依然被捕获,但事务仍然会回滚
System.out.println("Caught exception, but rollback still happens!");
}
}
✅ 5️⃣ 多线程环境下事务失效
问题复现:
@Transactional
public void addUser() {
new Thread(() -> {
userRepository.save(new User("AsyncUser"));
throw new RuntimeException("Test rollback");
}).start();
}
执行结果:
- 数据被插入
- 🚨 事务没有回滚!
为什么会失效?
Spring 的事务是 线程绑定的(基于 ThreadLocal
实现),
新线程不会继承原线程的事务上下文,因此新线程无法感知到原线程的事务边界。
✅ 解决方案:
使用 @Async
配合事务:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Async
@Transactional
public void addUserAsync() {
userRepository.save(new User("AsyncUser"));
throw new RuntimeException("Test rollback");
}
}
注意:
- 需要在启动类上加上
@EnableAsync
:
@SpringBootApplication
@EnableAsync
public class Application {
}
✅ 6️⃣ 数据库连接的自动提交未关闭
问题复现:
如果你的数据库连接池配置了 自动提交,那么事务控制会被绕过!
可能的配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test_db
username: root
password: 123456
hikari:
auto-commit: true # 🚨自动提交为true
为什么会失效?
- Spring 事务会通过 DataSource 获取连接,如果连接的
autoCommit=true
,每次 SQL 执行都会立刻提交。 - 事务注解控制的提交/回滚行为被绕过。
✅ 解决方案:
- 确保数据库连接池关闭自动提交:
spring:
datasource:
hikari:
auto-commit: false
- 或者手动关闭自动提交:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
✅ 7️⃣ 代理对象被绕过(比如使用 this 关键字)
问题复现:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void outerMethod() {
userRepository.save(new User("OuterMethod"));
this.innerMethod(); // 🚨不会触发事务
}
@Transactional
public void innerMethod() {
userRepository.save(new User("InnerMethod"));
throw new RuntimeException("Test rollback");
}
}
为什么会失效?
- Spring 的事务是基于代理对象实现的,
this.innerMethod()
是直接调用自身方法,绕过了代理对象。 - 事务拦截器不会生效,因此内部方法不会走事务逻辑。
✅ 解决方案:
- 通过代理对象调用事务方法:
@Autowired
private UserService userService;
public void outerMethod() {
userRepository.save(new User("OuterMethod"));
userService.innerMethod(); // 🚀触发事务
}
✅ 8️⃣ 嵌套事务设置不当
问题复现:
当你有嵌套事务时,如果 传播机制(Propagation) 配置不当,也可能造成事务失效。
例如:
@Transactional
public void outerMethod() {
userRepository.save(new User("Outer"));
innerMethod();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
userRepository.save(new User("Inner"));
throw new RuntimeException("Test rollback");
}
为什么会失效?
@Transactional(propagation = Propagation.NESTED)
Propagation.REQUIRES_NEW
表示开启一个新的事务,
外层事务和内层事务是独立的,即使内层回滚,外层不会受影响。- 所以
Outer
数据仍然会被提交!
✅ 解决方案:
- 如果你期望事务统一回滚,改成默认的
Propagation.REQUIRED
:
@Transactional(propagation = Propagation.REQUIRED)
public void innerMethod() {
userRepository.save(new User("Inner"));
throw new RuntimeException("Test rollback");
}
- 如果你想实现嵌套事务,可以考虑
NESTED
传播机制:
✅ 9️⃣ 事务管理器配置错误
问题复现:
当你使用了多个数据源时,如果事务管理器没有正确配置,也会造成事务失效:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
为什么会失效?
- 如果你的数据源和事务管理器不匹配,比如有多个数据源,但事务管理器只绑定了一个数据源,
其他数据源的事务不会生效。 - 没有声明事务管理器 时,Spring 会使用 默认事务管理器,可能不是你想要的那个。
✅ 解决方案:
- 指定事务管理器:
@Transactional(transactionManager = "transactionManager")
- 多数据源事务管理器配置:
@Bean("transactionManager1")
public PlatformTransactionManager transactionManager1(@Qualifier("dataSource1") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean("transactionManager2")
public PlatformTransactionManager transactionManager2(@Qualifier("dataSource2") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
✅ 🔟 被代理类不是 Spring 管理的 Bean
问题复现:
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void addUser() {
userRepository.save(new User("User"));
throw new RuntimeException("Test rollback");
}
}
调用:
UserService userService = new UserService();
userService.addUser(); // 🚨事务失效
为什么会失效?
- Spring 事务依赖于 AOP 代理,只有被 Spring 托管的 Bean 才会启用代理机制。
- 手动 new 对象不会触发事务机制。
✅ 解决方案:
- 让 Spring 托管 Bean:
@Service
public class UserService {
...
}
- 使用
ApplicationContext
获取代理对象:
@Autowired
private ApplicationContext applicationContext;
public void addUser() {
UserService proxy = applicationContext.getBean(UserService.class);
proxy.addUser(); // 🚀事务生效
}
✅ 1️⃣1️⃣ SpringBoot 的 @EnableTransactionManagement
未生效
问题复现:
如果你在项目中使用的是 SpringBoot,但是 @Transactional
却完全没反应,
可能是因为事务管理器没有启用!
为什么会失效?
- SpringBoot 默认开启事务管理(自动配置),但如果你自己创建了配置类,并关闭了自动配置,就可能导致事务失效。
✅ 解决方案:
- 显式开启事务管理器:
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
}
- 确认事务管理器是否生效:
@Autowired
private PlatformTransactionManager transactionManager;
@PostConstruct
public void checkTransactionManager() {
System.out.println("Transaction Manager: " + transactionManager);
}
✅ 1️⃣2️⃣ 数据库本身的问题
最后,有时候 不是代码问题,而是 数据库层面的配置问题 导致事务失效:
-
数据库权限不足:
如果数据库用户权限不足,无法执行ROLLBACK
操作,也会让事务无法回滚。 -
触发器 (Trigger):
如果表有触发器,会导致某些数据在触发器执行后直接提交,绕过事务管理。
🎯 总结:事务失效场景大全
事务失效场景 | 失效原因 | 解决方案 |
---|---|---|
同类方法调用 | 没有代理 | 通过代理对象调用 |
非 public 方法 | 只有 public 方法才会被代理 | 改为 public |
数据库引擎不支持事务 | 使用 MyISAM 而不是 InnoDB | 修改为 InnoDB |
异常被捕获 | Spring 默认只回滚 RuntimeException | 显式指定 rollbackFor |
多线程调用 | 新线程不会继承原线程的事务 | 使用 @Async 结合事务 |
自动提交未关闭 | 数据源 autoCommit=true | 关闭自动提交 |
代理对象被绕过 | this 调用自身方法 | 使用代理对象调用 |
嵌套事务传播机制设置不当 | REQUIRES_NEW 导致事务隔离 | 使用 NESTED 或 REQUIRED |
事务管理器配置错误 | 数据源和事务管理器不匹配 | 确保事务管理器正确绑定数据源 |
Bean 非 Spring 托管 | new 对象不会触发代理机制 | 让 Spring 托管 Bean |
@EnableTransactionManagement 未开启 | 没有启用事务管理器 | 显式开启事务管理器 |
数据库层面问题 | MySQL 触发器或权限问题 | 检查数据库配置 |
✨ 思考题:
- 如何在同一类方法调用时,确保事务能够生效?
- 为什么捕获异常后事务不会回滚?
- 事务和线程之间的关系是怎样的?
如果觉得这篇文章对你有帮助,记得点赞⭐、收藏📌、关注🚀!