一文搞懂spring boot本地事务@Transactional参数

 更新时间:2021年10月09日 10:51:24   作者:cyb-xh  
这篇文章主要介绍了spring boot本地事务@Transactional参数详解,本文通过示例代码图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

1. 本地事务

商品新增功能非常复杂,商品管理微服务在service层中调用保存spu和sku相关的方法,为了保证数据的一致性,必然会使用事务。

在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。

咱们之前玩的事务都是本地事务。所谓本地事务,是指该事务仅在当前项目内有效。

1.1. 基本概念

事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。

事务的四个特性(ACID):

  1. 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
  2. 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
  3. 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰
  4. 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

1.2. 隔离级别

事务并发引起一些读的问题:

  • 脏读:一个事务可以读取另一个事务未提交的数据
  • 不可重复读: 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配
  • 虚读(幻读: 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点

并发写:使用mysql默认的锁机制(独占锁)

解决读问题:设置事务隔离级别

  1. read uncommitted(0)
  2. read committed(2)
  3. repeatable read(4)
  4. Serializable(8)

隔离级别越高,性能越低。

一般情况下:脏读是不可允许的,不可重复读和幻读是可以被适当允许的。

1.3. 相关命令

查看全局事务隔离级别:SELECT @@global.tx_isolation

设置全局事务隔离级别:set global transaction isolation level read committed;

查看当前会话事务隔离级别:SELECT @@tx_isolation

设置当前会话事务隔离级别:set session transaction isolation level read committed;

查看mysql默认自动提交状态:select @@autocommit

设置mysql默认自动提交状态:set autocommit = 0;【不自动提交】

开启一个事务:start transaction;

提交事务:commit

回滚事务: rollback

在事务中创建一个保存点:savepoint tx1

回滚到保存点:rollback to tx1

1.4. 传播行为

事务的传播行为不是jdbc规范中的定义。传播行为主要针对实际开发中的问题

1567660421239

七种传播行为:

REQUIRED 支持当前事务,如果不存在,就新建一个

SUPPORTS 支持当前事务,如果不存在,就不使用事务

MANDATORY 支持当前事务,如果不存在,抛出异常

REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务

NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务

NEVER 以非事务方式运行,如果有事务存在,抛出异常

NESTED 如果当前事务存在,则嵌套事务执行(嵌套式事务)

  1. 依赖于JDBC3.0提供的SavePoint技术
  2. 删除用户 删除订单。在删除订单后,设置savePoint,执行删除用户。删除订单和删除用户在同一事务中,删除用户失败,事务回滚savePoint,由用户控制视图提交还是回滚

这七种事务传播机制最常用的就两种:

REQUIRED:一个事务,要么成功,要么失败

REQUIRES_NEW:两个不同事务,彼此之间没有关系。一个事务失败了不影响另一个事务

1.4.1. 伪代码练习

传播行为伪代码模拟:有a,b,c,d,e等5个方法,a中调用b,c,d,e方法的传播行为在小括号中标出

a(required){
	b(required);
	c(requires_new);
	d(required);
	e(requires_new);
	// a方法的业务
}

问题:

  1. a方法的业务出现异常,会怎样?a,b,d回滚 c,e不回滚
  2. d方法出现异常,会怎样?a,b,d回滚;c不回滚;e未执行
  3. e方法出现异常,会怎样?a,b,d,e回滚 c不回滚,e方法出异常会上抛影响到上级方法
  4. b方法出现异常,会怎样?a,b回滚 c,d,e未执行

加点难度:

a(required){
	b(required){
		f(requires_new);
		g(required)
	}
	c(requires_new){
		h(requires_new)
		i(required)
	}
	d(required);
	e(requires_new);
	// a方法的业务
}

问题:

  1. a方法业务出异常?a,b,g,d回滚;f,c,h,i,e不回滚
  2. e方法出异常?e,a,b,g,d回滚;f,c,h,i不回滚
  3. d方法出异常?a,b,g,d回滚;f,c,h,i不回滚;e为执行
  4. h,i方法分别出异常?h,i,c,a,b,g回滚;f不回滚;d,e未执行
  5. i方法出异常?i,c,a,b,g回滚;f,h不回滚;d,e未执行
  6. f,g方法分别出异常?f,g,b,a回滚;c,h,i,d,e未执行

1.4.2. 改造商品新增代码

现在商品保存的方法结构如下:

    @Override
    public void bigSave(SpuVo spuVo) {
        /// 1.保存spu相关
        // 1.1. 保存spu基本信息 spu_info
        Long spuId = saveSpu(spuVo);

        // 1.2. 保存spu的描述信息 spu_info_desc
        saveSpuDesc(spuVo, spuId);

        // 1.3. 保存spu的规格参数信息
        saveBaseAttr(spuVo, spuId);

        /// 2. 保存sku相关信息
        saveSku(spuVo, spuId);
    }

    /**
     * 保存sku相关信息及营销信息
     * @param spuInfoVO
     */
    private void saveSku(SpuVo spuVo, Long spuId) { 。。。 }

    /**
     * 保存spu基本属性信息
     * @param spuInfoVO
     */
    private void saveBaseAttr(SpuVo spuVo, Long spuId) { 。。。 }

    /**
     * 保存spu描述信息(图片)
     * @param spuInfoVO
     */
    private void saveSpuDesc(SpuVo spuVo, Long spuId) { 。。。 }

    /**
     * 保存spu基本信息
     * @param spuInfoVO
     */
    private void saveSpu(SpuVo spuVo) {  。。。 }

为了测试事务传播行为,我们在SpuInfoService接口中把saveSkuInfoWithSaleInfo、saveBaseAttrs、saveSpuDesc、saveSpuInfo声明为service接口方法。

public interface SpuInfoService extends IService<SpuInfoEntity> {

    PageVo queryPage(QueryCondition params);

    PageVo querySpuInfo(QueryCondition condition, Long catId);

    void saveSpuInfoVO(SpuInfoVO spuInfoVO);

    void saveSku(SpuVo spuVo, Long spuId);

    void saveBaseAttr(SpuVo spuVo, Long spuId);

    void saveSpuDesc(SpuVo spuVo, Long spuId);

    Long saveSpu(SpuVo spuVo);
}

再把SpuInfoServiceImpl实现类的对应方法改成public:

1584780507279

1.4.3. 测试1:同一service + requires_new

springboot 1.x使用事务需要在引导类上添加**@EnableTransactionManagement注解开启事务支持**

springboot 2.x可直接使用**@Transactional**玩事务,传播行为默认是REQUIRED

添加事务:

1584784895102

这时,在保存商品的主方法中制造异常:

1584784987731

由于保存商品描述方法使用的是requires_new,spu应该会回滚,spu_desc应该保存成功。

清空pms_spu_desc表,再添加一个spu保存。

结果pms_spu_desc表中依然没有数据。

但是控制台打印了新增pms_spu_desc表的sql语句:

1584791120439

说明saveSpuDesc方法的事务回滚了,也就是说该方法配置的事务传播机制没有生效。

解决方案:

把service方法放到不同的service中使用动态代理对象调用该方法

1.4.4. 测试2:不同service + requires_new

把saveSpuDesc方法放到SpuDescService中:

1584791341509

在实现类中实现该方法,可以把之前的实现copy过来:

1584791517742

改造SpuServiceImpl中保存商品的方法,调用SpuDescServiceImpl的saveSpuDesc方法:

1584791613579

再次重启gmall-pms,虽然控制台依然报错,但是数据可以保存成功,说明没有在一个事务中。

1567687774435

为什么测试1的事务传播行为没有生效,而测试2的事务传播行为生效了?

spring的事务是声明式事务,而声明式事务的本质是Spring AOP,SpringAOP的本质是动态代理。

事务要生效必须是代理对象在调用。

测试1:通过this调用同一个service中的方法,this是指service实现类对象本身,不是代理对象,就相当于方法中的代码粘到了大方法里面,相当于还是一个方法。

测试2:通过其他service对象(spuDescService)调用,这个service对象本质是动态代理对象

接下来debug,打个断点看看:

spuDescService:

1567689094127

this:

1567689136840

1.4.5. 在同一个service中使用传播行为

只需要把测试1中的this.方法名()替换成this代理对象.方法名()即可。

问题是怎么在service中获取当前类的代理对象?

在类中获取代理对象分三个步骤:

  1. 导入aop的场景依赖:spring-boot-starter-aop
  2. 开启AspectJ的自动代理,同时要暴露代理对象:@EnableAspectJAutoProxy(exposeProxy=true)
  3. 获取代理对象:SpuInfoService proxy = (SpuInfoService) AopContext.currentProxy();

具体如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

1567690207315

1567690368936

重启后测试:先清空pms_spu_info_desc表中数据

1567690577522

表中数据新增成功,说明saveSpuDesc方法走的是自己的事务,传播行为生效了。

debug可以看到,spuInfoService是一个代理对象。

1567690539055

1.5. 回滚策略

事务很重要的另一个特征是程序出异常时,会回滚。但并不是所有的异常都会回滚。

默认情况下的回滚策略:

  • 运行时异常:不受检异常,没有强制要求try-catch,都会回滚。例如:ArrayOutOfIndex,OutofMemory,NullPointException
  • 编译时异常:受检异常,必须处理,要么try-catch要么throws,都不回滚。例如:FileNotFoundException

可以通过@Transactional注解的下面几个属性改变回滚策略:

1567670630801

  1. rollbackFor:指定的异常必须回滚
  2. noRollbackFor:发生指定的异常不用回滚

1.5.1. 测试编译时异常不回滚

在商品保存方法中制造一个编译时异常:

1584791955821

重启测试,注意pms_spu表中数据:

控制台报异常:

1567691494892

pms_spu表中的数据新增成功了。

1567691611593

也就证明了编译时异常不回滚。

1.5.2. 定制回滚策略

经过刚才的测试,我们知道:

ArithmeticException异常(int i = 1/0)会回滚FileNotFoundException异常(new FileInputStream(“xxxx”))不回滚

接下来我们来改变一下这个策略:

1584792095693

测试:

FileNotFoundException:在程序中添加new FileInputStream(“xxxx”),然后测试。

1567692233983

还是id还是17,说明回滚了(回滚也会占用id=18)

ArithmeticException:在程序中添加int i = 1/0; 然后测试。

1567692364759

id是19,说明没有回滚。

1.6. 超时事务

@Transactional注解,还有一个属性是timeout超时时间,单位是秒。

1567692523018

timeout=3:是指第一个sql开始执行到最后一个sql结束执行之间的间隔时间。

即:超时时间(timeout)是指数据库超时,不是业务超时。

改造之前商品保存方法:SpuInfoServiceImpl类中

1584792229794

重启测试:控制台出现事务超时异常

1567693383569

1.7. 只读事务

@Transactional注解最后一个属性是只读事务属性

1567693468270

如果一个方法标记为readOnly=true事务,则代表该方法只能查询,不能增删改。readOnly默认为false

给商品新增的事务标记为只读事务:

1584792311622

测试:

1567693694019

到此这篇关于spring boot本地事务@Transactional参数详解的文章就介绍到这了,更多相关spring boot事务Transactional内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用spring整合Quartz实现—定时器功能

    使用spring整合Quartz实现—定时器功能

    这篇文章主要介绍了使用spring整合Quartz实现—定时器功能,不基于特定的基类的方法,需要的朋友可以参考下
    2018-04-04
  • IDEA 隐藏DEBUG日志的解决方法

    IDEA 隐藏DEBUG日志的解决方法

    IDEA 打印太多的DEBUG日志,看起来很烦,有没有办法隐藏日志,网上找了一圈,没有谁写的靠谱的,下面小编给大家分享下IDEA 如何隐藏DEBUG日志,需要的朋友可以参考下
    2022-09-09
  • Java使用lambda表达式简化代码的示例详解

    Java使用lambda表达式简化代码的示例详解

    这篇文章主要给大家介绍了Java如何使用lambda表达式简化代码的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-11-11
  • java使用tess4j进行图片文字识别功能

    java使用tess4j进行图片文字识别功能

    Tess4J 是Java (JNA) 对 Tesseract OCR API 的封装,Tess4J是java直接可使用的jar包,而Tesseract OCR是支持Tess4J进文件文字识别的基础,Tess4J可直接使用Maven方式引入,这篇文章主要介绍了java使用tess4j进行图片文字识别,需要的朋友可以参考下
    2023-04-04
  • Java SpringBoot 中的操作事务

    Java SpringBoot 中的操作事务

    这篇文章主要介绍了Java SpringBoot 中的操作事务,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • Java实现九宫格的简单实例

    Java实现九宫格的简单实例

    这篇文章主要介绍了 Java实现九宫格的简单实例的相关资料,需要的朋友可以参考下
    2017-06-06
  • 基于ArrayList源码解析(基于JDK1.8)

    基于ArrayList源码解析(基于JDK1.8)

    这篇文章主要介绍了关于ArrayList源码解析(基于JDK1.8),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • Spring Boot中使用LDAP来统一管理用户信息的示例

    Spring Boot中使用LDAP来统一管理用户信息的示例

    本篇文章主要介绍了Spring Boot中使用LDAP来统一管理用户信息的示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • 从底层源码深入分析Spring的IoC容器的实现原理

    从底层源码深入分析Spring的IoC容器的实现原理

    IoC容器负责管理对象的生命周期和依赖关系,大大简化了应用程序的开发和维,我们这篇文章将会从底层源码的角度深入分析Spring的IoC容器实现,探索它的工作原理和关键组件,需要的朋友可以参考下
    2023-07-07
  • Nacos Discovery服务治理解决方案

    Nacos Discovery服务治理解决方案

    DiscoveryClient是专门负责服务注册和发现的,我们可以通过它获取到注册到注册中心的所有服务,这篇文章主要介绍了Nacos Discovery服务治理,需要的朋友可以参考下
    2022-11-11

最新评论