【问题处理】解决Spring事务@Transactional多层嵌套失效

news2024/11/21 1:47:28

场景:

        在 AService 中,我会直接调用 A 的数据操作层去操作 A的数据 以及 A关联密切的其它数据,在操作完之后,会去调用 BService 和 CService 中更新对应的数据,并在每个方法上使用了事务,但在调用 BService 或者 CService 时候出现了异常,此时出现异常的BService 或者 CService 中数据没有改变,回滚了。但在 AService 中调用的 update 方法和出现异常前已经执行完的方法执行成功并且没有回滚。

伪代码如下:

1、AService实现类

@Service
@Slf4j
public class AServiceImpl implements IAService {
    private final IBService bService;
    private final ICService cService;
    public AServiceImpl(IBService bService, ICService cService) {
        this.bService = bService;
        this.cService = cService;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String modifyA(TestAParam param) throws BaseException {
        // 兜底:入参空字段校验
        this.judgeNull(param);

        TestModifyParam modifyParam = param.getModifyParam();

        String aCode = update(param, Boolean.TRUE);

        if (null != modifyParam.getStatus() && TestStatusConstant.EDITED.equals(modifyParam.getStatus())){
            // 保存B信息
            bService.saveInfo(param, aCode);
            // 保存C信息
            cService.saveInfo(param, aCode);
        }

        // 更新A数据关联其它数据
        if (StringUtils.isNotBlank(aCode)){
            setOtherData(param);
        }

        return aCode;
    }

    @Transactional(rollbackFor = Exception.class)
    public void setOtherData(TestAParam param) throws BaseException {
        // 其它关联数据处理
    }

    @Transactional(rollbackFor = Exception.class)
    public String update(TestAParam param, Boolean directlyFlag) throws BaseException {
        // 更新处理
       return null;
    }

    @Transactional(rollbackFor = Exception.class)
    public void judgeNull(TestAParam param) throws BaseException {
        // 参数空校验与提醒处理
    }

}

2、BService 和 CService 实现类(CService实现类异常处理差不多相同)

@Service
@Slf4j
public class BServiceImpl implements IBService {
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveInfo(TestAParam param, String aCode) throws BaseException {
        // 数据校验

        // 模拟代码,出现不匹配数据错误情况会抛出异常
        if (Objects.isNull(param)){
            throw new BaseException(CodeEnum.FAILED.getCode(),"BServiceImpl saveInfo param mistake");
        }

        // 其它操作

    }
}

一、Spring事务实现方式及原理

Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring 是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过 binlog 或者 redo log 实现的。

一般我们在程序里面使用的都是在方法上面加  @Transaction 注解,这种属于声明式事物

声明式事务本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

二、事务失效原因

2.1 数据库本身不支持事物

这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。

2.2 方法不是Public

注解 @Transactional 只能放在 public 修饰的方法上才起作用private 方法是不会被spring代理的)因此是不会有事物产生的,这种做法是无效的。

2.3 未被 Spring 管理的Bean

没有被spring管理的bean, spring连代理对象都无法生成,事务自然是无效的。

2.4 当前类的调用

@Service
public class UserServiceImpl implements UserService {

    public void update(User user) {
        updateUser(user);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateUser(User user) {
        // update user
    }

}

上面的这种情况下是不会有事物管理操作的。

通过看声明式事物的原理可知,spring使用的是AOP切面的方式本质上使用的是动态代理来达到事物管理的目的,当前类调用的方法上面加 @Transactional 这个是没有任何作用的,因为调用这个方法的是this。

再看下面的一种例子:

@Service
public class UserServiceImpl implements UserService {

    @Transactional(rollbackFor = Exception.class)
    public void update(User user) {
        updateUser(user);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUser(User user) {
        // update user
    }

}

这次在 update 方法上加了 @Transactional,updateUser 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

答案是:不管用!

因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。

还有就是在没有指定事务传播行为时,从源码中可以看到默认是使用 Propagation.REQUIRED。

2.5 配置的事物传播性有问题

@Service
public class UserServiceImpl implements UserService {

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void update(User user) {
        // update user
    }    
}

2.6 异常被你 "抓住"了

@Service
public class UserServiceImpl implements UserService {

    @Transactional(rollbackFor = Exception.class)
    public void update(User user) {

      try{
        // update user
      }catch(Execption e){
         log.error("异常",e)
      }
    }    
}

异常被抓了,这样子代理类就没办法知道你到底有没有错误,需不需要回滚,所以这种情况也是没办法回滚。

2.7 rollbackFor 异常指定错误

@Service
public class UserServiceImpl implements UserService {

    @Transactional
    public void update(User user) {
        // update user
    }    
}

上面这种没有指定回滚异常,这个时候默认的回滚异常是 RuntimeException ,如果出现其他异常那么就不会回滚事物。

三、Spring的事务传播行为

Spring 事务的传播行为说的是,当多个事务同时存在的时候, Spring 如何处理这些事务的行为。

类型说明
PROPAGATION_REQUIRED如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
PROPAGATION_SUPPORTS支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_MANDATORY支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
PROPAGATION_REQUIRES_NEW创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按 REQUIRED 属性执行。

当传播行为设置了PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER,PROPAGATION_SUPPORTS这三种时,就有可能存在事物不生效。

四、解决思路:

        1、声明式事务处理,采用合适的事务传播行为,将AService中修改A中数据方法update和更新A数据关联其它数据方法setOtherData通过AppContext.getBean()的方式直接获取AService中的方法,不违背事务失效原因中的2.4项(当前类的调用),并将保持B和C信息的方法单独抽取出来,提供一个saveBandCInfo方法,加上事务处理和传播行为,并抛出异常信息。

        2、使用编程式事务管理,手动配置事务边界,确保modifyA中所有方法在事务中执行。

五、采取方法:

        由于AService中的方法 modifyA 调用链路比较长,如果使用声明式事务处理,改动起来是比较大的,中间链路可能会存在事务传播行为失效的情况,此时使用编程式事务管理解决就会很明显轻松解决。

伪代码如下:

@Service
@Slf4j
public class AServiceImpl implements IAService {
    private final IBService bService;
    private final ICService cService;
    private final PlatformTransactionManager transactionManager;
    public AServiceImpl(IBService bService, ICService cService, PlatformTransactionManager transactionManager) {
        this.bService = bService;
        this.cService = cService;
        this.transactionManager = transactionManager;
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String modifyA(TestAParam param) throws BaseException {
        // 兜底:入参空字段校验
        this.judgeNull(param);
        // 编程式事务
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            TestModifyParam modifyParam = param.getModifyParam();
            String aCode = update(param, Boolean.TRUE);
            if (null != modifyParam.getStatus() && TestStatusConstant.EDITED.equals(modifyParam.getStatus())){
                // 保存B信息
                bService.saveInfo(param, aCode);
                // 保存C信息
                cService.saveInfo(param, aCode);
            }
            // 更新A数据关联其它数据
            if (StringUtils.isNotBlank(aCode)){
                setOtherData(param);
            }
            // 提交事务
            transactionManager.commit(status);
            return aCode;
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
            // 处理异常或根据需要重新抛出异常
            throw new BaseException(CodeEnum.FAILED.getCode(),"modifyA error message is {}", e.getMessage());
        }
            
    }

    @Transactional(rollbackFor = Exception.class)
    public void setOtherData(TestAParam productStrategyDO) throws BaseException {
        // 其它关联数据处理
    }

    @Transactional(rollbackFor = Exception.class)
    public String update(TestAParam param, Boolean directlyFlag) throws BaseException {
        // 更新处理
        return null;
    }

    @Transactional(rollbackFor = Exception.class)
    public void judgeNull(TestAParam param) throws BaseException {
        // 参数空校验与提醒处理
    }

  上述代码使用了 PlatformTransactionManager 接口的实现来手动管理事务。在代码中,我们首先获取 transactionManager 的实例,然后使用该实例手动创建事务定义和事务状态。在try块中执行update方法和其它方法,并在最后根据执行情况手动提交或回滚事务。

通过这种方式,可以确保update方法的操作在事务中进行,且在其它方法中发生异常时能够回滚。如果采取这个方式请确保在相应的配置类中将transactionManager正确配置为适用于你的应用程序的事务管理器实现。

👍如果对你有帮助,给博主一个免费的点赞以示鼓励
欢迎各位🔎点赞👍评论收藏⭐️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/927277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue2 computed计算属性,watch侦听器

一、今日学习目标 1.指令补充 指令修饰符v-bind对样式增强的操作v-model应用于其他表单元素 2.computed计算属性 基础语法计算属性vs方法计算属性的完整写法成绩案例 3.watch侦听器 基础写法完整写法 二、指令修饰符 1.什么是指令修饰符? 所谓指令修饰符就是…

Orchestrator介绍二 自身高可用性方案

目录 获得 HA 的方法 一 没有高可用性 (No high availability) 使用场景 架构组成 架构图 二 半高可用性(Semi HA) 三 基于共享数据库后端高可用(HA via shared backend) 四 基于Raft协议高可用 五…

RocketMQ消息存储

一、存储介质 ● 关系型数据库DB Apache下开源的另外一款MQ—ActiveMQ (默认采用的KahaDB做消息存储)可选用JDBC的方式来做消息持久化,通过简单的xmI配置信息即可实现JDBC消息存储。由于,普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下&a…

C语言gets( )函数详解

1.描述 char* gets( char* str)函数:从标准输入(stdin)读取字符串,遇到空格不结束,直到遇到回车,将字符串存储到str指向的字符串。 2.gets( )和scanf( )的区别 gets(str)和scanf("%s",str)作用…

windows中安装sqlite

1. 下载文件 官网下载地址:https://www.sqlite.org/download.html 下载sqlite-dll-win64-x64-3430000.zip和sqlite-tools-win32-x86-3430000.zip文件(32位系统下载sqlite-dll-win32-x86-3430000.zip)。 2. 安装过程 解压文件 解压上一步…

9.oracle中sign函数

在Oracle/PLSQL中, sign 函数返回一个数字的正负标志. 语法如下&#xff1a;sign( number ) number 要测试标志的数字. If number < 0, then sign returns -1. If number 0, then sign returns 0. If number > 0, then sign returns 1. 应用于: Oracle 8i, Oracle …

基于CMSIS的外设/设备驱动框架

先附上一张CMSIS的结构图 对于基于CMSIS的设备驱动框架开发涉及的文件有CMSIS目录下的&#xff0c;对外设驱动做了统一的驱动模型封装 /** \brief Access structure of the SPI Driver. */ typedef struct _ARM_DRIVER_SPI {ARM_DRIVER_VERSION (*GetVersion) (void)…

前端需要理解的浏览器知识

1 浏览器架构 浏览器是多进程多线程的应用程序&#xff0c;多进程可以避免相互影响和减少连环崩溃的几率&#xff1a; 浏览器&#xff08;主&#xff09;进程&#xff1a;主要负责界⾯显示、⽤户交互、⼦进程管理、存储等功能。内部会启动多个线程分别处理不同的任务。⽹络进…

【JMeter】常用线程组设置策略

目录 一、前言 二、单场景基准测试 1.介绍 2.线程组设计 3.测试结果 三、单场景并发测试 1.介绍 2.线程组设计 3.测试结果 四、单场景容量/爬坡测试 1.介绍 2.线程组设计 3.测试结果 五、混合场景容量/并发测试 1.介绍 六、稳定性测试 1.介绍 2.线程组设计 …

Servlet简介

一、servlet介绍 1、概念 servlet是一个运行在服务器端的小程序&#xff0c;也是一个接口&#xff0c;介绍了Java类被tomcat识别的规则。 2、servlet的创建和使用 &#xff08;1&#xff09;创建一个JavaEE项目 &#xff08;2&#xff09;定义一个类&#xff0c;实现servlet…

neo4jd3拓扑节点显示为节点标签(自定义节点显示)

需求描述&#xff1a;如下图所示&#xff0c;我的拓扑图中有需要不同类型的标签节点&#xff0c;我希望每个节点中显示的是节点的标签 在官方示例中&#xff0c;我们可以看到&#xff0c;节点里面是可以显示图标的&#xff0c;现在我们想将下面的图标换成我们自定义的内容 那…

【android12-linux-5.1】【ST芯片】HAL移植后配置文件生成报错

根据ST官方源码移植HAL源码后&#xff0c;执行readme指示中的生成配置文件指令时报错ST_HAL_ANDROID_VERSION未定义之类&#xff0c;应该是编译环境参数问题。makefile文件中是自动识别配置的&#xff0c;参数不祥就会报错&#xff0c;这里最快的解决方案是查询确定自己android…

【golang】panic函数、recover函数以及defer语句

从panic被引发到程序终止运行的大致过程是什么&#xff1f; 大致过程&#xff1a; 某个函数中的某行代码有意无意地引发了一个panic。这时&#xff0c;初始的panic详情会被建立起来&#xff0c;并且该程序的控制权会立即从从行代码转移至调用其所属函数的那行代码上&#xff…

ICT产教融合创新实训基地物联网实训室建设方案

一、概述 1.1物联网定义 物联网工程&#xff08;Internet of Things Engineering&#xff09;是一种以信息技术&#xff08;IT&#xff09;来改善实体世界中人们生活方式的新兴学科&#xff0c;它利用互联网技术为我们的日常生活活动提供服务和增益&#xff0c;从而让各种智能…

1996-2022全球sar卫星数据

数据简介 1992年JAXA&#xff08;Japan Aerospace Exploration Agency&#xff0c;日本宇宙航空研究开发机构&#xff09;发射了一颗JERS-1卫星&#xff0c;该卫星携带有18*24m分辨率的SAR传感器。随后&#xff0c;JAXA又在2006年和2014年分别发射了带有SAR传感器的alos卫星和…

HelpLook 免费版与商业版的比较,帮助您快速选择!

HelpLook 是一款零代码、开箱即用的帮助中心及博客网站搭建工具&#xff0c;只需简单几步&#xff0c;即可帮助企业、机构、个人发布在线品牌内容站点。 HelpLook 提供多个版本方案供不同需求的用户选择&#xff0c;今天想着重跟大家分享免费版和商业版&#xff0c;将从三个方面…

DataFrame.plot函数详解(五)

DataFrame.plot函数详解&#xff08;五&#xff09; 散点图和箱体图实例 1. scatter DataFrame.plot.scatter(x, y, sNone, cNone, **kwargs) c&#xff1a; 是每个点的颜色&#xff0c;可以是一个值&#xff0c;也可以是数组值 s&#xff1a; 是每个点的大小&#xff0c;可以…

MCU和MPU你分得清楚吗?

最近有不少同学表示在学习嵌入式的过程中分不清MCU和MPU&#xff0c;这两个确实是长得很像、容易混淆的概念&#xff0c;这里我为大家仔细分辨一下。 从概念上讲&#xff0c;MCU指的是微控制器&#xff0c;优势在于“控制”&#xff0c;MPU指的是微处理器&#xff0c;优势在于“…

Redis笔记——(狂神说)待续

Nosql概述 为什么要用NoSql&#xff1f; 1、单机mysql的年代&#xff1a;90年代&#xff0c;网站访问量小&#xff0c;很多使用静态网页html写的&#xff0c;服务器没压力。 当时瓶颈是&#xff1a;1)数据量太大一个机器放不下。2)数据的索引(BTree)&#xff0c;一个机器内存也…

激活函数总结

leakyRelu()激活函数 ReLU函数在输入大于0时返回输入值&#xff0c;否则返回0。 Leaky ReLU在输入小于0时会返回一个较小的负数&#xff0c;以保持一定的导数&#xff0c;使得信息可以继续向后传播。 Leaky ReLU函数&#xff1a; f(x) max(ax, x)其中&#xff0c;a是一个小…