Spring事务失效的问题及解决方案

 更新时间:2025年11月05日 08:51:35   作者:张 先生  
本文详细分析了Spring事务失效的原因,并提供了一个解决方案,在代码中,事务传播属性设置为REQUIRES_NEW解决了事务失效的问题,确保了日志记录的操作不会影响主事务的回滚,同时,文中还总结了各种事务传播属性的含义和使用场景

Spring事务失效问题

开场

Spring事务管理,在Service实现类的方法上添加@Transactional注解就能进行事务控制,但是最近遇到一个事务失效的问题,浅浅分析一下

问题

在开发过程中发现这样一个bug,有一段逻辑代码:商品入库、并且生成一个入库单的操作;首先肯定的是需要操作两张表,一是 入库加库存,二是 新增一个入库单类似于入库记录。但是测试时候遇到一个问题,有一个商品入库成功了,库存也加进去了,但是没有生成入库单,我立马想到的是难道是事务没有控制好?翻看接口代码发现还真是。。。

Spring事务失效的原因

我们都知道@Transactional失效的原因有多种,列举一下,后面有时间再仔细分析研究

1.方法访问修饰符:

  • @Transactional注解只对public方法生效。
  • 如果事务性方法是privateprotected,事务不会生效

2.类内部方法调用:

  • 在同一个类中调用标注了**@Transactional**的方法(即自调用)时,事务管理器不会介入,因为Spring AOP代理无法拦截内部调用

3.没有启用事务管理:

  • 必须显式启用Spring的事务管理,通常使用**@EnableTransactionManagement**注解在配置类上,或者在XML中配置使用
<tx:annotation-driven/>

4.事务传播属性不当:

  • 事务传播属性(Propagetion)配置不当会导致事务失效。
  • 例如:传播行为REQUIRES_NEW会挂起当前事务,创建一个新事物,这可能不是我们想要的

5.异常处理不当:

  • 默认情况下,Spring事务管理只在运行时异常(RuntimeExpection及其子类)和错误(Error及其子类)时回滚。
  • 如果捕获并处理了一场,但没有重新抛出,事务不会回滚。此外需要注意,对于检查异常(Exception及其子类),需要制定rollbackFor属性来触发事务回滚,例如:
@Transactional(rollbackFor = Exception.class)

6.多线程环境:

  • Transactional 注解无法再多线程环境中传播事务。
  • Spring事务管理依赖于线程局部变量(ThreadLocal),在不同献策会给你之间共享事务需要显式处理。(后面遇到的话仔细研究一下)

7.数据源和事务管理器配置不一致:

  • 确保数据源(DataSource)和事务管理器(TransactionManager)配置一致。
  • 如果有多个数据源和事务管理器,需要明确指定使用的事务管理器。

8.Spring代理模式限制:

  • Spring默认使用AOP代理来管理事务。
  • 默认情况下使用JDK动态代理,只有实现接口的类可以使用事务。
  • 如果没有实现接口,需要使用CGLIB代理,通过proxyTargetClass=true启用CGLIB代理。

9.异步方法:

  • 一步方法(如使用@Async注解的方法)运行在独立线程中,不会参与当前线程的事务管理。
  • 需要特别处理异步方法的事务管理。

排查bug

言归正传,我在这段逻辑代码中发现有两个问题,首先代码块被 try catch 捕获后没有重新抛出异常,而且这段代码在catch 里面有日志记录,插入到一张日志表;业务逻辑太多此处就不贴真实代码了,但是我写了一个demo是可以复刻问题的。

代码如下:

@Transactional(rollbackFor = Exception.class)
public User update(User userDto) {
    try {
        //查询用户
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
        User user = userDao.selectOne(queryWrapper);
        //修改数据:扣钱
        user.setMoney(user.getMoney().subtract(userDto.getMoney()));
        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
        userDao.update(user);
        //模拟异常
        int i = 1/0;
    } catch (Exception e) {
        log.error(e.getMessage(),e);
        //日志记录:将错误信息插入到日志表
        insertLog(userDto, e);
    }
    return null;
}

    /*
    * 日志记录的方法
    */
    public void recordLog(Exception e) {
        LogHistory logHistory = new LogHistory();
        logHistory.setCode("1");
        logHistory.setTime(new Date());
        logHistory.setErrorMsg(e.getMessage());
        logHistoryMapper.insert(logHistory);
    }

上面这段代码,大家还有发现问题其他吗,细心的小伙伴一定还能发现其他问题,那就是这个 catch 捕获异常后并没有抛出,所以这段代码并不会回滚,但是问题来了,如果我们在 catch 中最后抛出异常,那么日志记录的操作也将回滚,这里就会有点冲突,如果代码有异常报错,我们的目的就是要回滚,但是还不能把日志记录给回滚。

解决方案

发现了问题,该如何去解决呢?

在这里我用到了一个事务的传播行为:先说结论,将事务的传播属性设置为REQUIRES_NEW,就是在当前事务中新建了一个事务,用新的事务去控制我日志记录的操作,这样的话两个事务互不影响,A事务如果抛出异常,不影响B事务,B事务正常插入数据,完美解决!当然不止这一种解决方案,还有异步调用,mq发消息等方式。

但是需要注意,我们需要在单独的Service实现类中去编写日志记录的方法,不能在A事务中去编写了,代码如下:

    @Transactional(rollbackFor = Exception.class)
    public User update(User userDto) {
        try {
            //查询用户
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
            User user = userDao.selectOne(queryWrapper);
            //修改数据:扣钱
            user.setMoney(user.getMoney().subtract(userDto.getMoney()));
            LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.eq(StrUtil.isNotBlank(userDto.getId()), User::getId, userDto.getId());
            userDao.update(user);
            //模拟异常
            int i = 1/0;
        } catch (Exception e) {
            log.error(e.getMessage(),e);
            //日志记录:需要在另外的service中编写
            logHistoryService.insertLog(userDto, e);
            //抛出异常
            throw e;
        }
        return null;
    }
    /*
    * 日志记录
    * 此处我们需要设置事务的传播属性为 REQUIRES_NEW
    */
	@Transactional(propagation = Propagation.REQUIRES_NEW)
    public LogHistory insertLog(User userDto, Exception e) {
        LogHistory logHistory = new LogHistory();
        logHistory.setCode("111");
        logHistory.setTime(new Date());
        logHistory.setErrorMsg(e.getMessage());
        logHistoryMapper.insert(logHistory);
        return logHistory;
    }

事务的传播属性

在 Spring 中,事务的传播属性(Propagation)决定了事务的行为在方法间调用时的传播方式。传播属性通过 @Transactional 注解的 propagation 属性来设置,Spring 提供了以下几种传播属性:

1.REQUIRED(默认值):

  • 如果当前已经存在一个事务,则加入该事务。
  • 如果当前没有事务,则创建一个新的事务。
  • 这是最常见的传播行为。

2.REQUIRES_NEW

  • 无论当前是否存在事务,总是创建一个新的事务。
  • 如果当前存在事务,则挂起当前事务,直到新事务完成。

3.SUPPORTS

  • 如果当前存在事务,则加入该事务。
  • 如果当前没有事务,则以非事务方式执行。

4.NOT_SUPPORTED

  • 以非事务方式执行操作。
  • 如果当前存在事务,则挂起当前事务,直到当前操作完成。

5.MANDATORY

  • 必须在一个现有事务中运行。
  • 如果当前没有事务,则抛出异常。

6.NEVER

  • 必须在非事务上下文中运行。
  • 如果当前存在事务,则抛出异常。

7.NESTED

  • 如果当前存在事务,则在嵌套事务中运行。
  • 如果当前没有事务,则创建一个新的事务。
  • 嵌套事务使用保存点(savepoint),如果嵌套事务回滚,它只回滚到保存点,外部事务可以继续。
  • 如果当前没有事务,则抛出异常。

8.NEVER

  • 必须在非事务上下文中运行。
  • 如果当前存在事务,则抛出异常。

9.NESTED

  • 如果当前存在事务,则在嵌套事务中运行。
  • 如果当前没有事务,则创建一个新的事务。
  • 嵌套事务使用保存点(savepoint),如果嵌套事务回滚,它只回滚到保存点,外部事务可以继续。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java 读取指定路径的文本文件并返回String的方法

    Java 读取指定路径的文本文件并返回String的方法

    今天小编就为大家分享一篇Java 读取指定路径的文本文件并返回String的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-07-07
  • Hadoop组件简介

    Hadoop组件简介

    Hadoop作为一种分布式基础架构,可以使用户在不了解分布式底层细节的情况下,开发分布式程序。接下来通过本文给大家分享Hadoop组件简介,感兴趣的朋友一起看看吧
    2017-09-09
  • Idea如何使用Fast Request接口调试

    Idea如何使用Fast Request接口调试

    这篇文章主要介绍了Idea如何使用Fast Request接口调试问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • java中ImageReader和BufferedImage获取图片尺寸实例

    java中ImageReader和BufferedImage获取图片尺寸实例

    这篇文章主要介绍了java中ImageReader和BufferedImage获取图片尺寸实例,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • Java导出Excel动态表头的示例详解

    Java导出Excel动态表头的示例详解

    这篇文章主要为大家详细介绍了Java导出Excel动态表头的相关知识,文中的示例代码简洁易懂,具有一定的借鉴价值,有需要的小伙伴可以了解下
    2025-02-02
  • 面试官:详细谈谈Java对象的4种引用方式

    面试官:详细谈谈Java对象的4种引用方式

    这篇文章主要给大家介绍了java面试官常会问到的,关于Java对象的4种引用方式的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • 详解mysql插入数据后返回自增ID的七种方法

    详解mysql插入数据后返回自增ID的七种方法

    这篇文章主要介绍了详解mysql插入数据后返回自增ID的七种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • Intellij IDEA远程debug教程实战和要点总结(推荐)

    Intellij IDEA远程debug教程实战和要点总结(推荐)

    这篇文章主要介绍了Intellij IDEA远程debug教程实战和要点总结(推荐),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • 通过idea创建Spring Boot项目并配置启动过程图解

    通过idea创建Spring Boot项目并配置启动过程图解

    这篇文章主要介绍了通过idea创建Spring Boot项目并配置启动过程图解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • 巧用Spring中的@Order进行排序

    巧用Spring中的@Order进行排序

    这篇文章主要介绍了巧用Spring中的@Order进行排序,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08

最新评论