前言:
大家好,我是良辰丫,这篇文章我将带领大家一起去学习Spring Boot 事务文章,我们在学习数据库的时候已经接触过事务了,来跟随我的脚步一起来瞧一下Spring Boot 事务吧.💌💌💌
🧑个人主页:良辰针不戳
📖所属专栏:javaEE进阶篇之框架学习
🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。
💦期待大家三连,关注,点赞,收藏。
💌作者能力有限,可能也会出错,欢迎大家指正。
💞愿与君为伴,共探Java汪洋大海。
目录
- 1. 回忆事务
- 1.1 事务是什么
- 1.2 数据库中的事务
- 2. Spring Boot 事务
- 2.1 简单回忆一下SSM项目交互过程
- 2.2 编程式事务
- 2.3 声明式事务
- 2.3.1 声明式事务提交
- 2.3.2 声明式事务回滚
- 2.3.3 try catch处理异常
- 3. 注解 @Transactional的参数
- 4. 注解 @Transactional的工作原理
- 4. 事务的隔离级别
- 4.1 事务的四大特性
- 4.2 设置事务隔离级别的原因
- 4.3 如何设置事务隔离级别
- 4.4 数据库的事务隔离级别
- 4.5 Spring 事务隔离级别
1. 回忆事务
1.1 事务是什么
事务
是指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部失败.举一个简单的转账例子,张三给李四转账100元,此时张三的余额减去100,李四的余额加上100,这样财富和我们的预期.- 在我们学习java后端的过程中,事务的学习是必不可少的,因为我们要做项目,我们可以这样理解事务就是处理一些项目中的逻辑问题.
1.2 数据库中的事务
在数据库中我们就接触了事务,在事务中常见的操作如下.
-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
2. Spring Boot 事务
- 今天的学习会依赖咱们的mybatis的学习,如果大家还是对之前知识有疑惑的可以看我的mybatis文章.
- 链接: MyBatis学习
2.1 简单回忆一下SSM项目交互过程
我们在MyBatis已经带大家了解了SSM项目交互过程,今天我带大家回忆一下,为什么多次说呢?当然因为是核心(重要点),明白了各个层次如何交互,才能够真正去了解SSM代码的架构.
- 客户通过浏览器进行访问页面,也就是客户通过浏览器向服务器发送请求.
- 此时是通过控制器接收请求,收到请求后,控制器调用服务层.
- 服务层收到控制器的调用请求后,服务层调用持久层.
- 持久层收到服务层的调用请求后,连接数据(也就是操作数据库,与数据库建立连接).
- 此时数据库向持久层返回数据,持久层向服务层返回数据,服务器又向控制器返回数据,控制器最终把数据返回给浏览器(也就是前端页面).
2.2 编程式事务
所谓编程式事务
,是完全通过代码来实现事务的逻辑,我们只需要简单了解.
- 我们先创一个SSM项目框架.
- 在yml配置文件中做相应的配置
# 数据库连接配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/student?characterEncoding=utf8
username: root
password: "123456"
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL日志级别
logging:
level:
com:
example:
demo: debug
- 创建实体类
package com.example.demo.entity;
import lombok.Data;
@Data
public class Stu {
private Integer id;
private String name;
private Integer age;
}
- 创建service层
package com.example.demo.service;
import com.example.demo.entity.Stu;
import com.example.demo.mapper.StuMapper;
import org.springframework.beans.factory.annotation.Autowired;
public class StuService {
@Autowired
private StuMapper stuMapper;
public Integer add(Stu stu){
return stuMapper.add(stu);
}
}
- 创建controller层次
package com.example.demo.controller;
import com.example.demo.entity.Stu;
import com.example.demo.service.StuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/stu")
public class StuController {
@Autowired
private StuService stuService;
//注入事务的对象
@Autowired
private DataSourceTransactionManager transactionManager;
//设置事务的属性
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
//1.开启事务(下面语句表示得到并开启事务)
TransactionStatus transactionStatus =
transactionManager.getTransaction(transactionDefinition);
int res = stuService.add(stu);
System.out.println(res);
//2.回滚事务
transactionManager.rollback(transactionStatus);
return res;
}
}
- 通过postman进行测试
接下来我们发现数据库中数据是空的,因为我们进行了回滚操作.
- 提交事务
接下来我们不进行回滚,写一个提交事务的代码.
package com.example.demo.controller;
import com.example.demo.entity.Stu;
import com.example.demo.service.StuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/stu")
public class StuController {
@Autowired
private StuService stuService;
//注入事务的对象(通过事务管理器拿到事务)
@Autowired
private DataSourceTransactionManager transactionManager;
//设置事务的属性(事务的定义对象,拿一个事务的时候设置相应的属性)
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/add")
public int add(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
//1.开启事务(下面语句表示得到并开启事务)
TransactionStatus transactionStatus =
transactionManager.getTransaction(transactionDefinition);
int res = stuService.add(stu);
System.out.println(res);
// //2.回滚事务
// transactionManager.rollback(transactionStatus);
//提交事务
transactionManager.commit(transactionStatus);
return res;
}
}
此时我们的数据库就有数据了.
2.3 声明式事务
- 编程式事务相当于是手动挡,那么我们的声明式事务就相当于自动挡了(进入智能时代了).
- 接下来我们需要认识我们的事务注解 @Transactional,它可以自动提交事务.
@Transactional的优点:
- 可以添加在类上或者方法上,如果添加在类上,那么该类中的所有方法都具有事务的特点.
- 在方法执行前自动开启事务,在方法执行完(在没有异常的情况下)自动提交事务;当在方法执行期间出现异常的时候就会自动回滚事务.
2.3.1 声明式事务提交
//声明式的自动提交事务
@Transactional
@RequestMapping("/add2")
public int add2(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
int res = stuService.add(stu);
return res;
}
可以看出声明式事务几行代码就帮我们搞定了需求,非常方便.
接下来我们看一下数据库,我们会发现李四的数据已经在数据库里面了.
2.3.2 声明式事务回滚
接下来我们制造一个异常,让它自动进行回滚操作.
//声明式的自动提交事务
@Transactional
@RequestMapping("/add2")
public int add2(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
int res = stuService.add(stu);
int num = 100/0;
return res;
}
此时我们的访问页面就变成了这样,因为前端感知到了异常.
显示添加成功,然后我们看数据库
这里却没有王五的信息,可见回滚成功.
到了这里,大家可能会感到是异常干扰了数据库的执行???
那么我们注释掉提交事务的注解,再次执行观察效果.
页面还会出现异常信息.
因为我们没有加事务的注解,遇到异常不会回滚
2.3.3 try catch处理异常
try catch处理异常后即使有异常也不会进行回滚,因为这样做程序感知不到异常,程序只相信自己感知的异常,如果它感知不到异常,它就认为没有异常.
@Transactional
@RequestMapping("/add2")
public int add2(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
int res = stuService.add(stu);
try {
int num = 100/0;
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
此时我们的浏览器页面并没有报异常.
然后我们来看我们的数据库信息,我们会发现并没有回滚操作.
那么我们如何让它在try-catch的处理后还能进行回滚呢?
- 将异常抛出,在catch中
@Transactional
@RequestMapping("/add2")
public int add2(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
int res = stuService.add(stu);
try {
int num = 100/0;
} catch (Exception e) {
throw e;
// e.printStackTrace();
}
return res;
}
但是这种方式并不优雅,有点暴力,浏览器页面也不美观.
- 使用代码手动回滚异常
@Transactional
@RequestMapping("/add2")
public int add2(Stu stu){
//非空校验
if (stu == null || stu.getId() ==null
|| stu.getName() == null
|| stu.getName() == null) {
return 0;
}
int res = stuService.add(stu);
try {
int num = 100/0;
} catch (Exception e) {
// throw e;
// e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return res;
}
这种方式是比较推荐的.
3. 注解 @Transactional的参数
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器后,可以使用该属性指定选择哪个事务管理器 |
transactionManager | 当配置了多个事务管理器后,可以使用该属性指定选择哪个事务管理器 |
propagation | 表示事务的传播行为,默认为Propagation.REQUIRED |
isolation | 事务的隔离级别,默认值为Isolation.DEFAULT |
timeout | 事务的超出时间,默认值为-1(没有超出时间),如果超过该时间限制但事务没有完成,那么自动回滚事务(一般不建议大家使用这个) |
readOnly | 指定事务是否为只读事务,默认值为false.为了忽略那些不需要事务的方法,不如读取数据,可以设置readOnly为true. |
rollbackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型. |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型. |
norollbackFor | 抛出指定的异常类型,不会滚事务,也可以指定多个异常类型. |
norollbackForClassName | 抛出指定的异常类型,不会滚事务,也可以指定多个异常类型. |
4. 注解 @Transactional的工作原理
- @Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
- @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
- @Transactional 实现思路如下,其中开启事务与提交事务(或者回滚事务)依赖于切面.
- @Transactional执行过程我们简单了解即可.
4. 事务的隔离级别
4.1 事务的四大特性
-
原⼦性(Atomicity,或称不可分割性):⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。
-
⼀致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。
-
持久性(Durability) : 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
-
隔离性(Isolation,⼜称独⽴性) : 数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化(Serializable)。
4.2 设置事务隔离级别的原因
- 上面的四种特性中,只有隔离性(隔离级别)是可以设置的
- 设置事务的隔离级别是⽤来保障多个并发事务执⾏更可控,更符合操预期的
- 怎么理解呢,假设有一个平台,张三申请的账号与李四申请的账号资源要进行隔离
4.3 如何设置事务隔离级别
Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进⾏设置
4.4 数据库的事务隔离级别
- READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。
- READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读。
- REPEATABLE READ:可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读(Phantom Read)。
- SERIALIZABLE:序列化,事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多.
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读已提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
- 脏读:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错误的。
- 不可重复读:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。
- 幻读:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。
4.5 Spring 事务隔离级别
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重
复读。 - Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级
别)。 - Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低
从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个
Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。
Spring 中事务隔离级别只需要设置 @Transactional ⾥的 isolation 属性即可.
@RequestMapping("/stu")
@Transactional(isolation = Isolation.SERIALIZABLE)
public Object save(User user) {
// 业务代码
}
后序 :
到了这里关于Spring Boot 事务的学习也就结束了,下一篇文章我将带领大家去学习Spring 事务传播机制.🍬🍬🍬