27

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

 4 years ago
source link: http://spring.hhui.top/spring-blog/2020/02/02/200202-SpringBoot系列教程之事务传递属性/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

对于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. 系列博文&源码

系列博文

源码

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

ri6Vj2M.png!web

打赏 如果觉得我的文章对您有帮助,请随意打赏。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK