事务定义: 将⼀组操作封装成⼀个执⾏单元(封装到⼀起),要么全部成功,要么全部失败。
为什么要用事务?
⽐如转账分为两个操作:
第⼀步操作:A 账户 -100 元。
第⼆步操作:B 账户 +100 元。
如果没有事务,第⼀步执⾏成功了,第⼆步执⾏失败了,那么 A 账户平⽩⽆故的 100 元就“⼈间蒸
发”了。⽽如果使⽤事务就可以解决这个问题,让这⼀组操作要么⼀起成功,要么⼀起失败。
Spring 中的事务操作分为两类:
在开始讲解它们之前,咱们先来回顾事务在 MySQL 中是如何使⽤的❓🤔
事务在 MySQL 有 3 个重要的操作:开启事务、提交事务、回滚事务
,它们对应的操作命令如下:
-- 开启事务
start transaction;
-- 业务执⾏
-- 提交事务
commit;
-- 回滚事务
rollback;
此方式包含了 3 个重要的操作:
依赖 两个重要的对象:
DataSourceTransactionManager
:⽤来获取事务(开启事务)、提交或回滚事务的TransactionDefinition
:是事务的属性,在获取事务的时候需要将TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus实现代码如下:
application.yml
# 配置数据库连接
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username: root
password: 666666
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:
demo: debug
UserMapper
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
/**
* @date 2022/8/17 7:48
*/
@Mapper
public interface UserMapper {
int add(UserInfo userInfo);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<insert id="add">
insert into userinfo(username,password) values(#{username},#{password})
</insert>
</mapper>
UserInfo
package com.example.demo.model;
import lombok.Data;
/**
* @date 2022/8/17 7:48
*/
@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;
}
UserService
package com.example.demo.service;
import com.example.demo.mapper.UserMapper;
import com.example.demo.model.UserInfo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @date 2022/8/17 7:57
*/
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public int add(UserInfo userInfo){
return userMapper.add(userInfo);
}
}
UserController
package com.example.demo.controller;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
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;
/**
* @date 2022/8/17 7:56
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
// JDBC 事务管理器
@Autowired
private DataSourceTransactionManager transactionManager;
// 定义事务属性
@Autowired
private TransactionDefinition transactionDefinition;
// 在此方法中使用编程式的事务
@RequestMapping("/add")
public int add(UserInfo userInfo){
// 非空校验—验证用户名和密码不能为空
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;
}
}
测试结果如下:
而我们数据库中并没有添加进数据(实现了事务的回滚):
要想提交的话,执行下列代码:
测试结果如下:
声明式事务的实现很简单,只需要在需要的⽅法上添加
@Transactional
注解就可以实现了,⽆需⼿动开启事务和提交事务,进⼊⽅法时⾃动开启事务,⽅法执⾏完会⾃动提交事务,如果中途发⽣了没有处理的异常会⾃动回滚事务。
具体实现代码如下:
// 使用声明式事务
@Transactional // 在进入方法之前,自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,则自动回滚事务
@RequestMapping("/add1")
public int add1(UserInfo userInfo){
// 非空校验—验证用户名和密码不能为空
if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||
!StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result= userService.add(userInfo);
System.out.println("add 受影响的行数: "+result);
int n=10/0;
return result;
}
测试结果如下:
显示添加成功了:
但是数据库中并没有添加数据,这是因为代码中有报错,执行了自动回滚事务
没有异常后就可以真正的把数据添加进来了:
@Transactional 可以⽤来修饰⽅法
或类
:
修饰⽅法时
:需要注意只能应⽤到 public
⽅法上,否则不⽣效。修饰类时
:表明该注解对该类中所有的 public ⽅法
都⽣效(都会自动的开启和提交(回滚)事务)。参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时, 可以使用该属性指定选择哪个事务管理器 |
transactionManager | 当配置了多个事务管理器时, 可以使用该属性指定选择哪个事务管理器 |
propagation | 事务的传播行为. 默认为 Propagation.REQUIRED |
isolation | 事务的隔离级别. 默认为 Isolation.DAEFAULT |
timeout | 事务的超时事件. 默认值为-1, 如果超过该时间限制但事务还没有完成, 则自动回滚事务. |
readOnly | 指定事务是否只读事务. 默认为 false. 为了忽略那些不需要事务的方法, 比如读取数据, 可以设置为 read-only 为 true |
rollbackFor | 用于指定能够触发事务回滚的异常类型, 可以指定多个异常类型 |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型, 可以指定多个异常类型 |
noRollbackFor | 抛出指定的异常类型, 不回滚事务, 也可以指定多个异常类型 |
noRollbackForClassName | 抛出指定的异常类型, 不回滚事务, 也可以指定多个异常类型 |
@Transactional 在异常被捕获的情况下,不会进⾏事务⾃动回滚
验证以下代码是否会发⽣事务回滚:
@Transactional() // 在进入方法之前,自动开启事务,在方法执行完之后,自动提交事务,如果出现异常,则自动回滚事务
@RequestMapping("/add2")
public int add2(UserInfo userInfo){
// 非空校验—验证用户名和密码不能为空
if(userInfo==null || !StringUtils.hasLength(userInfo.getUsername())||
!StringUtils.hasLength(userInfo.getPassword())) {
return 0;
}
int result= userService.add(userInfo);
System.out.println("add 受影响的行数: "+result);
try {
int n=10/0;
}catch (Exception e){
}
return result;
}
测试结果如下:
并没有进行事务的回滚⬇️
解决⽅案1
: 对于捕获的异常,事务是会⾃动回滚的,因此解决⽅案1就是可以将异常重新抛出
,具体实现如下:
测试结果如下:
可以发现数据并没有插入到数据库中:
解决⽅案2
: ⼿动回滚事务
,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus()
可以得到当前的事务,然后设置回滚⽅法 setRollbackOnly
就可以实现回滚了,具体实现代码如下:
测试结果如下:
数据也没有插入到数据库中:
@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。
@Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途遇到的异常,则回滚事务。
@Transactional 实现思路预览:
@Transactional 具体执⾏细节如下图所示:
事务有4 ⼤特性(ACID):原⼦性、持久性、⼀致性和隔离性
,具体概念如下:
原⼦性
: ⼀个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执⾏过程中发⽣错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执⾏过⼀样。⼀致性
: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写⼊的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以⾃发性地完成预定的⼯作。持久性
: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。隔离性
: 数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可以防⽌多个事务并发执⾏时由于交叉执⾏⽽导致数据的不⼀致。事务隔离分为不同级别,包括读未提交(Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化
上⾯ 4 个属性,可以简称为ACID。
原⼦性(Atomicity,或称不可分割性)
⼀致性(Consistency)
隔离性(Isolation,⼜称独⽴性)
持久性(Durability)
⽽这 4 种特性中,只有隔离性(隔离级别)
是可以设置的。
Spring 中事务隔离级别可以通过 @Transactional
中的 isolation
属性进⾏设置,具体操作如下图所示:
READ UNCOMMITTED
:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,⽽未提交的数据可能会发⽣回滚,因此我们把该级别读取到的数据称之为脏数据
,把这个问题称之为脏读
。READ COMMITTED
:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执⾏中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读
。REPEATABLE READ
:可重复读,是 MySQL 的默认事务隔离级别,它能确保同⼀事务多次查询的结果⼀致。但也会有新的问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因)。明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这就叫幻读(Phantom Read)
。SERIALIZABLE
:序列化,事务最高隔离级别,它会强制事务排序,使之不会发⽣冲突,从⽽解决了脏读、不可重复读和幻读问题,但因为执⾏效率低,所以真正使⽤的场景并不多脏读
:⼀个事务读取到了另⼀个事务修改的数据之后,后⼀个事务⼜进⾏了回滚操作,从⽽导致第⼀个事务读取的数据是错的。不可重复读
:⼀个事务两次查询得到的结果不同,因为在两次查询中间,有另⼀个事务把数据修改了。幻读
:⼀个事务两次查询中得到的结果集不同,因为在两次查询中另⼀个事务有新增了⼀部分数据。Isolation.DEFAULT
:以连接的数据库的事务隔离级别为主。Isolation.READ_UNCOMMITTED
:读未提交,可以读取到未提交的事务,存在脏读。Isolation.READ_COMMITTED
:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。Isolation.REPEATABLE_READ
:可重复读,解决了不可重复读,但存在幻读(MySQL默认级别)。Isolation.SERIALIZABLE
:串⾏化,可以解决所有并发问题,但性能太低。从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个Isolation.DEFAULT
(以数据库的全局事务隔离级别为主)
注意事项:
Spring 的为准
连接数据库支持事务隔离级别
为基础Spring 事务传播机制定义了多个包含了事务的⽅法,相互调⽤时,事务是如何在这些⽅法间进⾏传递
的。
事务隔离级别是保证多个并发事务执⾏的可控性的
(稳定性的),⽽事务传播机制是保证⼀个事务在多个调⽤⽅法间的可控性的
(稳定性的)。
事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题,如下图所示:
⽽事务传播机制解决的是⼀个事务在多个节点(⽅法)中传递的问题,如下图所示:
Spring 事务的传播机制包含以下 7 种 :
Propagation.REQUIRED
: 默认的事务传播级别, 它表示如果当前存在事务, 则加入该事务; 如果当前没有事务, 则创建一个新的事务。Propagation.SUPPORTS
: 如果当前存在事务,则加入该事务; 如果当前没有事务, 则以非事务的方式继续运行。Propagation.MANDATORY
: 如果当前存在事务, 则加入该事务; 如果当前没有事务, 则抛出异常。Propagation.REQUIRES_NEW
: 表示创建一个新的事务, 如果当前存在事务, 则把当前事务挂起。Propagation.NOT_SUPPORTED
: 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起。Propagation.NEVER
:以非事务方式运行, 如果当前存在事务, 则抛出异常。Propagation.NESTED
: 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行; 如果不存在事务,则创建一个新的事务。以上 7 种传播⾏为,可以根据是否⽀持当前事务分为以下 3 类:
以情侣关系为例来理解以上分类:
以下代码实现中,先开启事务先成功插⼊⼀条⽤户数据,然后再执⾏⽇志报错,⽽在⽇志报错时发⽣了异常,观察 propagation = Propagation.REQUIRED
的执⾏结果。
LogService
:
package com.example.demo.service;
import com.example.demo.mapper.LogMapper;
import com.example.demo.model.LogInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* @date 2022/8/17 23:40
*/
@Service
public class LogService {
@Resource
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(LogInfo logInfo){
int n=10/0; // 异常
return logMapper.add(logInfo);
}
}
UserController
:
程序运行前的数据库数据如下:
测试结果如下:
虽然日志中显示添加用户成功了,但是整体都进行了回滚:
UserController 类中的代码不变,将添加⽤户和添加⽇志的⽅法修改为 REQUIRES_NEW 不⽀持当前事务,重新创建事务,观察执⾏结果:
测试:
程序执⾏结果:userinfo 表中成功插⼊了数据,loginfo 表执⾏失败,但没影响 UserController 中的事务。
测试:
程序执⾏结果:全部添加成功
方法调用流程:controller/add -> 用户添加方法 -> 日志添加方法
测试:
当日志添加方法出现异常之后,嵌套事务的执行结果是:
方法流程:controller/add -> 用户添加方法 -> 日志添加方法
测试:
当日志添加方法出现异常之后,加入事务的执行结果是: