实用指南:Spring事务什么时候会失效?

一、核心前提:Spring事务运行基础

Spring事务基于动态代理实现,仅当通过「Spring容器管理的Bean的代理对象」调用方法时,事务才会生效。所有失效场景的本质,都是破坏了“代理调用链路”或“事务上下文管理”。

二、11大事务失效场景


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

失效原理

Spring事务动态代理(JDK/CGLIB)会过滤非public方法:JDK代理仅能代理接口的public方法,CGLIB虽支持非public,但Spring源码(TransactionAttributeSource类)明确不解析非public方法的@Transactional注解。

失效代码

@Service
public class OrderService {
    // private修饰,事务不生效
    @Transactional
    private void createOrder(Order order) {
        orderMapper.insert(order);
        throw new RuntimeException("异常"); // 数据仍插入,不回滚
    }
}

解决方案

所有加@Transactional的方法,必须用public修饰。


场景2:方法用static/final修饰(阻断代理生成)

失效原理

  • static方法:属于类级方法,不依赖对象实例,而动态代理基于“对象实例”生成,无法代理;
  • final方法:CGLIB代理需生成原类子类并重写方法,final方法不可重写,无法植入事务逻辑。

失效代码

@Service
public class OrderService {
    // static方法,事务失效
    @Transactional
    public static void deleteOrder(Long id) {
        orderMapper.deleteById(id);
    }
    // final方法,事务失效
    @Transactional
    public final void refundOrder(Long id) {
        orderMapper.updateStatus(id, 2);
    }
}

解决方案

移除static/final关键字,确保方法为“public非静态非final”。


场景3:同类方法内部调用(最常见逻辑坑)

失效原理

同一类中,“非事务方法A”调用“事务方法B”时,调用的是「原对象的B方法」,而非「代理对象的B方法」,绕开代理导致事务逻辑未触发。

失效代码

@Service
public class OrderService {
    // 非事务方法
    public void submitOrder(Order order) {
        this.createOrder(order); // 内部调用,没走代理
    }
    // 事务方法(被内部调用,失效)
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        throw new RuntimeException("异常"); // 数据已插入,不回滚
    }
}

解决方案(按优先级)

  1. 拆类:将createOrder放到新Service(如OrderTxService),注入后通过代理调用;
  2. 自注入代理:本类注入自己的代理对象(@Autowired private OrderService proxy),用proxy.createOrder()调用;
  3. 暴露代理:启动类加@EnableAspectJAutoProxy(exposeProxy = true),用(OrderService)AopContext.currentProxy().createOrder()调用。

场景4:未捕获异常(吞掉异常,事务无感知)

失效原理

Spring事务通过“捕获方法抛出的异常”判断是否回滚,若用try-catch捕获异常且未重新抛出,Spring认为“执行成功”,不触发回滚。

失效代码

@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        try {
            orderMapper.insert(order);
            int i = 1 / 0; // 模拟异常
        } catch (Exception e) {
            e.printStackTrace(); // 只打印,不抛异常,事务失效
        }
    }
}

解决方案

捕获异常后重新抛出(如throw new RuntimeException(e)),或配合rollbackFor声明回滚异常。


场景5:异常类型不匹配(默认仅回滚运行时异常)

失效原理

Spring事务默认仅对「RuntimeException」和「Error」回滚,对「Checked异常(如IOException、SQLException)」不回滚,认为其是“可预期的手动处理异常”。

失效代码

@Service
public class OrderService {
    // 抛Checked异常,默认不回滚
    @Transactional
    public void exportExcel() throws IOException {
        orderMapper.exportData();
        throw new IOException("导出失败"); // 数据已提交,不回滚
    }
}

解决方案

通过rollbackFor显式声明回滚异常,推荐覆盖所有异常:@Transactional(rollbackFor = Exception.class)


场景6:数据源未被Spring管理(事务管不到连接)

失效原理

Spring事务管理器依赖“Spring托管的数据源(DataSource)”,若数据源是手动new的(未加@Bean),事务管理器无法获取连接,注解失效。

失效代码

// 错误:数据源未交给Spring管理
public class DataSourceConfig {
    public DataSource getDataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl("jdbc:mysql://localhost/test");
        return ds; // 无@Bean,Spring不管理
    }
    @Bean
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(getDataSource()); // 依赖未托管数据源
    }
}

解决方案

数据源加@Bean交给Spring管理,事务管理器注入Spring托管的数据源:

@Bean
public DataSource dataSource() { /* 配置数据源 */ }
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

场景7:类未被Spring扫描成Bean(无代理对象)

失效原理

仅Spring Bean才会生成动态代理,若类没加@Service/@Component,或不在@ComponentScan范围内,@Transactional被忽略。

失效代码

// 无@Service,Spring不扫描,非Bean
public class OrderService {
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

解决方案

  1. 类加@Service/@Component
  2. 若路径不符,启动类加@ComponentScan(basePackages = "com.xxx.service")

场景8:事务传播属性配置错误(主动放弃事务)

失效原理

传播属性定义多事务嵌套规则,配置“不支持事务”的属性会导致失效,常见错误属性:

  • NOT_SUPPORTED:非事务方式运行,暂停当前事务;
  • NEVER:必须无事务,有则抛异常;
  • SUPPORTS:无外层事务则不启用事务。

失效代码

@Service
public class PayService {
    // 传播属性设为NOT_SUPPORTED,无事务
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void pay(Long orderId) {
        payMapper.insertRecord(orderId);
        throw new RuntimeException("支付异常"); // 数据已插入,不回滚
    }
}

解决方案

日常用默认REQUIRED(有则加入,无则新建),特殊场景用REQUIRES_NEW(新建事务)或NESTED(嵌套事务)。


场景9:数据库引擎不支持事务(底层不支持)

失效原理

Spring事务依赖数据库引擎支持,如MySQL的MyISAM是“非事务引擎”,InnoDB才支持事务。

失效代码

-- MyISAM引擎,无事务
CREATE TABLE `order` (
  `id` bigint PRIMARY KEY AUTO_INCREMENT
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

解决方案

  1. InnoDB引擎(MySQL 5.5+默认);
  2. 已创建表修改引擎:ALTER TABLE order ENGINE=InnoDB;

场景10:事务超时(超时后回滚失效)

失效原理

timeout属性(默认-1,无超时)设为N秒时,方法执行超N秒,Spring强制终止事务并回滚,但长时间SQL可能已执行完,导致回滚失效。

失效代码

@Service
public class OrderService {
    // 超时3秒,SQL执行5秒
    @Transactional(timeout = 3)
    public void batchCreate(List list) {
        for (Order o : list) { // 循环插入耗时5秒
            orderMapper.insert(o);
            Thread.sleep(5);
        }
    }
}

解决方案

  1. 调大timeout(不建议过大,避免占用连接);
  2. 拆分任务(如分批插入,每批提交事务)。

场景11:新线程中ThreadLocal上下文丢失(跨线程事务失效)

失效原理

Spring事务将数据库连接绑定到当前线程的ThreadLocal中,新线程(如new Thread()、线程池)的ThreadLocal为空,无法获取父线程的连接,只能新建连接,脱离原事务管理。

失效代码

@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void createOrderWithLog(Order order) {
        // 父线程:事务内操作(正常)
        orderMapper.insert(order);
        // 新线程:ThreadLocal为空,用新连接,脱离事务
        new Thread(() -> {
            logMapper.insertLog("创建订单:" + order.getNo()); // 不回滚
        }).start();
        throw new RuntimeException("异常"); // 父线程回滚,但日志已插入
    }
}

执行结果

父线程“订单插入”回滚,新线程“日志插入”成功,数据不一致。

解决方案(按优先级)

  1. 事务后异步(推荐):用TransactionSynchronizationManager在事务提交后执行异步操作:

    @Transactional(rollbackFor = Exception.class)
    public void createOrderWithLog(Order order) {
        orderMapper.insert(order);
        String orderNo = order.getNo();
        // 事务提交后执行日志
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                new Thread(() -> logMapper.insertLog("创建订单:" + orderNo)).start();
            }
        });
    }
  2. 消息队列解耦:事务方法提交后发送消息,消费者异步处理日志(彻底避免跨线程问题);

  3. 手动传递上下文:不推荐,需手动绑定父线程连接到新线程ThreadLocal,易引发线程安全问题。


三、事务失效排查流程(四步定位法)

  1. 检查Bean有效性:类是否加@Service,是否在扫描范围内;
  2. 检查方法修饰符:是否为public,有无static/final
  3. 检查调用与异常:是否同类内部调用,异常是否捕获未抛,rollbackFor是否配置;
  4. 检查底层支持:数据库引擎是否为InnoDB,数据源是否托管,传播属性是否正确,是否跨线程。

文章的技术细节就到这里啦~

作为专注 Java 后端的学习者,我会持续输出:

Java 八股核心原理深度解析:例如 JVM、并发编程、集合框架等面试高频考点。
实用高频小知识点笔记:帮你快速掌握面试常问的基础细节。
原理拆解与知识点总结:助你构建扎实的知识体系,备战面试。
觉得有用的话,欢迎关注我! 后续内容将帮助你梳理关键知识,避免错过备战面试的重要笔记~

专注 Java 后端,持续输出干货!

---

技能提升

学习不止于此,推荐继续深入:


开发资源

posted @ 2025-11-20 14:41  clnchanpin  阅读(98)  评论(0)    收藏  举报