SpringBoot系列教程之事务传递属性

本文涉及的产品
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: SpringBoot系列教程之事务传递属性


200202-SpringBoot系列教程之事务传递属性

对于mysql而言,关于事务的主要知识点可能几种在隔离级别上;在Spring体系中,使用事务的时候,还有一个知识点事务的传递属性同样重要,本文将主要介绍7中传递属性的使用场景

I. 配置

本文的case,将使用声明式事务,首先我们创建一个SpringBoot项目,版本为2.2.1.RELEASE,使用mysql作为目标数据库,存储引擎选择Innodb,事务隔离级别为RR

1. 项目配置

在项目pom.xml文件中,加上spring-boot-starter-jdbc,会注入一个DataSourceTransactionManager的bean,提供了事务支持

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

2. 数据库配置

进入spring配置文件application.properties,设置一下db相关的信息

## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=

3. 数据库

新建一个简单的表结构,用于测试

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. 使用说明

0. 准备

在正式开始之前,得先准备一些基础数据

@Component
public class PropagationDemo {
    @Autowired
    private JdbcTemplate jdbcTemplate;
 
    @PostConstruct
    public void init() {
        String sql = "replace into money (id, name, money) values (420, '初始化', 200)," + "(430, '初始化', 200)," +
                "(440, '初始化', 200)," + "(450, '初始化', 200)," + "(460, '初始化', 200)," + "(470, '初始化', 200)," +
                "(480, '初始化', 200)," + "(490, '初始化', 200)";
        jdbcTemplate.execute(sql);
    }
}

其次测试事务的使用,我们需要额外创建一个测试类,后面的测试case都放在类PropagationSample中; 为了使输出结果更加友好,提供了一个封装的call方法

@Component
public class PropagationSample {
    @Autowired
    private PropagationDemo propagationDemo;
    
    private void call(String tag, int id, CallFunc<Integer> func) {
        System.out.println("============ " + tag + " start ========== ");
        propagationDemo.query(tag, id);
        try {
            func.apply(id);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        propagationDemo.query(tag, id);
        System.out.println("============ " + tag + " end ========== \n");
    }
 
 
    @FunctionalInterface
    public interface CallFunc<T> {
        void apply(T t) throws Exception;
    }
}

1. REQUIRED

也是默认的传递属性,其特点在于

  • 如果存在一个事务,则在当前事务中运行
  • 如果没有事务则开启一个新的事务

使用方式也比较简单,不设置@Transactional注解的propagation属性,或者设置为 REQUIRED即可

/**
 * 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务
 *
 * @param id
 */
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void required(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("required: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
 
    throw new Exception("事务回滚!!!");
}

上面就是一个基础的使用姿势

private void testRequired() {
    int id = 420;
    call("Required事务运行", id, propagationDemo::required);
}

输出结果如下

============ Required事务运行 start ========== 
Required事务运行 >>>> {id=420, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
required: after updateMoney name >>>> {id=420, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事务回滚!!!
Required事务运行 >>>> {id=420, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ Required事务运行 end ========== 

2. SUPPORTS

其特点是在事务里面,就事务执行;否则就非事务执行,即

  • 如果存在一个事务,支持当前事务
  • 如果没有事务,则非事务的执行

使用姿势和前面基本一致

@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public void support(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("support: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
 
    throw new Exception("事务回滚!!!");
}

这个传递属性比较特别,所以我们的测试case需要两个,一个事务调用,一个非事务调用

测试事务调用时,我们新建一个bean: PropagationDemo2,下面的support方法支持事务运行

@Component
public class PropagationDemo2 {
    @Autowired
    private PropagationDemo propagationDemo;
 
    @Transactional(rollbackFor = Exception.class)
    public void support(int id) throws Exception {
        // 事务运行
        propagationDemo.support(id);
    }
}

对于非事务调用,则是直接在测试类中调用(请注意下面的call方法,调用的是两个不同bean中的support方法)

private void testSupport() {
    int id = 430;
    // 非事务方式,异常不会回滚
    call("support无事务运行", id, propagationDemo::support);
 
    // 事务运行
    id = 440;
    call("support事务运行", id, propagationDemo2::support);
}

输出结果如下:

============ support无事务运行 start ========== 
support无事务运行 >>>> {id=430, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
support: after updateMoney name >>>> {id=430, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事务回滚!!!
support无事务运行 >>>> {id=430, name=更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ support无事务运行 end ========== 
 
============ support事务运行 start ========== 
support事务运行 >>>> {id=440, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
support: after updateMoney name >>>> {id=440, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事务回滚!!!
support事务运行 >>>> {id=440, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ support事务运行 end ========== 

从上面的输出,也可以得出结果:非事务执行时,不会回滚;事务执行时,回滚

3. MANDATORY

需要在一个正常的事务内执行,否则抛异常

使用姿势如下

@Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
public void mandatory(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("mandatory: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
 
    throw new Exception("事务回滚!!!");
}

这种传播属性的特点是这个方法必须在一个已有的事务中运行,所以我们的测试case也比较简单,不再事务中运行时会怎样?

private void testMandatory() {
    int id = 450;
    // 非事务方式,抛异常,这个必须在一个事务内部执行
    call("mandatory非事务运行", id, propagationDemo::mandatory);
}

输出结果

============ mandatory非事务运行 start ========== 
mandatory非事务运行 >>>> {id=450, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
No existing transaction found for transaction marked with propagation 'mandatory'
mandatory非事务运行 >>>> {id=450, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ mandatory非事务运行 end ========== 

从上面的输出可知,直接抛出了异常,并不会执行方法内的逻辑

4. NOT_SUPPORT

这个比较有意思,被它标记的方法,总是非事务地执行,如果存在活动事务,则挂起

(实在是没有想到,有什么场景需要这种传播属性)

一个简单的使用case如下:

@Transactional(propagation = Propagation.NOT_SUPPORTED, rollbackFor = Exception.class)
public void notSupport(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("notSupport: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
    throw new Exception("回滚!");
}

接下来需要好好的想一下我们的测试用例,首先是它需要在一个事务中调用,外部事物失败回滚,并不会影响上面这个方法的执行结果

我们在PropagationDemo2中,添加测试case如下

@Transactional(rollbackFor = Exception.class)
public void notSupport(int id) throws Exception {
    // 挂起当前事务,以非事务方式运行
    try {
        propagationDemo.notSupport(id);
    } catch (Exception e) {
    }
 
    propagationDemo.query("notSupportCall: ", id);
    propagationDemo.updateName(id, "外部更新");
    propagationDemo.query("notSupportCall: ", id);
    throw new Exception("回滚");
}

输出结果如下

============ notSupport start ========== 
notSupport >>>> {id=460, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
notSupport: after updateMoney name >>>> {id=460, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
notSupportCall:  >>>> {id=460, name=更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
notSupportCall:  >>>> {id=460, name=外部更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
回滚
notSupport >>>> {id=460, name=更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ notSupport end ========== 

从上面输出可以看出

  • NOT_SUPPORT 标记的方法,属于非事务运行(因为抛异常,修改没有回滚)
  • 外部事务回滚,不会影响其修改

5. NEVER

总是非事务地执行,如果存在一个活动事务,则抛出异常。

使用姿势如下

/**
 * 总是非事务地执行,如果存在一个活动事务,则抛出异常。
 *
 * @param id
 * @throws Exception
 */
@Transactional(propagation = Propagation.NEVER, rollbackFor = Exception.class)
public void never(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("notSupport: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
}

我们的测试就比较简单了,如果在事务中运行,是不是会抛异常

PropagationDemo2中,添加一个事务调用方法

@Transactional(rollbackFor = Exception.class)
public void never(int id) throws Exception {
    propagationDemo.never(id);
}

测试代码

private void testNever() {
    int id = 470;
    call("never非事务", id, propagationDemo2::never);
}

输出结果

============ never非事务 start ========== 
never非事务 >>>> {id=470, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
Existing transaction found for transaction marked with propagation 'never'
never非事务 >>>> {id=470, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ never非事务 end ==========

直接抛出了异常,并没有执行方法内的业务逻辑

6. NESTED

其主要特点如下

  • 如果不存在事务,则开启一个事务运行
  • 如果存在事务,则运行一个嵌套事务;

上面提出了一个嵌套事务的概念,什么是嵌套事务呢?

  • 一个简单的理解:外部事务回滚,内部事务也会被回滚;内部事务回滚,外部无问题,并不会回滚外部事务

接下来设计两个测试用例,一个是内部事务回滚;一个是外部事务回滚

a. case1 内部事务回滚
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void nested(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("nested: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
 
    throw new Exception("事务回滚!!!");
}

PropagationDemo2这个bean中,添加一个外部事务,捕获上面方法的异常,因此外部执行正常

@Transactional(rollbackFor = Exception.class)
public void nested(int id) throws Exception {
    propagationDemo.updateName(id, "外部事务修改");
    propagationDemo.query("nestedCall: ", id);
    try {
        propagationDemo.nested(id);
    } catch (Exception e) {
    }
}

测试代码

private void testNested() {
    int id = 480;
    call("nested事务", id, propagationDemo2::nested);
}

输出结果如下

============ nested事务 start ========== 
nested事务 >>>> {id=480, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
nestedCall:  >>>> {id=480, name=外部事务修改, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested: after updateMoney name >>>> {id=480, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested事务 >>>> {id=480, name=外部事务修改, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ nested事务 end ==========

仔细看一下上面的结果,外部事务修改的结果都被保存了,内部事务的修改被回滚了,没有影响最终的结果

b. case2 外部事务回滚
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void nested2(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("nested: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return;
        }
    }
}

PropagationDemo2这个bean中,添加一个外部事务,内部事务正常,但是外部事务抛异常,主动回滚

@Transactional(rollbackFor = Exception.class)
public void nested2(int id) throws Exception {
    // 嵌套事务,外部回滚,会同步回滚内部事务
    propagationDemo.updateName(id, "外部事务修改");
    propagationDemo.query("nestedCall: ", id);
    propagationDemo.nested2(id);
    throw new Exception("事务回滚");
}

测试代码

private void testNested() {
    int id = 490;
    call("nested事务2", id, propagationDemo2::nested2);
}

输出结果如下

============ nested事务2 start ========== 
nested事务2 >>>> {id=490, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
nestedCall:  >>>> {id=490, name=外部事务修改, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested: after updateMoney name >>>> {id=490, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事务回滚
nested事务2 >>>> {id=490, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ nested事务2 end ========== 

仔细看上面的输出,对别case1,其特别在于全部回滚了,内部事务的修改也被回滚了

7. REQUIRES_NEW

这个和上面的NESTED有点相似,但是又不一样

  • 当存在活动事务时,新创建一个事务执行
  • 当不存在活动事务时,和REQUIRES效果一致,创建一个事务执行

注意

REQUIRES_NEWNESTED相比,两个事务之间没有关系,任何一个回滚,对另外一个无影响

测试case和前面差不多,不多做细说...

8. 小结

前面介绍了7中传播属性,下面简单对比和小结一下

事务 特点
REQUIRED 默认,如果存在事务,则支持当前事务;不存在,则开启一个新事务
SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行
MANDATORY 需要在一个正常的事务内执行,否则抛异常
REQUIRES_NEW 不管存不存在事务,都开启一个新事务
NOT_SUPPORTED 不管存不存在,都以非事务方式执行,当存在事务时,挂起事务
NEVER 非事务方式执行,如果存在事务,则抛异常
NESTED 如果不存在事务,则开启一个事务运行;如果存在事务,则运行一个嵌套事务

II. 其他

0. 系列博文&源码

系列博文

源码


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
6月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
1088 5
|
12月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
337 0
|
12月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
1517 0
|
12月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
300 0
|
12月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
268 0
|
前端开发 Java 数据安全/隐私保护
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
文章通过一个简单的SpringBoot项目,详细介绍了前后端如何实现用户登录功能,包括前端登录页面的创建、后端登录逻辑的处理、使用session验证用户身份以及获取已登录用户信息的方法。
2290 2
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
|
Cloud Native Java C++
Springboot3新特性:开发第一个 GraalVM 本机应用程序(完整教程)
文章介绍如何在Spring Boot 3中利用GraalVM将Java应用程序编译成独立的本机二进制文件,从而提高启动速度、减少内存占用,并实现不依赖JVM运行。
2257 1
Springboot3新特性:开发第一个 GraalVM 本机应用程序(完整教程)
|
12月前
|
缓存 NoSQL Java
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
1150 79
|
12月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——指定项目配置文件
在实际项目中,开发环境和生产环境的配置往往不同。为简化配置切换,可通过创建 `application-dev.yml` 和 `application-pro.yml` 分别管理开发与生产环境配置,如设置不同端口(8001/8002)。在 `application.yml` 中使用 `spring.profiles.active` 指定加载的配置文件,实现环境快速切换。本节还介绍了通过配置类读取参数的方法,适用于微服务场景,提升代码可维护性。课程源码可从 [Gitee](https://gitee.com/eson15/springboot_study) 下载。
523 0
|
12月前
|
Java 微服务 Spring
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
在微服务架构中,随着业务复杂度增加,项目可能需要调用多个微服务。为避免使用`@Value`注解逐一引入配置的繁琐,可通过定义配置类(如`MicroServiceUrl`)并结合`@ConfigurationProperties`注解实现批量管理。此方法需在配置文件中设置微服务地址(如订单、用户、购物车服务),并通过`@Component`将配置类纳入Spring容器。最后,在Controller中通过`@Resource`注入配置类即可便捷使用,提升代码可维护性。
232 0

热门文章

最新文章