开发者

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

开发者 https://www.devze.com 2025-11-05 10:30 出处:网络 作者: 张 先生
目录Spring事务失效问题开场问题Spring事务失效的原因排查bug解决方案事务的传播属性总结Spring事务失效问题
目录
  • Spring事务失效问题
    • 开场
    • 问题
    • Spring事务失效的原因
    • 排查bug
    • 解决方案
    • 事务的传播属性
  • 总结

    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::get编程客栈Id, 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(jsnew Date());
            logHistory.setErrorMsg(e.getMessage());
            logHistoryMapper.insert(logHistory);
        }
    

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

    解决方案

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

    在这里我用到了一个事务的传播行为:先说结论,将事务的传播属性设置为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 logHistpythonory;
        }
    

    事务的传播属性

    在 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),如果嵌套事务回滚,它只回滚到保存点,外部事务可以继续。

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号