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日志并进行多维度分析。
相关文章
|
18天前
|
缓存 安全 Java
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
40 2
Spring高手之路26——全方位掌握事务监听器
|
20天前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
1月前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
52 1
Spring高手之路24——事务类型及传播行为实战指南
|
24天前
|
JavaScript Java 关系型数据库
Spring事务失效的8种场景
本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。
|
1月前
|
XML Java 数据库连接
Spring中的事务是如何实现的
Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
47 3
|
3月前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
3月前
|
Java 测试技术 数据库
Spring事务传播机制(最全示例)
在使用Spring框架进行开发时,`service`层的方法通常带有事务。本文详细探讨了Spring事务在多个方法间的传播机制,主要包括7种传播类型:`REQUIRED`、`SUPPORTS`、`MANDATORY`、`REQUIRES_NEW`、`NOT_SUPPORTED`、`NEVER` 和 `NESTED`。通过示例代码和数据库插入测试,逐一展示了每种类型的运作方式。例如,`REQUIRED`表示如果当前存在事务则加入该事务,否则创建新事务;`SUPPORTS`表示如果当前存在事务则加入,否则以非事务方式执行;`MANDATORY`表示必须在现有事务中运行,否则抛出异常;
181 4
Spring事务传播机制(最全示例)
|
2月前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
39 2
|
2月前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题
|
3月前
|
Java Spring
Spring 事务传播机制是什么?
Spring 事务传播机制是什么?
27 4
下一篇
DataWorks