Spring 事务
使用事务的原因
事务是把一组操作封装成一个执行单元(封装到一起),要么全部成功,要么全部失败。
比如: 用户A 要给 用户B 转账 100块,第一步是 用户A 扣 100 快,第二步是 用户B 加 100块,但是如果第一步执行成功了,但是第二步突然失败了,那么 A的 100块,就消失了。这搁谁都受不了,所以事务就可以来解决这样的问题,让这组操作要么一起成功,要么一起失败。
实现事务的原理 :是通过 日志 来实现的。会记录一个日志,只有事务的开始和事务的执行,但是没有事务的结束。等下一次恢复的时候,会进行日志的自检,如果发现日志只执行了一半,没执行完,就会执行补偿机制。之前扣掉的钱,现在再加回来。就是表示执行失败了。
Spring 中事务的实现
Spring 中的事务操作分为两类:
- 编程式事务(手动写代码操作事务)
- 声明式事务(里有注解自动开启和提交事务)
编程式事务
编程式事务,也就是手动操作事务,和 MySQL 操作事务类似,也是有三个重要操作步骤:
- 开启事务(获取事务)
- 提交事务
- 回滚事务
Spring Boot 内置了两个对象,也就是涵盖了上面的这些功能:
- DataSourceTransactionManager:⽤来获取事务(开启事务)、提交或回滚事务的。
- TransactionDefinition:是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从⽽获得⼀个事务 TransactionStatus。
我们需要创建一个 SSM 项目,然后配置一下配置文件的信息。yml 代码如下:
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8
username: root
password: sjp151
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mybatis/**Mapper.xml
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
level:
com:
example:
springtransaction: debug
设置 User 的信息:
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private String createtime;
private String updatetime;
private int state;
}
然后在 Mapper 当中添加方法:
@Mapper
public interface UserMapper {
int add(UserInfo userInfo);
}
xml 当中的 SQL 代码如下:
<insert id="add">
insert into userinfo(username,password) values (#{username}, #{password});
</insert>
回滚事务
设置 Controller 的方法:
@RequestMapping("/add")
public int add(UserInfo userInfo) throws InterruptedException {
//非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
//开启事务
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
System.out.println("add 受影响的行数:" + result);
transactionManager.rollback(transactionStatus);//回滚事务
return result;
}
先看数据库的内容:
运行结果如下:
也就是受影响的行数是 1,然后我们看数据库的内容:
并没有发生改变,也就是我们的事务回滚起效果了。
提交事务
上面的代码是回滚事务,下面我们来试试提交事务:
@RequestMapping("/add")
public int add(UserInfo userInfo) throws InterruptedException {
//非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
//开启事务
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
int result = userService.add(userInfo);
System.out.println("add 受影响的行数:" + result);
//提交事务
transactionManager.commit(transactionStatus); return result;
}
运行结果如下:
然后返回数据库查看信息:
成功添加数据信息。
声明式事务
声明式事务,也就是添加 @Transactional 注解,但是只有方法执行没有问题的时候,才会进行事务的提交操作,如果方法出现了异常,才会进行回滚操作。代码如下:
@RequestMapping("/add2")
public int add2(UserInfo userInfo) {
//非空校验
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result = 0;
try {
result = userService.add(userInfo);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("add 受影响的行数:" + result);
//程序出现异常之后,事务就会回滚
int num = 10/0;
return result;
}
访问结果如下:
直接报错,报错信息是 除0,然后来看 SQL:
说明程序是先插入,然后才报错的。查看数据库信息:
数据库并没有添加新的数据,也就是数据进行回滚了。去掉 除0 的那行,然后再运行,结果如下:
@Transactional 作用范围
@Transactional 注解也可以用来修饰类或方法:
- 修饰方法时,需要注意的是,只能应用到 public 方法上,否则不生效。
- 修饰类时,表示这个注解对类中所有的 public 方法都生效。都会自动开启和提交 / 回滚事务。
@Transactional 的参数
@Transactional 有很多参数,如下图:
- value 和 transactionManager,就是如果有多个事务管理器时,可以使用 该属性指定选择哪个事务管理器。
- propagation,就是指事务的传播级别,就是有多个事务存在嵌套的情况,那么,我多个事务之间的行为模式,就叫做事务的传播机制。
- isolation 和 事务的隔离级别,isolation 参数 是用来设置 事务的隔离级别。
MySQL 事务的隔离级别
MySQL 的事务隔离级别有四种:
-
READ UNCOMMITTED:读未提交,也叫未提交读。
a)这个隔离级别的事务可以看到其他事务中未提交的数据。
b)它的隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚。
c)因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。 -
READ COMMITTED:读已提交,也叫提交读。
a)这个隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。
b)但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果(可能有老六修改了提交的数据),这种现象叫做不可重复读。 -
REPEATABLE READ:可重复读。
a)是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果是⼀致的。但也会有新的问题。
b)此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,在下一次 以同样的 SQL 查询的时候,突然发现多出一条记录。无缘无故 多出一个条记录,好神奇,这就叫幻读(Phantom Read)。 -
SERIALIZABLE:序列化。
a)它事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。
用图片更直观的看:
使用事务
通过 isolation 就可以实现事务的隔离级别了:
第一个是默认事务隔离级别,也就是可重复读。
要注意的是:
- 当 Spring 中设置了事务隔离级别 和 连接的数据库(MySQL)事务隔离级别 发生冲突的时候,那么,会以 Spring 的为主。
- Spring 中的 事务隔离级别机制的实现,是依靠 连接的数据库中 支持的事务隔离级别 为基础。
简单来说:虽然我们在项目中设置了隔离级别,但是!项目中的隔离级别是否生效,还是要看 连接的数据库 是否支持 项目中设置的隔离级别。
timeout :事务的超时时间。
-
默认值是 -1。表示的是,没有时间限制。没有规定超过多少时间算超时。
-
如果设置了超时时间,并超过了该时间。也就是说: 事务 到了规定的时间,还没有执行完。此时,就会认为这个事务无法完成,自动回滚事务。数据库现有数据如下:
在 userService 当中休眠三秒钟。代码如下:
public int add(UserInfo userInfo) { try { Thread.sleep(3*1000); } catch (InterruptedException e) { e.printStackTrace(); } return userMapper.add(userInfo); }
然后在方法里面设置 timeout:
@Transactional(timeout = 1) @RequestMapping("/add2") public int add2(UserInfo userInfo) { //非空校验 if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) || !StringUtils.hasLength(userInfo.getPassword())) { return 0; } int result = 0; result = userService.add(userInfo); System.out.println("add 受影响的行数:" + result); //程序出现异常之后,事务就会回滚 return result; }
运行结果如下,报了 Transaction time out 错误:
readOnly :就是指定事务为只读事务,默认是 false,默认指定的事务不是 只读事务。表示该事务只能被读取。不能进行其它操作。确认只是读数据的话,就设置 read-only 为 true。
rollbackFor (类型)/ rollbackForClassName(String类型) :就是指定能够触发事务回滚的异常类型。可以指定多个异常类型。当发生某些异常的时候,能够 执行 事务回滚操作。
要注意的是 :由于我们都是使用注解来解决问题的,内部的运行对于我们程序员来说是不可见的(黑盒)。所以,有些时候,是会出现一些意外错误的。
@Transactional 在异常被捕获的情况下,就不会进⾏事务⾃动回滚。 :
- 当然也可以进行抛出异常,然后事务自动进行回滚。
- 当然也可以通过代码手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
来实现事务的回滚。
@Transactional 工作原理
- @Transactional 是基于 AOP 实现的,AOP 优势使用动态代理实现的。
- 如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
- @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
@Transactional 实现思路大致如下:
@Transactional 具体执行细节如下图:
Spring 事务传播机制
什么是事务传播机制
Spring 事务传播机制定义了多个包含了事务的方法,相互调用时,事务时如何在这些方面进行传递的。
但是我们到了具体的业务方法里面,到框架里面,业务场景就会变得很复杂很复杂。就像。传播机制如下,在一条调用链之下:
并发事务如下:
传播机制有哪些
在方法说明添加 @Transactional 之后,然后添加 propagation 属性就可以了:
一共有上面这七种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。就像结婚买房,房子(事务)是必须项。女方就是:男方买房子,就住进去,如果男方没房子,也没事,一起努力买房,但是房子必须有。
- Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。继续使用上面的例子,也就是说,男方有没有房子都无所谓,就想和他结婚。有房子,就住。没有,就租房。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。就相当于,如果男方没钱,就不结婚,甚至直接分手。有的话,女方就直接住进去。
- Propagation.REQUIRES_NEW:表示创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。就相当于男方已经买了房,但是女生表示不是新房,我不干了。
- Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。就相当于,男方有房子,女方也不住,就是要租房,
- Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。就相当于,男方有房子,就不结婚了,直接分手。如果没有房子,那我就和你结婚。
- Propagation.NESTED:如果当前存在事务,则创建⼀个事务作为当前事务的嵌套事务来运⾏。如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED(创建一个新事物)。就相当于是:男方有房,女方家里又给了一套房,如果男生没有房,女方表示也可以直接送一套。
上面的几种,可以划分为以下三类:
如果划分为情侣关系的话,就是这样:
支持当前事务(REQUIRED)
我们需要创建两张表,用户表 和 日志表,这样搭配起来,才能突出事务嵌套的情况。方便我们去模拟事务嵌套的情况。我们来使用 userinfo 表和 loginfo 表:
创建 LogInfo 的 Mapper 方法:
@Mapper
public interface LogMapper {
int add(LogInfo logInfo);
}
XML 当中的 SQL 如下:
<insert id="add">
insert into loginfo(name,`desc`) values (#{name},#{desc});
</insert>
Controller 当中进行添加方法:
@Transactional(propagation = Propagation.REQUIRED)
@RequestMapping("/add4")
public int add4(UserInfo userInfo) throws InterruptedException {
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int userResult = userService.add(userInfo);
System.out.println("添加用户:" + userResult);
LogInfo logInfo = new LogInfo();
logInfo.setName("添加用户");
logInfo.setDesc("添加用户结果:" + userResult);
int logResult = logService.add(logInfo);
return userResult;
}
然后 Service 层进行 add 方法:
@Transactional(propagation = Propagation.REQUIRED)
public int add(LogInfo logInfo) {
int num = 10/0;
return logMapper.add(logInfo);
}
这样的话,日志添加失败,事务是会全部回滚,还是只有日志回滚。两张表的数据如下:
访问结果下:
报了我们之前设置好的算术异常。日志信息如下:
说明添加用户的时候没问题,添加日志的时候出了异常。然后再次查看那两张表的数据:
数据库当中的数据没有发生改变,也就是说一个方法里面,如果发生了异常,那么所有事务都会回滚。也就是 REQUIRED 的结果。
Controller,UserService,LogService 的执行逻辑如下 :
不支持当前事务(REQUIRES_NEW)
对 Service 层都加上 REQUIRES_NEW 事务,logservice:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(LogInfo logInfo) {
int result = logMapper.add(logInfo);;
System.out.println("添加日志结果:" + result);
int num = 10/0;
return logMapper.add(logInfo);
}
userservice:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(UserInfo userInfo) throws InterruptedException {
int result = userMapper.add(userInfo);
return result;
}
运行结果如下:
还是算术异常,然后看日志:
日志当中的用户和日志都添加成功了,然后我们来看数据库数据:
发现 userinfo 数据表里面多了休息,而 loginfo 却没有,这就是我们设置了 REQUIRES_NEW 的原因。然后 loginfo 报错,数据回滚。
非事务(NOT_SUPPORTED)
也是不支持当前事务的一种,但这个是直接以非事务运行的,然后我们把 service 当中的 add 改为 NOT_SUPPORTED。
userservice:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public int add(UserInfo userInfo) throws InterruptedException {
int result = userMapper.add(userInfo);
return result;
}
logservice:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public int add(LogInfo logInfo) {
int result = logMapper.add(logInfo);;
System.out.println("添加日志结果:" + result);
int num = 10/0;
return logMapper.add(logInfo);
}
然后继续运行:
仍然是算术异常的500错误,然后查看日志:
也是添加完日志之后才报的错,我们查看数据库信息:
发现两者都添加了数据,也就是说在 NOT_SUPPORTED 事务下,是不会发生事务回滚的。
事务嵌套 NESTED
NESTED 就是支持事务的嵌套,就是把 Controller 和 Service 当中的事务都设置为 NESTED,那么此时就算是把 Service 当中的事务嵌套在 主事务中了。
controller 代码:
@Transactional(propagation = Propagation.NESTED)
@RequestMapping("/add4")
public int add4(UserInfo userInfo) throws InterruptedException {
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())
|| !StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int userResult = userService.add(userInfo);
System.out.println("添加用户:" + userResult);
LogInfo logInfo = new LogInfo();
logInfo.setName("添加用户");
logInfo.setDesc("添加用户结果:" + userResult);
int logResult = logService.add(logInfo);
return userResult;
}
userservice:
@Transactional(propagation = Propagation.NESTED)
public int add(UserInfo userInfo) throws InterruptedException {
int result = userMapper.add(userInfo);
return result;
}
logservice:
@Transactional(propagation = Propagation.NESTED)
public int add(LogInfo logInfo) {
int result = logMapper.add(logInfo);;
System.out.println("添加日志结果:" + result);
try {
int num = 10/0;
} catch (NumberFormatException e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return logMapper.add(logInfo);
}
运行结果如下:
仍然是报算术异常,看日志:
显示都添加成功了,然后看数据库:
发现 log 的添加方法出现异常之后,数据回滚了。调用流程如下:
- controller/add -> 用户添加方法 -> 日志添加方法。
- 当日志添加方法出现异常之后,嵌套事务的执行结果是:
a)用户添加不受影响,添加用户成功了
日志添加失败,因为发生异常,回滚了事务。