18

Spring Boot 系列教程之声明式事务 Transactional

 4 years ago
source link: https://mp.weixin.qq.com/s/oS2TwRVwH4lPBfR8YK4vCQ
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.
jyyq6vE.jpg!web

200119-SpringBoot 系列教程之声明式事务 Transactional

当我们希望一组操作,要么都成功,要么都失败时,往往会考虑利用事务来实现这一点;之前介绍的 db 操作,主要在于单表的 CURD,本文将介绍声明式事务 @Transactional 的使用姿势

I. 配置

本篇主要介绍的是 jdbcTemplate 配合事务注解 @Transactional 的使用姿势,至于 JPA,mybatis 在实际的使用区别上,并不大,后面会单独说明

创建一个 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. 使用说明

1. 初始化

为了体现事务的特点,在不考虑 DDL 的场景下,DML 中的增加,删除 or 修改属于不可缺少的语句了,所以我们需要先初始化几个用于测试的数据

@Service
public class SimpleDemo {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        String sql = "replace into money (id, name, money) values (120, '初始化', 200)," +
                "(130, '初始化', 200)," +
                "(140, '初始化', 200)," +
                "(150, '初始化', 200)";
        jdbcTemplate.execute(sql);
    }
}

我们使用 replace into 语句来初始化数据,每次 bean 创建之后都会执行,确保每次执行后面你的操作时,初始数据都一样

2. transactional

这个注解可以放在类上,也可以放在方法上;如果是标注在类上,则这个类的所有公共方法,都支持事务;

如果类和方法上都有,则方法上的注解相关配置,覆盖类上的注解

下面是一个简单的事务测试 case

private boolean updateName(int id) {
    String sql = "update money set `name`='更新' where id=" + id;
    jdbcTemplate.execute(sql);
    return true;
}

public void query(String tag, int id) {
    String sql = "select * from money where id=" + id;
    Map map = jdbcTemplate.queryForMap(sql);
    System.out.println(tag + " >>>> " + map);
}

private boolean updateMoney(int id) {
    String sql = "update money set `money`= `money` + 10 where id=" + id;
    jdbcTemplate.execute(sql);
    return false;
}

/**
 * 运行异常导致回滚
 *
 * @return
 */
@Transactional
public boolean testRuntimeExceptionTrans(int id) {
    if (this.updateName(id)) {
        this.query("after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    throw new RuntimeException("更新失败,回滚!");
}

在我们需要开启事务的公共方法上添加注解 @Transactional ,表明这个方法的正确调用姿势下,如果方法内部执行抛出运行异常,会出现事务回滚

注意上面的说法,正确的调用姿势,事务才会生效;换而言之,某些 case 下,不会生效

3. 测试

接下来,测试一下上面的方法事务是否生效,我们新建一个 Bean

@Component
public class TransactionalSample {
    @Autowired
    private SimpleDemo simpleService;

    public void testSimpleCase() {
        System.out.println("============ 事务正常工作 start ========== ");
        simpleService.query("transaction before", 130);
        try {
            // 事务可以正常工作
            simpleService.testRuntimeExceptionTrans(130);
        } catch (Exception e) {
        }
        simpleService.query("transaction end", 130);
        System.out.println("============ 事务正常工作 end ========== \n");
    }
}

在上面的调用中,打印了修改之前的数据和修改之后的数据,如果事务正常工作,那么这两次输出应该是一致的

实际输出结果如下,验证了事务生效,中间的修改 name 的操作被回滚了

============ 事务正常工作 start ==========
transaction before >>>> {id=130, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
after updateMoney name >>>> {id=130, name=更新, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
transaction end >>>> {id=130, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
============ 事务正常工作 end ==========

4. 注意事项

a. 适用场景

在使用注解 @Transactional 声明式事务时,其主要是借助 AOP,通过代理来封装事务的逻辑,所以 aop 不生效的场景,也适用于这个事务注解不生效的场景

简单来讲,下面几种 case,注解不生效

  • private 方法上装饰 @Transactional ,不生效
  • 内部调用,不生效

    • 举例如: 外部调用服务 A 的普通方法 m,而这个方法 m,调用本类中的声明有事务注解的方法 m2, 正常场景下,事务不生效

b. 异常类型

此外,注解 @Transactional 默认只针对运行时异常生效,如下面这种 case,虽然是抛出了异常,但是并不会生效

@Transactional
public boolean testNormalException(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    throw new Exception("声明异常");
}

如果需要它生效,可以借助 rollbackFor 属性来指明,触发回滚的异常类型

@Transactional(rollbackFor = Exception.class)
public boolean testSpecialException(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    throw new IllegalArgumentException("参数异常");
}

测试一下上面的两种 case

public void testSimpleCase() {
    System.out.println("============ 事务不生效 start ========== ");
    simpleService.query("transaction before", 140);
    try {
        // 因为抛出的是非运行异常,不会回滚
        simpleService.testNormalException(140);
    } catch (Exception e) {
    }
    simpleService.query("transaction end", 140);
    System.out.println("============ 事务不生效 end ========== \n");


    System.out.println("============ 事务生效 start ========== ");
    simpleService.query("transaction before", 150);
    try {
        // 注解中,指定所有异常都回滚
        simpleService.testSpecialException(150);
    } catch (Exception e) {
    }
    simpleService.query("transaction end", 150);
    System.out.println("============ 事务生效 end ========== \n");
}

输出结果如下,正好验证了上面提出的内容

============ 事务不生效 start ==========
transaction before >>>> {id=140, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
after updateMoney name >>>> {id=140, name=更新, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
transaction end >>>> {id=140, name=更新, money=210, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
============ 事务不生效 end ==========

============ 事务生效 start ==========
transaction before >>>> {id=150, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
after updateMoney name >>>> {id=150, name=更新, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
transaction end >>>> {id=150, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
============ 事务生效 end ==========

c. @Transactional 注解的属性信息

上面的内容,都属于比较基本的知识点,足以满足我们一般的业务需求,如果需要进阶的话,有必要了解一下属性信息

以下内容来自: 透彻的掌握 Spring 中@transactional 的使用 [1]

属性名 说明 name 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。 propagation 事务的传播行为,默认值为 REQUIRED。 isolation 事务的隔离度,默认值采用 DEFAULT。 timeout 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 read-only 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 rollback-for 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。 no-rollback- for 抛出 no-rollback-for 指定的异常类型,不回滚事务。

关于上面几个属性的使用实例,以及哪些情况下,会导致声明式事务不生效,会新开坑进行说明,敬请期待。。。

II. 其他

0. 系列博文&源码

系列博文

源码

  • 工程: https://github.com/liuyueyi/spring-boot-demo [7]

  • 实例源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction [8]

1. 一灰灰 Blog

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

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

  • 一灰灰 Blog 个人博客 https://blog.hhui.top [9]

  • 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top [10]

YfIZNnM.jpg!web 一灰灰blog

参考资料

[1]

透彻的掌握 Spring 中@transactional 的使用: https://www.cnblogs.com/xd502djj/p/10940627.html

[2]

180926-SpringBoot高级篇DB之基本使用: http://spring.hhui.top/spring-blog/2018/09/26/180926-SpringBoot%E9%AB%98%E7%BA%A7%E7%AF%87DB%E4%B9%8B%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/

[3]

190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解: http://spring.hhui.blog/spring-blog/2019/04/07/190407-SpringBoot%E9%AB%98%E7%BA%A7%E7%AF%87JdbcTemplate%E4%B9%8B%E6%95%B0%E6%8D%AE%E6%8F%92%E5%85%A5%E4%BD%BF%E7%94%A8%E5%A7%BF%E5%8A%BF%E8%AF%A6%E8%A7%A3/

[4]

190412-SpringBoot高级篇JdbcTemplate之数据查询上篇: http://spring.hhui.top/spring-blog/2019/04/12/190412-SpringBoot%E9%AB%98%E7%BA%A7%E7%AF%87JdbcTemplate%E4%B9%8B%E6%95%B0%E6%8D%AE%E6%9F%A5%E8%AF%A2%E4%B8%8A%E7%AF%87/

[5]

190417-SpringBoot高级篇JdbcTemplate之数据查询下篇: http://spring.hhui.top/spring-blog/2019/04/17/190417-SpringBoot%E9%AB%98%E7%BA%A7%E7%AF%87JdbcTemplate%E4%B9%8B%E6%95%B0%E6%8D%AE%E6%9F%A5%E8%AF%A2%E4%B8%8B%E7%AF%87/

[6]

190418-SpringBoot高级篇JdbcTemplate之数据更新与删除: http://spring.hhui.top/spring-blog/2019/04/18/190418-SpringBoot%E9%AB%98%E7%BA%A7%E7%AF%87JdbcTemplate%E4%B9%8B%E6%95%B0%E6%8D%AE%E6%9B%B4%E6%96%B0%E4%B8%8E%E5%88%A0%E9%99%A4/

[7]

https://github.com/liuyueyi/spring-boot-demo: https://github.com/liuyueyi/spring-boot-demo

[8]

https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction

[9]

https://blog.hhui.top: https://blog.hhui.top

[10]

http://spring.hhui.top: http://spring.hhui.top


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK