事务是把一组操作封装成一个执行单元(封装到一起),要么全部成功,要么全部失败。
比如: 用户A 要给 用户B 转账 100块,第一步是 用户A 扣 100 快,第二步是 用户B 加 100块,但是如果第一步执行成功了,但是第二步突然失败了,那么 A的 100块,就消失了。这搁谁都受不了,所以事务就可以来解决这样的问题,让这组操作要么一起成功,要么一起失败。
实现事务的原理 :是通过 日志 来实现的。会记录一个日志,只有事务的开始和事务的执行,但是没有事务的结束。等下一次恢复的时候,会进行日志的自检,如果发现日志只执行了一半,没执行完,就会执行补偿机制。之前扣掉的钱,现在再加回来。就是表示执行失败了。
Spring 中的事务操作分为两类:
编程式事务,也就是手动操作事务,和 MySQL 操作事务类似,也是有三个重要操作步骤:
Spring Boot 内置了两个对象,也就是涵盖了上面的这些功能:
我们需要创建一个 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 有很多参数,如下图:

MySQL 的事务隔离级别有四种:
READ UNCOMMITTED:读未提交,也叫未提交读。
a)这个隔离级别的事务可以看到其他事务中未提交的数据。
b)它的隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚。
c)因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读。
READ COMMITTED:读已提交,也叫提交读。
a)这个隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。
b)但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果(可能有老六修改了提交的数据),这种现象叫做不可重复读。
REPEATABLE READ:可重复读。
a)是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果是⼀致的。但也会有新的问题。
b)此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,在下一次 以同样的 SQL 查询的时候,突然发现多出一条记录。无缘无故 多出一个条记录,好神奇,这就叫幻读(Phantom Read)。
SERIALIZABLE:序列化。
a)它事务最⾼隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多。
用图片更直观的看:

通过 isolation 就可以实现事务的隔离级别了:

第一个是默认事务隔离级别,也就是可重复读。
要注意的是:
简单来说:虽然我们在项目中设置了隔离级别,但是!项目中的隔离级别是否生效,还是要看 连接的数据库 是否支持 项目中设置的隔离级别。
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 具体执行细节如下图:

Spring 事务传播机制定义了多个包含了事务的方法,相互调用时,事务时如何在这些方面进行传递的。
但是我们到了具体的业务方法里面,到框架里面,业务场景就会变得很复杂很复杂。就像。传播机制如下,在一条调用链之下:

并发事务如下:

在方法说明添加 @Transactional 之后,然后添加 propagation 属性就可以了:

一共有上面这七种:
上面的几种,可以划分为以下三类:

如果划分为情侣关系的话,就是这样:

我们需要创建两张表,用户表 和 日志表,这样搭配起来,才能突出事务嵌套的情况。方便我们去模拟事务嵌套的情况。我们来使用 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 的执行逻辑如下 :

对 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 报错,数据回滚。
也是不支持当前事务的一种,但这个是直接以非事务运行的,然后我们把 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 就是支持事务的嵌套,就是把 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 的添加方法出现异常之后,数据回滚了。调用流程如下: