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. 系列博文 & 源码
系列博文
180926-SpringBoot 高级篇 DB 之基本使用 [2]
190407-SpringBoot 高级篇 JdbcTemplate 之数据插入使用姿势详解 [3]
190412-SpringBoot 高级篇 JdbcTemplate 之数据查询上篇 [4]
190417-SpringBoot 高级篇 JdbcTemplate 之数据查询下篇 [5]
190418-SpringBoot 高级篇 JdbcTemplate 之数据更新与删除 [6]
源码
工程: 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]
一灰灰 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高级篇DB之基本使用/
[3]
190407-SpringBoot 高级篇 JdbcTemplate 之数据插入使用姿势详解: http://spring.hhui.blog/spring-blog/2019/04/07/190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解/
[4]
190412-SpringBoot 高级篇 JdbcTemplate 之数据查询上篇: http://spring.hhui.top/spring-blog/2019/04/12/190412-SpringBoot高级篇JdbcTemplate之数据查询上篇/
[5]
190417-SpringBoot 高级篇 JdbcTemplate 之数据查询下篇: http://spring.hhui.top/spring-blog/2019/04/17/190417-SpringBoot高级篇JdbcTemplate之数据查询下篇/
[6]
190418-SpringBoot 高级篇 JdbcTemplate 之数据更新与删除: http://spring.hhui.top/spring-blog/2019/04/18/190418-SpringBoot高级篇JdbcTemplate之数据更新与删除/
- [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
来源: http://www.tuicool.com/articles/ziiaInz