面试题:Spring事务失效的八大场景

简单答法:

  1. 方法内的自调用:Spring 事务是基于 AOP 的,只要使用代理对象调用某个方法时,Spring 事务才能生效,而在一个方法中调用使用 this.xxx () 调用方法时,this 并不是代理对象,所以会导致事务失效。a.解决办法 1:把调用方法拆分到另外一个 Bean 中b.解决办法 2:自己注入自己c.解决办法 3:AopContext.currentProxy ()+@EnableAspectJAutoProxy (exposeProxy = true)

  2. 方法是 private 的:Spring 事务会基于 CGLIB 来进行 AOP,而 CGLIB 会基于父子类来实现代理,子类是代理类,父类是被代理类,如果父类中的某个方法是 private 的,那么子类就没有办法重写它,也就没有办法额外增加 Spring 事务的逻辑。

  3. 方法是 final 的:原因和 private 是一样的,也是由于子类不能重写父类中的 final 的方法

  4. 单独的线程调用方法:当 Mybatis 或 JdbcTemplate 执行 SQL 时,会从 ThreadLocal 中去获取数据库连接对象,如果开启事务的线程和执行 SQL 的线程是同一个,那么就能拿到数据库连接对象;如果不是同一个线程,那就拿不到数据库连接对象,这样,Mybatis 或 JdbcTemplate 就会自己去新建一个数据库连接用来执行 SQL,此数据库连接的 autocommit 为 true,那么执行完 SQL 就会提交,后续再抛异常也就不能再回滚之前已经提交了的 SQL 了。

  5. 没加 @Configuration 注解:如果用 SpringBoot 基本没有这个问题,但是如果用的 Spring,那么可能会有这个问题。这个问题的原因其实也是由于 Mybatis 或 JdbcTemplate 会从 ThreadLocal 中去获取数据库连接,但是 ThreadLocal 中存储的是一个 MAP,MAP 的 key 为 DataSource 对象,value 为连接对象;而如果我们没有在 AppConfig 上添加 @Configuration 注解的话,会导致 MAP 中存的 DataSource 对象和 Mybatis、JdbcTemplate 中的 DataSource 对象不相等,从而也拿不到数据库连接,导致自己去创建数据库连接了。

  6. 异常被吃掉:如果 Spring 事务没有捕获到异常,那么也就不会回滚了,默认情况下 Spring 会捕获 RuntimeException 和 Error。

  7. 类没有被 Spring 管理

  8. 数据库不支持事务

详细的答法

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 事务默认仅捕获RuntimeExceptionError;若异常被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 引擎),即使代码配置正确,事务也无法生效。

修复方案(数据库配置)

  1. 查看 MySQL 表引擎

    sql

    复制代码
    -- 查看表的引擎类型
    SHOW CREATE TABLE user;
  2. 修改为 InnoDB 引擎(支持事务)

    sql

    复制代码
    -- 修改表引擎
    ALTER TABLE user ENGINE = InnoDB;
    
    -- 全局配置(新建表默认InnoDB)
    -- 修改my.cnf配置文件
    default-storage-engine = InnoDB
相关推荐
ss2732 小时前
类的线程安全:多线程编程-银行转账系统:如果两个线程同时修改同一个账户余额,没有适当的保护机制,会发生什么?
java·开发语言·数据库
郑泰科技2 小时前
windows下启动hbase的步骤
数据库·windows·hbase
子一!!2 小时前
MySQL数据库基础操作
数据库·mysql·oracle
DarkAthena2 小时前
【GaussDB】从 sqlplus 到 gsql:Shell 中执行 SQL 文件方案的迁移与改造
数据库·sql·oracle·gaussdb
Wpa.wk2 小时前
接口自动化 - 了解接口自动化框架RESTAssured (Java版)
java·数据库·自动化
wa的一声哭了2 小时前
内积空间 内积空间二
java·开发语言·python·spring·java-ee·django·maven
二等饼干~za8986682 小时前
GEO优化---关键词搜索排名源码开发思路分享
大数据·前端·网络·数据库·django
cike_y2 小时前
Spring使用注解开发
java·后端·spring·jdk1.8
程序员柒叔2 小时前
Dify 集成-向量数据库
数据库·milvus·向量数据库·工作流·dify·向量库