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

栏目: IT技术 · 发布时间: 4年前

内容简介:200119-SpringBoot 系列教程之声明式事务 Transactional当我们希望一组操作,要么都成功,要么都失败时,往往会考虑利用事务来实现这一点;之前介绍的 db 操作,主要在于单表的 CURD,本文将介绍声明式事务本篇主要介绍的是
Spring Boot 系列教程之声明式事务 Transactional

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]

Spring Boot 系列教程之声明式事务 Transactional
一灰灰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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

思考,快与慢

思考,快与慢

[美] 丹尼尔·卡尼曼 / 胡晓姣、李爱民、何梦莹 / 中信出版社 / 2012-7 / 69.00元

《纽约时报》2011年度十大好书 新书上市,连续20多周蝉联亚马逊、《纽约时报》畅销书排行榜前20名,上市至今超过7个月,横扫全球各大畅销书排行榜,稳居亚马逊总榜前50名 《经济学人》、《华尔街日报》、《卫报》、《纽约时报》、《金融时报》、《商业周刊》、《华盛顿邮报》、等国外权威媒体,《三联生活周刊》、《商学院》、《东方早报》等国内知名媒体争相报道,国内外读者好评如潮 人类究竟有......一起来看看 《思考,快与慢》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具