简单答法:
-
方法内的自调用:Spring 事务是基于 AOP 的,只要使用代理对象调用某个方法时,Spring 事务才能生效,而在一个方法中调用使用 this.xxx () 调用方法时,this 并不是代理对象,所以会导致事务失效。a.解决办法 1:把调用方法拆分到另外一个 Bean 中b.解决办法 2:自己注入自己c.解决办法 3:AopContext.currentProxy ()+@EnableAspectJAutoProxy (exposeProxy = true)
-
方法是 private 的:Spring 事务会基于 CGLIB 来进行 AOP,而 CGLIB 会基于父子类来实现代理,子类是代理类,父类是被代理类,如果父类中的某个方法是 private 的,那么子类就没有办法重写它,也就没有办法额外增加 Spring 事务的逻辑。
-
方法是 final 的:原因和 private 是一样的,也是由于子类不能重写父类中的 final 的方法
-
单独的线程调用方法:当 Mybatis 或 JdbcTemplate 执行 SQL 时,会从 ThreadLocal 中去获取数据库连接对象,如果开启事务的线程和执行 SQL 的线程是同一个,那么就能拿到数据库连接对象;如果不是同一个线程,那就拿不到数据库连接对象,这样,Mybatis 或 JdbcTemplate 就会自己去新建一个数据库连接用来执行 SQL,此数据库连接的 autocommit 为 true,那么执行完 SQL 就会提交,后续再抛异常也就不能再回滚之前已经提交了的 SQL 了。
-
没加 @Configuration 注解:如果用 SpringBoot 基本没有这个问题,但是如果用的 Spring,那么可能会有这个问题。这个问题的原因其实也是由于 Mybatis 或 JdbcTemplate 会从 ThreadLocal 中去获取数据库连接,但是 ThreadLocal 中存储的是一个 MAP,MAP 的 key 为 DataSource 对象,value 为连接对象;而如果我们没有在 AppConfig 上添加 @Configuration 注解的话,会导致 MAP 中存的 DataSource 对象和 Mybatis、JdbcTemplate 中的 DataSource 对象不相等,从而也拿不到数据库连接,导致自己去创建数据库连接了。
-
异常被吃掉:如果 Spring 事务没有捕获到异常,那么也就不会回滚了,默认情况下 Spring 会捕获 RuntimeException 和 Error。
-
类没有被 Spring 管理
-
数据库不支持事务
详细的答法
Spring 事务失效场景 & 解决方案清单
1. 场景:方法内自调用(this.xxx ())
- 场景描述 :在同一个类的方法中,用
this.xxx()调用其他事务方法,事务不生效。 - 原因分析 :Spring 事务基于 AOP,仅当代理对象 调用方法时事务才会增强;
this是原对象而非代理对象,无法触发事务逻辑。 - 解决方案 :
- 把被调用方法拆分到另一个 Spring Bean 中,通过 Bean 注入调用;
- 在当前类中自注入自身(如
@Autowired private 当前类名 thisBean;,通过thisBean.xxx()调用); - 结合
AopContext.currentProxy()+ 在配置类加@EnableAspectJAutoProxy(exposeProxy = true),通过((当前类名)AopContext.currentProxy()).xxx()调用。
2. 场景:方法为 private 修饰
- 场景描述 :被
private修饰的方法,事务不生效。 - 原因分析 :Spring 事务(CGLIB 代理)基于 "父子类继承" 实现增强,子类无法重写父类的
private方法,因此无法添加事务逻辑。 - 解决方案 :避免用
private修饰需要事务的方法,改用public/protected。
3. 场景:方法为 final 修饰
- 场景描述 :被
final修饰的方法,事务不生效。 - 原因分析 :CGLIB 代理的子类无法重写父类的
final方法,无法增强事务逻辑。 - 解决方案 :避免用
final修饰需要事务的方法。
4. 场景:跨线程调用事务方法
- 场景描述:开启事务的线程,与执行 SQL 的线程不是同一个,事务无法回滚。
- 原因分析 :数据库连接存在
ThreadLocal中(线程私有),跨线程会拿不到事务连接,Mybatis/JdbcTemplate 会新建autocommit=true的连接,SQL 执行后直接提交,后续异常无法回滚。 - 解决方案 :保证事务内的 SQL 操作与事务开启在同一个线程中,避免异步线程执行 SQL。
5. 场景:Spring 环境未加 @Configuration 注解
- 场景描述 :Spring(非 SpringBoot)环境下,配置类未加
@Configuration,事务失效。 - 原因分析 :
ThreadLocal中存储的连接以DataSource为 key,若配置类无@Configuration,会导致事务的DataSource与 Mybatis/JdbcTemplate 的DataSource不是同一个对象,拿不到事务连接,进而新建自动提交的连接。 - 解决方案 :在 Spring 的配置类上添加
@Configuration注解。
6. 场景:异常被 "吃掉"
- 场景描述:事务方法内的异常被捕获但未抛出,事务不回滚。
- 原因分析 :Spring 事务默认仅捕获
RuntimeException和Error;若异常被try-catch捕获且未重新抛出,Spring 无法感知异常,不会触发回滚。 - 解决方案:不要捕获事务方法内的异常(或捕获后重新抛出),确保异常被 Spring 感知。
7. 场景:类未被 Spring 管理
- 场景描述:类未被 Spring 容器托管,事务不生效。
- 原因分析:Spring 事务基于容器管理的 Bean 生成代理对象,非托管类没有代理,无法触发事务逻辑。
- 解决方案 :给类添加
@Service/@Component等 Spring 组件注解,使其被容器扫描管理。
8. 场景:数据库不支持事务
- 场景描述:数据库本身不支持事务,事务失效。
- 原因分析:数据库引擎(如 MySQL 的 MyISAM)不具备事务特性。
- 解决方案:更换支持事务的数据库引擎(如 MySQL 的 InnoDB)。
结合代码:
1. 场景:方法内自调用(this.xxx ())
失效代码示例
java
运行
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
// 非事务方法
public void updateUserInfo(String userId, String userName) {
// 内部this调用事务方法,事务失效
this.updateUserName(userId, userName);
}
// 事务方法(被this调用,不生效)
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作(即使抛异常也不会回滚)
// userMapper.updateUserName(userId, userName);
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
修复代码示例(三种方案可选)
方案 1:拆分到另一个 Bean
java
运行
// 1. 新建被调用Bean
@Service
public class UserTxService {
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
// 2. 原Bean注入调用
@Service
public class UserService {
@Autowired
private UserTxService userTxService; // 注入新Bean
public void updateUserInfo(String userId, String userName) {
// 调用其他Bean的事务方法,事务生效
userTxService.updateUserName(userId, userName);
}
}
方案 2:自注入自身
java
运行
@Service
public class UserService {
@Autowired
private UserService userService; // 自注入自身
public void updateUserInfo(String userId, String userName) {
// 用自注入的Bean调用,事务生效
userService.updateUserName(userId, userName);
}
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
方案 3:AopContext.currentProxy ()
java
运行
// 1. 配置类开启暴露代理
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class SpringConfig {
}
// 2. 业务类调用
@Service
public class UserService {
public void updateUserInfo(String userId, String userName) {
// 获取代理对象并调用,事务生效
UserService proxyService = (UserService) AopContext.currentProxy();
proxyService.updateUserName(userId, userName);
}
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
2. 场景:方法为 private 修饰
失效代码示例
java
运行
@Service
public class UserService {
public void updateUser(String userId, String userName) {
this.updateUserName(userId, userName);
}
// private修饰,事务失效
@Transactional(rollbackFor = Exception.class)
private void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
修复代码示例
java
运行
@Service
public class UserService {
public void updateUser(String userId, String userName) {
this.updateUserName(userId, userName);
}
// 改为public修饰,事务生效
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
3. 场景:方法为 final 修饰
失效代码示例
java
运行
@Service
public class UserService {
// final修饰,事务失效
@Transactional(rollbackFor = Exception.class)
public final void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
修复代码示例
java
运行
@Service
public class UserService {
// 移除final修饰,事务生效
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
4. 场景:跨线程调用事务方法
失效代码示例
java
运行
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
public void updateUserAsync(String userId, String userName) {
// 新开线程调用事务方法,事务失效
new Thread(() -> {
updateUserName(userId, userName);
}).start();
}
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作(跨线程无法回滚)
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
修复代码示例
java
运行
@Service
public class UserService {
// 保证事务方法与SQL执行在同一个线程,事务生效
public void updateUserAsync(String userId, String userName) {
// 直接调用(不跨线程)
updateUserName(userId, userName);
// 若需异步,需保证事务在异步线程内开启(示例)
// CompletableFuture.runAsync(() -> {
// // 异步线程内直接执行事务方法(自身就是代理调用)
// updateUserName(userId, userName);
// });
}
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
5. 场景:Spring 环境未加 @Configuration 注解
失效代码示例
java
运行
// 无@Configuration注解,导致DataSource不一致,事务失效
public class SpringConfig {
@Bean
public DataSource dataSource() {
// 配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
修复代码示例
java
运行
import org.springframework.context.annotation.Configuration;
// 添加@Configuration注解,事务生效
@Configuration
public class SpringConfig {
@Bean
public DataSource dataSource() {
// 配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
6. 场景:异常被 "吃掉"(捕获未抛出)
失效代码示例
java
运行
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
try {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
} catch (Exception e) {
// 捕获异常但未抛出,Spring无法感知,事务不回滚
System.out.println("异常信息:" + e.getMessage());
}
}
}
修复代码示例
java
运行
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
try {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
} catch (Exception e) {
System.out.println("异常信息:" + e.getMessage());
// 重新抛出异常,让Spring感知,触发事务回滚
throw new RuntimeException(e);
// 或抛出指定异常:throw new BusinessException("更新失败", e);
}
}
}
7. 场景:类未被 Spring 管理
失效代码示例
java
运行
// 无Spring组件注解,未被容器托管,事务失效
public class UserService {
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
// 手动new对象调用,事务不生效
public class Test {
public static void main(String[] args) {
UserService userService = new UserService(); // 非Spring容器创建
userService.updateUserName(null, "张三");
}
}
修复代码示例
java
运行
import org.springframework.stereotype.Service;
// 添加@Service注解,被Spring容器托管
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class)
public void updateUserName(String userId, String userName) {
// 数据库更新操作
if (userId == null) {
throw new RuntimeException("用户ID不能为空");
}
}
}
// 通过Spring容器注入调用,事务生效
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
UserService userService = context.getBean(UserService.class); // 容器获取Bean
userService.updateUserName(null, "张三");
}
}
8. 场景:数据库不支持事务
失效说明
该场景无代码层面问题,核心是数据库引擎不支持事务(如 MySQL MyISAM 引擎),即使代码配置正确,事务也无法生效。
修复方案(数据库配置)
-
查看 MySQL 表引擎
sql
-- 查看表的引擎类型 SHOW CREATE TABLE user; -
修改为 InnoDB 引擎(支持事务)
sql
-- 修改表引擎 ALTER TABLE user ENGINE = InnoDB; -- 全局配置(新建表默认InnoDB) -- 修改my.cnf配置文件 default-storage-engine = InnoDB