• Spring事务与事务传播机制


    目录

    1.事务的基本概念

    2.Spring事务的实现

    3.事务隔离级别

    4.事务传播机制


    1.事务的基本概念

    关于事务的一些基础概念我已经在MYSQL中讲解过了,有不了解的可以移步至此篇文章:
    MySQL基础——数据库索引与事务_invictusQAQ的博客-CSDN博客

    2.Spring事务的实现

    Spring 中的事务操作分为两类:

    1. 编程式事务(⼿动写代码操作事务)。

    2. 声明式事务(利⽤注解⾃动开启和提交事务)。

    在我们讲解他们如何使用之前,我们先来回顾一下MYSQL中事务的使用

    2.1 回顾MYSQL中事务的使用

    1. -- 开启事务
    2. start transaction;
    3. -- 业务执⾏
    4. -- 提交事务
    5. commit;
    6. -- 回滚事务
    7. rollback;

     2.2 Spring 编程式事务

    当然此种方法较为麻烦,而且实际也很少使用,所以我们仅做了解

    Spring ⼿动操作事务和上⾯ MySQL 操作事务类似,它也是有 3 个重要操作步骤:

    1.开启事务(获取事务)。

    2.提交事务。

    3.回滚事务。

    SpringBoot 内置了两个对象,DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或 回滚事务的,而TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus,实现代码如下: 

    1. @Autowired
    2. private DataSourceTransactionManager dataSourceTransactionManager;
    3. @Autowired
    4. private TransactionDefinition transactionDefinition;
    5. @Autowired
    6. private UserService userService;
    7. @RequestMapping("/add")
    8. public int add(UserInfo userInfo){
    9. //参数合法性判断
    10. if(userInfo==null||!StringUtils.hasLength(userInfo.getUsername())
    11. || !StringUtils.hasLength(userInfo.getPassword())) return 0;
    12. // 开启事务(获取事务)
    13. TransactionStatus transactionStatus=dataSourceTransactionManager.getTransaction(transactionDefinition);
    14. int result= userService.add(userInfo);
    15. System.out.println("add 受影响的行数:" + result);
    16. //回滚事务
    17. dataSourceTransactionManager.rollback(transactionStatus);
    18. //或者提交事务
    19. //dataSourceTransactionManager.commit(transactionStatus);
    20. return result;
    21. }

    2.3 Spring 声明式事务(自动)

    声明式事务的实现很简单,只需要在需要的⽅法上添加 @Transactional 注解就可以实现了,无需手动 开启事务和提交事务,进入方法时⾃动开启事务,方法执行完会⾃动提交事务,如果中途发生了没有处 理的异常会自动回滚事务,具体实现代码如下:

    1. // 使用声明式事务
    2. @Transactional
    3. //@Transactional内部可以设置隔离级别等其他属性
    4. @RequestMapping("/add2")
    5. public int add2(UserInfo userInfo){
    6. if(userInfo==null||!StringUtils.hasLength(userInfo.getUsername())
    7. || !StringUtils.hasLength(userInfo.getPassword())) return 0;
    8. // 开启事务(获取事务)
    9. int result= userService.add(userInfo);
    10. System.out.println("add 受影响的行数:" + result);
    11. return result;
    12. }

    还是之前的实现添加用户的功能,但是代码却简洁了不少

    2.3.1 @Transactional 作用范围 

    @Transactional 可以用来修饰方法或类:

    修饰方法时:需要注意只能应用到 public ⽅法上,否则不⽣效。推荐此种⽤法。

    修饰类时:表明该注解对该类中所有的 public ⽅法都⽣效。

    2.3.2 @Transactional 参数说明 

    前面我们提到@Transactional是可以设置多种参数的,比如下面:

     而它们的具体含义如下,大家可以根据实际情况灵活去选择

     2.3.3 注意事项

    前面我们提到了@Transactional在事务内部出现异常时是会自动回滚的,这个也可以配合我们前面提到了@Transactional的rollbackFor等和异常处理相关的参数使用。但是假如我们使用了try-catch语句去处理了可能出现异常的代码段,那么此时即使发生了异常事务也不会回滚。

    例如下面的代码,它由于使用了try-catch语句捕获异常,所以此时@Transactional就不会再去处理该异常,自然也就不会对事务进行回滚。因为Spring认为你已经使用了try-catch使用他就不会再去干涉了。

    1. @RestController
    2. public class UserController {
    3. @Resource
    4. private UserService userService;
    5. @RequestMapping("/save")
    6. @Transactional
    7. public Object save(User user) {
    8. // 插⼊数据库
    9. int result = userService.save(user);
    10. try {
    11. // 执⾏了异常代码(0不能做除数)
    12. int i = 10 / 0;
    13. } catch (Exception e) {
    14. System.out.println(e.getMessage());
    15. }
    16. return result;
    17. }
    18. }

    那么我们如何让事务在这种情况下依然能够实现自动回滚呢? 

    解决方案1:对于捕获的异常,事务是会⾃动回滚的,因此解决方案1就是可以将异常重新抛出,具体实现如下:

    1. @RequestMapping("/save")
    2. @Transactional(isolation = Isolation.SERIALIZABLE)
    3. public Object save(User user) {
    4. // 插⼊数据库
    5. int result = userService.save(user);
    6. try {
    7. // 执⾏了异常代码(0不能做除数)
    8. int i = 10 / 0;
    9. } catch (Exception e) {
    10. System.out.println(e.getMessage());
    11. // 将异常重新抛出去
    12. throw e;
    13. }
    14. return result;
    15. }

    当然此种方法将异常捕获后又重新抛出,似乎很奇怪,所以还有解决方法2。

    解决方案2:⼿动回滚事务,在方法中使用 TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚方法 setRollbackOnly 就可以实现回滚了,具体实现代码如下:

    1. @RequestMapping("/save")
    2. @Transactional(isolation = Isolation.SERIALIZABLE)
    3. public Object save(User user) {
    4. // 插⼊数据库
    5. int result = userService.save(user);
    6. try {
    7. // 执⾏了异常代码(0不能做除数)
    8. int i = 10 / 0;
    9. } catch (Exception e) {
    10. System.out.println(e.getMessage());
    11. // ⼿动回滚事务
    12. TransactionAspectSupport.currentTransactionStatus().setRollbackOnl
    13. y();
    14. }
    15. return result;
    16. }

    2.3.4 @Transactional 工作原理

    @Transactional 是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果⽬标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。 @Transactional 在开始执行业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途 遇到的异常,则回滚事务。

    @Transactional 具体执行细节如下图所示:

     3.事务隔离级别

     3.1 事务特性回顾

    1.原子性:最核心的特性,即逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部失败

    2.一致性:保证数据一致,没有纰漏

    3.持久性:只要事务执行成功,造成的修改就是可持久化保存的(保存在磁盘/硬盘中)

    4.隔离性:描述多个事务并行执行所发生的情况

    而这 4 种特性中,只有隔离性(隔离级别)是可以设置的。 

    为什么要设置事务的隔离级别?

    设置事务的隔离级别是⽤来保障多个并发事务执⾏更可控,更符合操作者预期的。

    其实再说得通俗一点就是我们可以通过设置事务的隔离级别来控制脏读,不可重复读,幻读的现象发生与否

    3.2 Spring 中设置事务隔离级别 

    Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置,具体操作如下图所 示:

    3.2.1 MySQL 事务隔离级别 

    在讲解Spring中的事务隔离级别之前我们先来回顾一下MYSQL中的事务隔离级别。

    1. READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提 交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。

    2. READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据, 因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间 的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。

    3. REPEATABLE READ:可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询 的结果⼀致。但也会有新的问题,比如此级别的事务正在执⾏时,另⼀个事务成功的插⼊或者删除了某条数据,此时再去查询数据会发现突然多了或者少了部分数据,好像出现了幻觉,这就叫幻读 (Phantom Read)。

    4. SERIALIZABLE:序列化,事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从而解决 了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使⽤的场景并不多。

     这里我们重点区分一下不可重复读与幻读的区别:

    不可重复读强调的重点在于修改(update)而幻读强调的重点在于插入和删除(insert/delete)

     

    3.2.2 Spring 事务隔离级别 

     Spring 中事务隔离级别包含以下 5 种,仅仅只比MYSQL多了一种DEFAULT级别:

    1. Isolation.DEFAULT:以连接的数据库的事务隔离级别为主

    2. Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读

    3. Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重 复读

    4. Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级 别)

    5. Isolation.SERIALIZABLE:串行化,可以解决所有并发问题,但性能太低

    注意事项:

     1.当Spring设置了事务隔离级别和连接的数据库(MYSQL)隔离级别冲突时,以Spring的隔离级别为准

    2.Spring事务中的事务隔离级别机制的实现是依靠连接数据库支持的隔离级别为基础

    4.Spring事务传播机制

    4.1 什么是事务传播机制

    Spring 事务传播机制定义了多个包含了事务的⽅法,相互调用时,事务是如何在这些⽅法间进⾏传递的。

    4.2 为什么需要事务传播机制

    事务隔离级别是保证多个并发事务执行的可控性的(稳定性的),而事务传播机制是保证⼀个事务在多个调⽤⽅法间的可控性的(稳定性的)。

    直观的去理解就是事务隔离级别是保证事务并行可控性,类似于下图:

     而事务传播机制是解决了事务调用链的可控性,类似于下图:

     4.3 事务的传播机制有哪些

    Spring 事务传播机制包含以下 7 种:

    1. Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。

    2. Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的方式继续运行。

    3. Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当 前没有事务,则抛出异常。

    4. Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂 起。也就是说不管外方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开 启⾃⼰的事务,且开启的事务相互独立,互不干扰。

    5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

    6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

    7. Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏;如 果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。


    我们可以通过将其分类来帮助我们记忆 

     

     

    4.4 Spring 事务传播机制使用和各种场景演示 

    首先我们搭建测试所需环境,此处我们依然选择模拟ssm项目结构,结构如下:

     我们程序的执行逻辑大概如下:

     UserMapper:

    1. package com.example.springtransactiondemo.mapper;
    2. import com.example.springtransactiondemo.model.UserInfo;
    3. import org.apache.ibatis.annotations.Mapper;
    4. @Mapper
    5. public interface UserMapper {
    6. public int add(UserInfo userInfo);
    7. }

    LogMapper:

    1. package com.example.springtransactiondemo.mapper;
    2. import com.example.springtransactiondemo.model.LogInfo;
    3. import org.apache.ibatis.annotations.Mapper;
    4. @Mapper
    5. public interface LogMapper {
    6. public int add(LogInfo logInfo);
    7. }

    UserService:

    1. package com.example.springtransactiondemo.service;
    2. import com.example.springtransactiondemo.mapper.UserMapper;
    3. import com.example.springtransactiondemo.model.UserInfo;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.stereotype.Service;
    6. import org.springframework.transaction.annotation.Propagation;
    7. import org.springframework.transaction.annotation.Transactional;
    8. @Service
    9. public class UserService {
    10. @Autowired
    11. private UserMapper userMapper;
    12. @Transactional
    13. public int add(UserInfo userInfo){
    14. int result= userMapper.add(userInfo);
    15. return result;
    16. }
    17. }

    LogService:

    1. package com.example.springtransactiondemo.service;
    2. import com.example.springtransactiondemo.mapper.LogMapper;
    3. import com.example.springtransactiondemo.model.LogInfo;
    4. import org.springframework.beans.factory.annotation.Autowired;
    5. import org.springframework.stereotype.Service;
    6. import org.springframework.transaction.annotation.Propagation;
    7. import org.springframework.transaction.annotation.Transactional;
    8. @Service
    9. public class LogService {
    10. @Autowired
    11. private LogMapper logMapper;
    12. @Transactional
    13. public int add(LogInfo logInfo) {
    14. int result = logMapper.add(logInfo);
    15. System.out.println("添加日志结果:" + result);
    16. int num=1/0;//模拟发生异常的情况
    17. return result;
    18. }
    19. }

    而为了测试我们spring的事务传播机制,最终我们UserController的执行逻辑如下:

     所以我们得到了我们测试所需的UserController代码如下:

    1. package com.example.springtransactiondemo.controller;
    2. import com.example.springtransactiondemo.model.LogInfo;
    3. import com.example.springtransactiondemo.model.UserInfo;
    4. import com.example.springtransactiondemo.service.LogService;
    5. import com.example.springtransactiondemo.service.UserService;
    6. import org.springframework.beans.factory.annotation.Autowired;
    7. import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    8. import org.springframework.stereotype.Controller;
    9. import org.springframework.transaction.TransactionDefinition;
    10. import org.springframework.transaction.TransactionStatus;
    11. import org.springframework.transaction.annotation.Transactional;
    12. import org.springframework.transaction.interceptor.TransactionAspectSupport;
    13. import org.springframework.util.StringUtils;
    14. import org.springframework.web.bind.annotation.RequestMapping;
    15. import org.springframework.web.bind.annotation.RestController;
    16. import javax.annotation.sql.DataSourceDefinition;
    17. @RestController
    18. public class UserController {
    19. @Autowired
    20. private UserService userService;
    21. @Autowired
    22. private LogService logService;
    23. @Transactional
    24. @RequestMapping("/add")
    25. public int add3(UserInfo userInfo){
    26. //参数合法性校验
    27. if (userInfo == null ||
    28. !StringUtils.hasLength(userInfo.getUsername()) ||
    29. !StringUtils.hasLength(userInfo.getPassword())) {
    30. return 0;
    31. }
    32. int userResult=userService.add(userInfo);
    33. System.out.println("添加用户:" + userResult);
    34. LogInfo logInfo=new LogInfo();
    35. logInfo.setName("添加用户");
    36. logInfo.setDesc("添加用户结果:" + userResult);
    37. int logResult= logService.add(logInfo);
    38. return userResult;
    39. }
    40. }

     4.4.1 支持当前事务(REQUIRED)

    以下代码实现中,先开启事务先成功插⼊⼀条⽤户数据,然后再执行日志报错,而在日志报错是发⽣了 异常,观察 propagation = Propagation.REQUIRED 的执行结果。

    这里的测试代码我们在上面已经放出来了,就不在赘述。而得到的结果是程序报错,数据库没有插入任何数据。

    执行流程描述:

    1. UserService 中的保存⽅法正常执⾏完成。

    2. LogService 保存日志程序报错,因为使⽤的是 Controller 中的事务,所以整个事务回滚。 3. 数据库中没有插入任何数据,也就是步骤 1 中的用户插⼊⽅法也回滚了。 

    4.4.2 不支持当前事务(REQUIRES_NEW) 

    UserController 类中的代码不变,将添加用户和添加日志的方法修改为 REQUIRES_NEW 不⽀持当前事务,重新创建事务,观察执行结果:

    此处我们的LogService依然模拟抛出异常,但此次用户数据插入成功,日志数据插入失败被回滚了。说明REQUIRES_NEW创建的两个新事务是独立的,不会互相影响。

    4.4.3 NESTED 嵌套事务 

    UserController 类中的代码不变,将添加用户和添加日志的方法修改为 Propagation.NESTED 嵌套事务,重新创建事务,观察执行结果:

    此处我们的LogService依然模拟抛出异常,此次用户数据插入成功,日志数据插入失败被回滚了。说明嵌套事务是可以部分回滚的,没有因为嵌套事务抛出异常而导致同一层的嵌套事务以及外层的事务报错回滚。

    4.5 事务传播机制的相关注意事项 

    4.5.1 REQUIRED / REQUIRES_NEW / NESTED的区别

    REQUIRED会加入当前的事务,此时可以看作两个事务融为一体,其中一个事务报错另一个也会一起陪它回滚。而REQUIRES_NEW则是创建了新的事务,和别的事务没有关系,所以同层次的另一个事务报错不影响该事务继续执行。而NESTED嵌套事务 进入之后相当于新建了⼀个保存点,滚回时只回滚到当前保存点,因此之前的事务是不受影响的,所以可以实现部分回滚。

    4.5.2 有关事务自调用问题  

    以下部分内容来自一个@Transactional哪里来这么多坑?_Foo.的博客-CSDN博客

    部分内容根据自己的理解进行了修改 

    1. @Service
    2. public class DmzService {
    3. public void saveAB(A a, B b) {
    4. saveA(a);
    5. saveB(b);
    6. }
    7. @Transactional
    8. public void saveA(A a) {
    9. dao.saveA(a);
    10. }
    11. @Transactional
    12. public void saveB(B b){
    13. dao.saveB(a);
    14. }
    15. }

    上面三个方法都在同一个类DmzService中,其中saveAB方法中调用了本类中的saveA跟saveB方法,这就是自调用。在上面的例子中saveA跟saveB上的事务会失效

    那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP的,当容器在创建dmzService这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类:

    1. public class DmzServiceProxy {
    2. private DmzService dmzService;
    3. public DmzServiceProxy(DmzService dmzService) {
    4. this.dmzService = dmzService;
    5. }
    6. public void saveAB(A a, B b) {
    7. dmzService.saveAB(a, b);
    8. }
    9. public void saveA(A a) {
    10. try {
    11. // 开启事务
    12. startTransaction();
    13. dmzService.saveA(a);
    14. } catch (Exception e) {
    15. // 出现异常回滚事务
    16. rollbackTransaction();
    17. }
    18. // 提交事务
    19. commitTransaction();
    20. }
    21. public void saveB(B b) {
    22. try {
    23. // 开启事务
    24. startTransaction();
    25. dmzService.saveB(b);
    26. } catch (Exception e) {
    27. // 出现异常回滚事务
    28. rollbackTransaction();
    29. }
    30. // 提交事务
    31. commitTransaction();
    32. }
    33. }

    简单的说就是我们理想情况下应该是先通过代理类然后采取调用目标方法,但实际上我们会通过this来直接调用目标方法,也就是说此时我们没有经过代理类而是直接通过this来调用,此时就导致了代理失效的问题。

    常见的自调用导致的事务失效还有一个例子,如下:

    1. @Service
    2. public class DmzService {
    3. @Transactional
    4. public void save(A a, B b) {
    5. saveB(b);
    6. }
    7. @Transactional(propagation = Propagation.REQUIRES_NEW)
    8. public void saveB(B b){
    9. dao.saveB(a);
    10. }
    11. }

     当我们调用save方法时,我们预期的执行流程是这样的:

     但还是由于this的存在,所以我们实际上还是会不经过代理去直接通过this调用目标方法导致事务失效。

    4.5.3 关于REQUIRES_NEW的补充

    REQUIRES_NEW官方文档解释:
    Create a new transaction, and suspend the current transaction if one exists.
    意思是,创建一个新事务,如果当前存在事务,将这个事务挂起。也就是说如果当前存在事务,那么将当前的事务挂起,并开启一个新事务去执行REQUIRES_NEW标志的方法。

    但是当它在嵌套调用的时候有几点我们是需要注意的:

    1.标志REQUIRES_NEW会新开启事务,外层事务不会影响内部事务的提交/回滚
    2.标志REQUIRES_NEW的内部事务的异常,会影响外部事务的回滚 

    我们也可以结合NESTED一起来记忆

    REQUIRES_NEW 和 NESTED ,前者是内层异常影响外层,外层不影响内层;后者正好相反,内层加try catch后 异常不影响外层,外层会影响内层。 

    4.5.4 REQUIRES与REQUIRES_NEW嵌套场景简单总结 

    1.同类中:无事务方法 嵌套 事务方法 ,事务不生效,因spring 中事务基于代理实现。详细原理请见4.5.2

    2.同类中:REQUIRES 嵌套 REQUIRES_NEW ,REQUIRES_NEW不生效,加入到REQUIRES事务中,原理同上。

    3.同类中:REQUIRES(1) 嵌套 REQUIRES (2),其实REQUIRES(2)事务注解也是不生效的,只是加入到REQUIRES(1)事务中,看起来REQUIRES(2)事务注解是生效的,原理同上。

    4.同类中:REQUIRES(1) 嵌套 REQUIRES或REQUIRES_NEW (2),(1) 中 try {(2)}catch{e.printStackTrace();} , 2异常,1与2都不会回滚,因为2实际无事务,异常也被1 catch,故都不回滚。

    5.不同类中:REQUIRES 嵌套 REQUIRES_NEW,情况1:REQUIRES无异常,REQUIRES_NEW发生异常,REQUIRES与REQUIRES_NEW都回滚。

    6.不同类中:REQUIRES 嵌套 REQUIRES_NEW,情况2:REQUIRES(1)发生异常,REQUIRES_NEW(2)无异常,1的事务回滚,2的事务正常提交。这种情况在4.5.3提及过。

    7.不同类中:REQUIRES(1)嵌套 REQUIRES(2),(1) 中 try {(2)}catch{e.printStackTrace();} , 2异常 , 1与2都会回滚(尽管catch了),因为1,2在同一事务中,发生异常Rolling back,故都回滚(同一事务中,要么都提交,要么都不提交)。

    8.不同类中:REQUIRES(1)嵌套 REQUIRES_NEW(2),(1) 中 try {(2)}catch{e.printStackTrace();} , 2异常, 1不回滚,2回滚,因为2在新事务中,发生异常Rolling back,且异常被1catch不被1感知,故1不回滚,2回滚。

  • 相关阅读:
    为啥这么做实现不了优雅关闭服务,gin 项目
    java97-中断线程的另一种处理
    使用Stable Diffusion生成艺术二维码
    【智能家居入门2】(MQTT协议、微信小程序、STM32、ONENET云平台)
    【STM32】IAP升级03关闭总中断,检测栈顶指针
    椭球面的切平面
    优酷弹幕穿人「渲染技术」揭秘
    微信小程序:实现音乐播放器的功能
    TCP Window Full & TCP Zero Window
    ResNet网络结构,BN以及迁移学习详解
  • 原文地址:https://blog.csdn.net/weixin_60778429/article/details/128053071