为了账号安全,请及时绑定邮箱和手机立即绑定

Spring Boot 事务失效?盘点 8 大高频场景 + 避坑方案

在 Spring Boot 开发中,@Transactional 注解几乎是处理数据库事务的标配,但实际开发中经常遇到“注解加了,事务却没生效”的情况。本文梳理了 8 种高频的事务失效场景,结合具体代码示例分析原因,并给出可落地的避坑方案,帮你彻底搞定事务失效问题。

一、先搞懂:Spring 事务的核心原理

在分析失效场景前,先明确 Spring 声明式事务的核心逻辑——基于 AOP 动态代理实现

  1. Spring 会为标注 @Transactional 的类生成代理对象;
  2. 只有通过代理对象调用事务方法时,才会触发事务拦截器,创建/提交/回滚事务;
  3. 若绕过代理直接调用目标方法,事务注解会完全失效。

这是理解所有事务失效场景的基础,记住:事务生效的前提是“走代理”

二、8 大事务失效场景全解析

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

失效原因

Spring AOP 基于动态代理实现,而代理的本质是生成子类重写目标方法:

  • private 方法:子类无法重写,代理无法拦截;
  • final 方法:子类不能重写,代理逻辑无法植入;
  • static 方法:属于类而非实例,代理对象无法调用。

示例代码(失效)

@Service
public class OrderService {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //  private 修饰,事务失效
    @Transactional(rollbackFor = Exception.class)
    private void createOrder(Long productId, Long userId) {
        jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
        // 抛异常也不会回滚
        throw new RuntimeException("下单失败");
    }

    //  final 修饰,事务失效
    @Transactional(rollbackFor = Exception.class)
    public final void deductStock(Long productId) {
        jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
    }

    //  static 修饰,事务失效
    @Transactional(rollbackFor = Exception.class)
    public static void saveLog(String content) {
        jdbcTemplate.update("INSERT INTO t_log (content) VALUES (?)", content);
    }
}

避坑方案

  • 事务方法必须是 public 非 final 非 static 修饰;
  • 若需封装私有逻辑,可将事务逻辑抽离到 public 方法,私有方法仅做纯业务处理:
@Service
public class OrderService {
    //  public 方法,事务生效
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Long userId) {
        // 调用私有方法(仅做业务逻辑)
        doCreateOrder(productId, userId);
    }

    // 私有方法仅处理业务,不标注事务
    private void doCreateOrder(Long productId, Long userId) {
        jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
    }
}

场景 2:本类内部调用事务方法

失效原因

本类内部调用时,直接调用目标对象的方法,而非代理对象,事务拦截器未触发,注解失效。

示例代码(失效)

@Service
public class OrderService {
    @Resource
    private JdbcTemplate jdbcTemplate;

    // 非事务方法
    public void outerMethod(Long productId, Long userId) {
        //  内部调用事务方法,绕过代理,事务失效
        innerTransactionalMethod(productId, userId);
    }

    // 事务方法(被内部调用)
    @Transactional(rollbackFor = Exception.class)
    public void innerTransactionalMethod(Long productId, Long userId) {
        jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
        throw new RuntimeException("下单失败"); // 不会回滚
    }
}

避坑方案

核心思路:让调用走代理对象,有 3 种实现方式:

方案 1:注入自身代理对象(推荐)
@Service
public class OrderService {
    // 注入自身代理对象(需开启 expose-proxy = true)
    @Resource
    private OrderService selfProxy;

    public void outerMethod(Long productId, Long userId) {
        //  通过代理对象调用,事务生效
        selfProxy.innerTransactionalMethod(productId, userId);
    }

    @Transactional(rollbackFor = Exception.class)
    public void innerTransactionalMethod(Long productId, Long userId) {
        // 事务逻辑...
    }
}

注意:需在启动类开启代理暴露:

@EnableTransactionManagement(exposeProxy = true) // 关键配置
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
方案 2:拆分 Bean(最彻底)

将事务方法拆分到独立 Bean,通过跨 Bean 调用触发代理:

// 独立的事务 Bean
@Service
public class OrderTransactionService {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Long userId) {
        // 事务逻辑...
    }
}

// 原 Bean 调用独立 Bean
@Service
public class OrderService {
    @Resource
    private OrderTransactionService transactionService;

    public void outerMethod(Long productId, Long userId) {
        //  跨 Bean 调用,事务生效
        transactionService.createOrder(productId, userId);
    }
}

场景 3:异常被“吞掉”未抛出

失效原因

Spring 事务回滚的前提是:事务方法抛出未被捕获的异常。若异常被内部捕获且未重新抛出,事务管理器无法感知异常,会默认提交事务。

示例代码(失效)

@Service
public class OrderService {
    @Resource
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Long userId) {
        try {
            jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
            jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
            throw new RuntimeException("下单失败");
        } catch (Exception e) {
            //  捕获异常但不抛出,事务不会回滚
            log.error("下单异常", e);
        }
    }
}

避坑方案

  • 异常必须抛到事务方法外层,让事务管理器感知;
  • 若需捕获异常做日志/补偿,捕获后重新抛出:
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Long userId) {
    try {
        // 业务逻辑
    } catch (Exception e) {
        log.error("下单异常", e);
        //  重新抛出异常,触发事务回滚
        throw new RuntimeException("下单失败", e);
    }
}

场景 4:未指定 rollbackFor,检查型异常导致不回滚

失效原因

@Transactional 默认仅回滚 RuntimeExceptionError,若抛出检查型异常(如 IOExceptionSQLException),事务不会回滚。

示例代码(失效)

@Service
public class OrderService {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //  未指定 rollbackFor,检查型异常不回滚
    @Transactional
    public void createOrder(Long productId, Long userId) throws Exception {
        jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
        // 抛出检查型异常,事务不回滚
        throw new Exception("下单失败");
    }
}

避坑方案

强制指定 rollbackFor = Exception.class,覆盖所有异常类型:

//  指定 rollbackFor,所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Long userId) throws Exception {
    // 业务逻辑...
    throw new Exception("下单失败"); // 事务会回滚
}

场景 5:传播行为配置错误

失效原因

若事务方法的传播行为配置为 SUPPORTS/NOT_SUPPORTED/NEVER,且调用方无事务上下文,事务会以非事务方式执行,注解失效。

示例代码(失效)

@Service
public class OrderService {
    @Resource
    private JdbcTemplate jdbcTemplate;

    //  传播行为为 SUPPORTS,调用方无事务则非事务执行
    @Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
    public void createOrder(Long productId, Long userId) {
        jdbcTemplate.update("UPDATE t_stock SET count = count - 1 WHERE product_id = ?", productId);
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
        throw new RuntimeException("下单失败"); // 不会回滚
    }
}

// 调用方无事务
@Controller
public class OrderController {
    @Resource
    private OrderService orderService;

    @PostMapping("/order")
    public ResponseEntity<String> createOrder(@RequestParam Long productId, @RequestParam Long userId) {
        orderService.createOrder(productId, userId); // 无事务上下文,SUPPORTS 以非事务执行
        return ResponseEntity.ok("下单成功");
    }
}

避坑方案

  • 核心业务方法默认用 Propagation.REQUIRED(默认值,可省略);
  • 仅查询场景用 SUPPORTS,且需确保核心写操作不依赖该传播行为:
//  核心写操作用 REQUIRED(默认)
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long productId, Long userId) {
    // 业务逻辑...
}

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

失效原因

Spring Boot 自动配置的事务管理器仅对默认数据源生效,若自定义多数据源但未手动配置事务管理器,事务注解会失效。

示例代码(失效)

// 自定义多数据源,但未配置事务管理器
@Configuration
public class DataSourceConfig {
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

// 事务注解失效,因为无对应事务管理器
@Service
public class OrderService {
    @Resource
    @Qualifier("primaryDataSource")
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Long userId) {
        // 业务逻辑...
    }
}

避坑方案

为每个自定义数据源配置事务管理器:

@Configuration
public class DataSourceConfig {
    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    //  配置主数据源事务管理器
    @Bean(name = "primaryTransactionManager")
    @Primary
    public DataSourceTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // 从数据源同理...
}

场景 7:数据库不支持事务

失效原因

部分数据库/存储引擎不支持事务(如 MySQL 的 MyISAM 引擎、SQLite 非 WAL 模式),即使加了 @Transactional,也无法保证事务原子性。

示例代码(失效)

-- MySQL 表使用 MyISAM 引擎,不支持事务
CREATE TABLE t_order (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    product_id BIGINT
) ENGINE=MyISAM;
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Long userId) {
        // 插入订单后抛异常,但 MyISAM 不回滚
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
        throw new RuntimeException("下单失败");
    }
}

避坑方案

  • 确保数据库/表使用支持事务的引擎(如 MySQL 用 InnoDB);
  • 检查表引擎:
-- 查看表引擎
SHOW TABLE STATUS LIKE 't_order';
-- 修改为 InnoDB
ALTER TABLE t_order ENGINE=InnoDB;

场景 8:事务超时时间过短,导致提前回滚(隐性失效)

失效原因

事务方法执行时间超过 timeout 配置的时间,事务管理器会强制回滚,看似“失效”,实则是超时触发回滚。

示例代码(失效)

@Service
public class OrderService {
    //  超时时间 1 秒,业务执行超时报错
    @Transactional(rollbackFor = Exception.class, timeout = 1)
    public void createOrder(Long productId, Long userId) throws InterruptedException {
        // 模拟耗时操作(2 秒)
        Thread.sleep(2000);
        jdbcTemplate.update("INSERT INTO t_order (user_id, product_id) VALUES (?, ?)", userId, productId);
    }
}

避坑方案

  • 合理设置超时时间,避免过短;
  • 将耗时操作(如远程调用、IO)移出事务:
@Transactional(rollbackFor = Exception.class, timeout = 30) // 合理超时时间
public void createOrder(Long productId, Long userId) {
    // 非事务耗时操作:先查远程价格
    BigDecimal price = productRemoteService.getPrice(productId);
    // 事务内仅执行数据库操作(短事务)
    jdbcTemplate.update("INSERT INTO t_order (user_id, product_id, price) VALUES (?, ?, ?)", userId, productId, price);
}

三、事务失效排查清单(快速定位问题)

  1. 事务方法是否为 public 非 final 非 static
  2. 是否存在本类内部调用事务方法?
  3. 异常是否被捕获未抛出?
  4. 是否指定 rollbackFor = Exception.class
  5. 传播行为是否为 REQUIRED(核心写操作)?
  6. 自定义数据源是否配置了事务管理器?
  7. 数据库/表是否支持事务(如 InnoDB)?
  8. 事务超时时间是否合理?

四、总结

Spring Boot 事务失效的核心原因可归纳为三类:

  1. 代理未触发:方法修饰符错误、本类内部调用;
  2. 异常未感知:异常被吞、未指定 rollbackFor;
  3. 配置/环境问题:传播行为错误、事务管理器缺失、数据库不支持事务。

只要抓住“走代理、抛异常、配对参数”三个核心,99% 的事务失效问题都能解决。日常开发中,建议遵循“最简配置原则”:

  • 仅给外层入口方法加 @Transactional(rollbackFor = Exception.class)
  • 被调用方法不加事务注解,专注业务逻辑;
  • 避免滥用传播行为,核心场景用默认的 REQUIRED
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
27
获赞与收藏
81

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消