Spring 事务的传播机制

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Spring 事务的传播机制

在我们平常中, 说到传播肯定是扩散, 传送或者散布的意思. 在 Spring 的事务中, 它也有传播, 而Spring 中的事务传播它是一种机制即传播机制. 这个事务传播机制和我们说的传播定义很像, 也就是说在多个包含事务的方法里相互调用时, 它们之间是如何扩散或者传递的.


一. 传播机制的作用



我们之前学事务的隔离级别中, 解决的时多个事务同时调用数据库的问题. 它保证了多个并发但独立的事务执行时是可控的( 稳定性 ).

1d49422466c10b2016dfa9464ab9f687.png而在事务的传播机制中, 它保证的是一个事务在多个调用方法之间的可控性( 稳定性 )

b0b6c279dcb815be480f38256d51cc8a.png

比如在我们常说的运钞过程, 在这个过程中, 运钞人员有很多环节需要执行 : 点钞 -> 运钞 -> 清点等等. 而事务的传播机制就是保证运钞这个事务在运钞过程中是可靠的. 也就是在每个运钞环节中是可靠的.


二. 事务的传播机制



在 Spring 中事务的传播机制一共有七种 :


  1. Propagation.REQUIRED

默认的事务传播级别, 表示如果当前存在事务, 则加入该事务; 如果当前没有事务, 则创建一个新事物


  1. Propagation.SUPPORTS

如果当前存在事务, 则加入事务; 如果当前不存在事务, 则以非事务的方式继续运行


  1. Propagation.MANDATORY

如果当前存在事务, 则加入事务; 如果当前没有事务, 则抛出异常


  1. Propagation.REQUIRES_NEW

表示创建一个新的事物, 如果当前存在事务则把当前事务挂起. 即无论外部是否开启事务, REQUIRES_NEW 修饰的方法会新开启自己的事务, 并且开启的事务相互之间独立, 互不干扰


  1. Propagation.NOT_SUPPORTED

以非事务的方式运行, 如果当前存在事务则把当前事务挂起.


  1. Propagation.NEVER

以非事务的方式运行, 如果当前存在事务则抛出异常


  1. Propagation.NESTED

如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行; 如果当前不存在事务, 则等价于 Propagation.REQUIRED. 即创建一个新的事物


对于上面的七种事务, 根据是否支持当前事务可以划分为三类 :


  1. 支持当前事务 :
  1. REQUIRED ( 需要有 )
  2. SUPPORTS ( 可以有 )
  3. MANDATORY ( 强制有 )


  1. 不支持当前事务 :
  1. REQUIRES_NEW
  2. NOT_SUPPORTED
  3. NEVER


  1. 嵌套事务 :
  1. NESTED


三. 支持当前事务演示( REQUIRED )



为了方便演示, 需要建立两张表. 一张用户表和一张日志表. 通过往用户表中新增数据后加入成功添加的日志信息到日志表进行演示

38e249dd95af61d4e9590675fa3f9fb8.png

3720c690d2546565e5306cb34fcef0c8.png


具体的调用过程 :

50e070377b02b9360843b2aacdedfb84.png


1. 创建实体类


由于后面我会采用参数传实体对象的方式进行添加, 因此需要用户表的实体对象和日志表的实体对象


用户表实体对象 :

@Data // 记得加该注解
    public class UserEntity {
        private int id;
        private String username;
        private String password;
        private String photo;
        private LocalDateTime createtime;
        private LocalDateTime updatetime;
        private int state;
    }


日志表实体对象 :

@Data
    public class LogEntity {
        private int id;
        private String timestamp;
        private String message;
    }


2. 建立 Mybatis 接口方法


用户操作的接口方法

@Mapper
    public interface UserMapper {
        int add(UserEntity user); // 传入用户利于后期维护
    }


日志操作的接口方法

@Mapper
    public interface LogMapper {
        int addLog(LogEntity log);
    }


3. 建立Mybatis 的 XML 方法


用户操作接口中 add 方法的具体实现

<insert id="add" >
    insert into userinfo(username, password) values(
    #{username}, #{password}
)
    </insert>


日志操作接口中 add 方法的具体实现

<insert id="addLog">
    insert into log(message) values(
    #{message}
)
    </insert>


4. service 层调用 mapper 层


在日志操作的 LogService 中建立 add 方法( 开启事务并默认设置为 REQUIRED 传播级别 )调用 mapper 层中的 add 添加日志方法

@Service
    public class LogService {
        @Autowired
        private LogMapper logMapper;
        // 
        @Transactional(propagation = Propagation.REQUIRED)
        public int add(LogEntity log) {
            int result = logMapper.add(log);
            System.out.println("添加日志条数 : " + result);
            int a = 10 / 0;
            return result;
        }
    }


在用户操作的 UserService 中建立 add 方法 ( 开启事务并默认设置为 REQUIRED 传播级别 ) 调用 mapper 层中的 add 添加用户方法, 完成后同时调用 LogService 中的 add 方法添加日志

@Service
    public class UserService {
        @Autowired
        private UserMapper userMapper;
        @Autowired
        private LogService logService;
        @Transactional(propagation = Propagation.REQUIRED)
        public int add(UserEntity user) {
            // 添加用户
            int addListUserResult = userMapper.add(user);
            System.out.println("添加用户条数 : " + addListUserResult);
            // 添加日志信息
            LogEntity log = new LogEntity();
            log.setMessage("添加用户成功");
            logService.add(log);
            return addListUserResult;
        }
    }


5. controller 层调用 service 层


实际上应该创建一个 LogController 来控制 LogService, 为了方便演示直接在 UserService 中直接调用了 LogService.add( ) 方法

@RestController
    @RequestMapping("/user3")
    public class UserController3 {
        @Autowired
        private UserService userService;
        @RequestMapping("/add")
        @Transactional(propagation = Propagation.REQUIRED)
        public int add(String username, String password) {
            if (username == null || password == null ||
                username.equals("") || password.equals("")) return 0;
            int result = 0;
            UserEntity user = new UserEntity();
            user.setUsername(username);
            user.setPassword(password);
            result = userService.add(user);
            return result;
        }
    }


由于上面我们的三个添加方法都开启了事务, 并且设置的是默认的隔离级别, 由 UserController -> UserService -> UserMapper和LogMapper. 存在着这样的方法调用链. 根据我们的 REQUIRED 传播机制, 因为当前的 UserController 存在事务, 后面的 UserService 开启事务后会加入到当前的 UserController 的事务中, 在 UserService 里出现了异常, 会不会影响到整个调用链呢 ?


6. 预期验证


可以明确的是, 在LogService.add 方法中出现了算数异常, 那么日志肯定是会进行回滚操作的. 但是这个算数异常会不会影响到调用链上的 UserService.add 方法添加用户呢 ?


可以看到, 控制台显示用户已经添加成功了, 日志也添加了, 最后报出了算数异常而终止了

1eaaeebd70f22cb63377393a1d39ca73.png


先看数据库中的日志表 : 回滚了, 并没有添加数据, 符合异常后主动回滚的预期

d3e18370b3119cd3dfaca43e4c49cef7.png


再来看数据库中的用户表 : 可以看到, " wangwu " 这条数据是没有成功插入进来的.

f57e961f82d48249ed7594cb7b70a0a6.png


从上面的演示可以看到, 默认的传播机制 REQUIRED 调用链上的所有方法, 只要一个方法出现了异常, 那么这些方法开启的事务合并成的一个大流程的事务都会回滚. 也就是说无论是外部回滚还是内部回滚, 整个调用链都会回滚. 一荣俱荣一损俱损.


四. 不支持当前事务演示 ( REQUIRES_NEW )



将 UserController 中的添加方法以及 UserService 中添加用户方法和 LogService 中的添加日志方法开启事务后设置其事务的传播机制为 REQUIRES_NEW,

修改 UserController3 中的添加用户方法的传播机制

@RequestMapping("/add")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(String username, String password) {
        if (username == null || password == null ||
            username.equals("") || password.equals("")) return 0;
        int result = 0;
        UserEntity user = new UserEntity();
        user.setUsername(username);
        user.setPassword(password);
        result = userService.add(user);
        return result;
    }


修改 UserService 中的添加用户方法的传播机制

@Service
    public class LogService {
        @Autowired
        private LogMapper logMapper;
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public int add(LogEntity log) {
            int result = logMapper.add(log);
            System.out.println("添加日志条数 : " + result);
            int a = 10 / 0;
            return result;
        }
    }


修改 LogService 中的添加日志方法的传播机制

@Service
    public class LogService {
        @Autowired
        private LogMapper logMapper;
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public int add(LogEntity log) {
            int result = logMapper.add(log);
            System.out.println("添加日志条数 : " + result);
            int a = 10 / 0;
            return result;
        }
    }


对于上面的代码, 由于设置其隔离级别为 REQUIRES_NEW 表示开启新的事务, 并且和其他事务独立. 因此在 LogService 中出现算数异常会进行回滚, 但并不会影响调用链上的其他事务. 因此用户应该正常添加, 日志进行回滚.


同样调用 UserController3 里的添加方法观察

a2d431b3a580de350e27c0a586aed13d.png


在控制台中看到, 用户已经添加, 日志也已经显示添加

image.png


数据库中查看却发现, 日志虽然回滚了, 但是用户添加也跟着回滚了. 这是为什么 ?

f2ae83596adae096ebc849edd4a5b369.png


对于此事不符合我们设置的传播机制的预期结果而言, 是因为设置了事务注解, 此时虽然日志添加出现了算数异常, 它是一个单独的事务. 但在这一整个事务的调用链上出现了异常, 让外面的 UserController 的添加方法 和 UserService 的添加方法感知到了异常, 因此进行了回滚.


因此, 对于日志异常这个问题, 我们可以单独抛出异常处理后再来观察 REQUIRES_NEW 的传播机制

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(LogEntity log) {
        int result = 0;
try {
    result = logMapper.add(log);
    System.out.println("添加日志条数 : " + result);
    int a = 10 / 0;
} catch (Exception e) {
    // 主动对异常进行处理回滚
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return result;
}


此时再去访问刚刚的路由方法就不在报错了, 因我们已经捕获异常并主动处理了

9aad3a0d9b95405980ab4dfbf36a35d5.png44a8dffb59eb25f9809be9d951c49b29.png


此时在到数据库中查看可以看到, 日志正确回滚. 但添加用户并不受到算数异常的影响正确添加了

064bad2d42da74fe37fb794736fe6648.png


也就是说, REQUIRES_NEW 传播机制中, 会建立新的事务, 并且这些事务都是相互独立的, 互不干扰. 也就是常说的各干各的.


五. 嵌套式事务演示



嵌套式事务就一个高中有三个年级, 一个年级又有很多个班一样. 类似于套娃的的形式.

同样的将上面的 UserController 中的添加方法以及 UserService 中添加用户方法和 LogService 中的添加日志方法开启事务后设置其事务的传播机制为 NESTED 嵌套式 ( 我就不重复演示修改了 ) . 其余代码还是刚刚演示不支持当前事务的代码

7968fa13c9d6870d2cdde282e75b3a55.png

访问同样的路由方法

3a5d0bf31a3bc3fb3c16559a325a8b0e.png

可以看到, 日志和用户都添加成功了. 由于在日志添加中主动进行算数异常的回滚. 因此日志是会回滚的. 看看用户添加是否也回滚了呢 ?

aa3b7fe6858295a1de2623b7142f8a47.png


在数据库中可以看到, 日志正确回滚了. 但是用户也正常添加了. 这就是嵌套式事务.


73a5d36d311d830f0880695104084453.png


肯定有细心的人发现了, 和我们上面演示的不支持当前事务的 REQUIRES_NEW 效果一样. 它两有什么区别呢 ?


六. 嵌套式事务和加入式事务的区别



区别在于 REQUIRES_NEW 无论当前存不存在事务都会建立一个新的事务, 而我们的 NESTED 如果当前存在事务则会把这个嵌套进到当前事务中, 并不会创建新事务.

**嵌套事务之所以能够实现部分事务的回滚,是因为事务中有⼀个保存点(savepoint)的概念,嵌套事务 进⼊之后相当于新建了⼀个保存点,⽽滚回时只回滚到当前保存点,因此之前的事务是不受影响的. 而 REQUIRED 是加⼊到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚, 这就是嵌套事务和加⼊事务的区别 **


  • 整个事务如果执行成功, 二者结果就是一样的
  • 如果事务执行到一半失败了, 那么加入事务整个事务会全部回滚. 而嵌套事务会局部回滚, 不会影响上一个方法中执行的结果.


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
29天前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
2月前
|
Java 关系型数据库 MySQL
Spring 事务失效场景总结
Spring 事务失效场景总结
43 4
|
29天前
|
Java 程序员 数据库连接
女朋友不懂Spring事务原理,今天给她讲清楚了!
该文章讲述了如何解释Spring事务管理的基本原理,特别是针对女朋友在面试中遇到的问题。文章首先通过一个简单的例子引入了传统事务处理的方式,然后详细讨论了Spring事务管理的实现机制。
女朋友不懂Spring事务原理,今天给她讲清楚了!
|
27天前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
|
29天前
|
前端开发 Java 数据库连接
一天十道Java面试题----第五天(spring的事务传播机制------>mybatis的优缺点)
这篇文章总结了Java面试中的十个问题,包括Spring事务传播机制、Spring事务失效条件、Bean自动装配方式、Spring、Spring MVC和Spring Boot的区别、Spring MVC的工作流程和主要组件、Spring Boot的自动配置原理和Starter概念、嵌入式服务器的使用原因,以及MyBatis的优缺点。
|
1月前
|
Java 关系型数据库 MySQL
Spring Boot事务配置管理
主要总结了 Spring Boot 中如何使用事务,只要使用 @Transactional 注解即可使用,非常简单方便。除此之外,重点总结了三个在实际项目中可能遇到的坑点,这非常有意义,因为事务这东西不出问题还好,出了问题比较难以排查,所以总结的这三点注意事项,希望能帮助到开发中的朋友。
|
2月前
|
Java 数据库连接 API
Spring事务管理嵌套事务详解 : 同一个类中,一个方法调用另外一个有事务的方法
Spring事务管理嵌套事务详解 : 同一个类中,一个方法调用另外一个有事务的方法
|
3月前
|
XML Java 数据库
Spring框架第五章(声明式事务)
Spring框架第五章(声明式事务)
|
2月前
|
Java Spring
spring 事务控制 设置手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
spring 事务控制 设置手动回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
2月前
|
XML Java 关系型数据库
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景