【DB系列】声明式事务Transactional

文章目录
  1. I. 配置
    1. 1. 项目配置
    2. 3. 数据库
  2. II. 使用说明
    1. 1. 初始化
    2. 2. transactional
    3. 3. 测试
    4. 4. 注意事项
      1. a. 适用场景
      2. b. 异常类型
      3. c. @Transactional 注解的属性信息
  3. III. 其他
    1. 0. 系列博文&源码
    2. 1. 一灰灰Blog

当我们希望一组操作,要么都成功,要么都失败时,往往会考虑里利用事务来实现这一点;之前介绍的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,提供了事务支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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相关的信息

```properties
## 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. 数据库

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

1
2
3
4
5
6
7
8
9
10
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修改属于不可缺少的语句了,所以我们需要先初始化几个用于测试的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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的操作被回滚了

1
2
3
4
5
============ 事务正常工作 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,虽然是抛出了异常,但是并不会生效

1
2
3
4
5
6
7
8
9
10
11
@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属性来指明,触发回滚的异常类型

1
2
3
4
5
6
7
8
9
10
11
12

@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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");
}

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

1
2
3
4
5
6
7
8
9
10
11
============ 事务不生效 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 的使用

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

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

III. 其他

0. 系列博文&源码

系列博文

源码

1. 一灰灰Blog

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

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

一灰灰blog


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