前言:从 2 个新手灵魂提问说起

今天开发时踩了个大坑:在ServiceImpl方法上加了@Transactional,数据插入失败却没回滚?带着疑问查了半天,发现居然是方法没加public!顺着这个问题,我又延伸出新困惑:内部提炼的方法要不要加public?同一类里方法自调用为啥事务失效?

结合查资料和实战验证,我把 Java(Spring)事务失效的核心场景、解决方案和关键疑问整理成这篇指南,新手看完直接避坑,再也不用为事务 “失灵” 头秃~

一、先解答 2 个核心疑问(新手最关心)

疑问 1:事务方法必须加 public 吗?

必须加!否则事务 100% 失效原因:Spring 事务默认基于「AOP 动态代理」实现,不管是 JDK 动态代理还是 CGLIB 代理,都只能拦截public修饰的方法。非 public 方法(private、protected、default)无法被代理拦截,@Transactional注解相当于 “摆设”。

疑问 2:内部提炼的方法需要加 public 吗?

分场景判断,不用一刀切:

  1. 仅内部自调用(不对外暴露):不用加 public,用private/protected即可(符合封装原则)。事务是否生效,只看「发起调用的外层方法」是否为public+@Transactional
    @Service
    public class UserServiceImpl {
        // 外层方法:public+@Transactional(事务生效)
        @Override
        @Transactional
        public void saveUser(User user) {
            this.validateUser(user); // 内部private方法,无需public
            userMapper.insert(user);
        }
        
        // 内部校验方法:仅内部调用,private足够
        private void validateUser(User user) {
            if (user.getName() == null) {
                throw new IllegalArgumentException("用户名不能为空");
            }
        }
    }
    
  2. 需被其他类调用 / 单独触发事务:必须加public!若需要独立事务,还要单独加@Transactional(其他类调用时会走 Spring 代理,事务才生效)。

二、事务失效 10 大场景(带原因 + 示例 + 解决方案)

场景 1:方法非 public 修饰(最基础坑)

  • 原因:AOP 动态代理无法拦截非 public 方法,@Transactional失效;
  • 错误示例:private void saveData() { ... } 加@Transactional
  • 解决方案:事务方法统一加public修饰。

场景 2:同一类方法内部自调用(高频坑)

  • 原因:A 方法(无事务)调用本类 B 方法(有@Transactional)时,用this调用的是 “原始对象”,未经过 Spring 代理,事务无法触发;
  • 错误示例:
    @Service
    public class UserServiceImpl {
        public void methodA() {
            this.methodB(); // 自调用,事务失效
        }
        
        @Transactional
        public void methodB() {
            userMapper.insert(user);
        }
    }
    
  • 解决方案(3 种,优先选前 2 种):
    1. 拆分类(最推荐):把 B 方法抽到新的 Service 类,通过@Autowired注入调用;
      // 新Service类
      @Service
      public class UserHelperService {
          @Transactional
          public void methodB(User user) {
              userMapper.insert(user);
          }
      }
      
      // 原Service类
      @Service
      public class UserServiceImpl {
          @Autowired
          private UserHelperService helperService;
          
          public void methodA(User user) {
              helperService.methodB(user); // 代理调用,事务生效
          }
      }
      
    2. 从 Spring 上下文拿代理对象:实现ApplicationContextAware接口,获取自身代理对象;
      @Service
      public class UserServiceImpl implements ApplicationContextAware {
          private ApplicationContext applicationContext;
          
          @Override
          public void setApplicationContext(ApplicationContext ctx) {
              this.applicationContext = ctx;
          }
          
          public void methodA(User user) {
              // 获取代理对象调用
              UserServiceImpl proxy = applicationContext.getBean(UserServiceImpl.class);
              proxy.methodB(user);
          }
          
          @Transactional
          public void methodB(User user) {
              userMapper.insert(user);
          }
      }
      
    3. 自注入(懒人写法):@Autowired注入自身代理对象(谨慎使用,可读性稍差)。

场景 3:事务传播机制配置错误

  • 原因:传播机制决定事务嵌套规则,以下配置会主动关闭事务;
  • 失效传播机制:
    • Propagation.NOT_SUPPORTED:挂起当前事务,以非事务方式执行;
    • Propagation.NEVER:存在事务则抛异常,拒绝执行;
    • Propagation.SUPPORTS:无事务时以非事务执行(不主动开事务);
  • 解决方案:默认用Propagation.REQUIRED(存在事务则加入,无则新建,无需手动配置)。

场景 4:异常被捕获未抛出(异常 “被吞”)

  • 原因:Spring 事务默认仅在 “未处理的 RuntimeException/Error” 时回滚;若用try-catch捕获异常且未重新抛出,事务无法感知;
  • 错误示例:
    @Transactional
    public void saveUser(User user) {
        try {
            userMapper.insert(user);
            int i = 1 / 0; // 模拟异常
        } catch (Exception e) {
            e.printStackTrace(); // 仅打印,未抛出
        }
    }
    
  • 解决方案:捕获后重新抛出异常(throw new RuntimeException(e)),或用TransactionStatus.setRollbackOnly()手动回滚。

场景 5:捕获了非指定异常类型

  • 原因:默认不处理 “受检异常”(如 IOException、SQLException);若@Transactional指定rollbackFor,实际异常不在范围内则不回滚;
  • 错误示例:
    @Transactional(rollbackFor = RuntimeException.class)
    public void saveUser() throws IOException {
        throw new IOException("文件读取失败"); // 事务不回滚
    }
    
  • 解决方案:rollbackFor = Exception.class(兜底所有异常,新手推荐)。

场景 6:未配置事务管理器

  • 原因:Spring 事务需要绑定PlatformTransactionManager(如DataSourceTransactionManager),未配置则注解 “空转”;
  • 必备配置:
    @Configuration
    public class TransactionConfig {
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
  • 注意:SpringBoot 项目若引入spring-boot-starter-jdbc,会自动配置,无需手动写。

场景 7:类未被 Spring 管理(非 Bean)

  • 原因:只有加@Service/@Component等注解、被 Spring 扫描注册为 Bean 的类,@Transactional才会被 AOP 代理;
  • 错误示例:UserService service = new UserServiceImpl(); service.saveUser(user);(手动 new 对象,非 Spring 管理);
  • 解决方案:通过@Autowired注入对象,禁止手动new

场景 8:数据库存储引擎不支持事务

  • 原因:事务最终依赖数据库支持,MySQL 的MyISAM引擎不支持事务(已废弃),InnoDB才支持;
  • 检查方式:执行show table status like '表名',查看Engine字段是否为InnoDB
  • 解决方案:建表时指定引擎ENGINE=InnoDB

场景 9:多线程调用(事务上下文不共享)

  • 原因:Spring 事务上下文存在ThreadLocal中,仅对当前线程生效;子线程的数据库操作不纳入主线程事务;
  • 错误示例:
    @Transactional
    public void saveUser(User user) {
        new Thread(() -> userMapper.insert(user)).start(); // 子线程操作不回滚
    }
    
  • 解决方案:避免多线程中执行事务操作,若必须用,需用分布式事务(如 Seata)。

场景 10:事务方法被 final/static 修饰

  • 原因:final 方法无法被 AOP 动态代理重写,static 方法不属于对象实例(代理无法拦截);
  • 错误示例:public final void saveUser(...) { ... } 加@Transactional
  • 解决方案:事务方法避免用 final/static 修饰。

三、新手避坑 3 大核心原则(记牢少踩坑)

  1. 修饰符原则:事务方法必加public,内部自调用方法用private
  2. 异常原则:要么不捕获异常,要么捕获后重新抛出,rollbackFor = Exception.class兜底;
  3. 代理原则:调用事务方法必须走 Spring 代理(@Autowired注入 / 上下文获取),禁止this自调用、手动new对象。

结语

Spring 事务失效的本质就两类:事务未被正确代理(非 public、自调用、非 Bean 等)、执行逻辑跳出事务控制(异常被吞、多线程、传播机制错误等)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐