目录
前言
在数据库的时候我们就接触过事物这一概念,简单说事务是一组操作的集合, 是一个不可分割的操作.事务会把所有的操作作为一个整体, 一起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败,事物出现的场景一般都为,操作两个或者多个方法是,需要这些方法要么同时进行要么同时失败.例如银行卡存取钱,我通过ATM机存取钱的时候,后端的数据系统肯定是要进行同步操作的,总不能我通过ATM机取或存了钱,但是由于事故我钱存取了,但是后端数据库却没有变化,这个时候就是"事务"的登场了.
1.事务的操作
事务的操作主要分为三个步骤
1. 开启事start transaction/ begin (一组操作前开启事务)
2. 提交事务: commit (这组操作全部成功, 提交事务)
3. 回滚事务: rollback (这组操作中间任何一个操作出现异常, 回滚事务)
2.Spring对于事务的实现
Spring中的事务操作分为两类:一是编程式事务(手写代码去操作事务),二是声明式事务(通过注解来操作事务).
在进行事务讲解之前我们先创建好数据库以备后续使用
这里的两张表分别为用户表(user_info) 和 日志表(log_info).
还有就是在Spring项目中创建好对应的model和mapper
//用户表
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private Date createTime;
private Date updateTime;
}
//日志表
@Data
public class LogInfo {
private Integer id;
private String userName;
private String op;
private Date createTime;
private Date updateTime;
}
@Mapper
public interface UserInfoMapper {
@Insert("insert into user_info (user_name,password) values (#{userName},#{password})")
Integer insert (String userName,String password);
}
@Mapper
public interface LogInfoMapper {
@Insert("insert into log_info (user_name,op) values (#{userName},#{op})")
Integer insert(String userName,String op);
}
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public Integer insertUser(String userName,String password) {
return userInfoMapper.insert(userName,password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
public Integer insertLog (String userName,String op){
return logInfoMapper.insert(userName,op);
}
}
(以上都为前提准备)
2.1 编程式事务(手动版)
编程式事务主要需要使用到两个类:1.DataSourceTransactionManager 2.TransactionDefinition
SpringBoot 内置了两个对象:
1. DataSourceTransactionManager 事务管理器. 用来获取事务(开启事务), 提交或回滚事务的
2. TransactionDefinition 是事务的属性, 在获取事务的时候需要将TransactionDefinition 传递进去从而获得一个事务 TransactionStatus
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
private TransactionDefinition transactionDefinition;//配置
@Autowired
private UserService userService;
@RequestMapping("/registry")
public String registry(String userName,String password) {
//开启事务 获取这个操作最开始的状态(如果我后面发生了错误就回到刚开始的状态,就相当于没进行过这个操作)
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
//回滚事务
dataSourceTransactionManager.rollback(transactionStatus);//回滚到在开启事务时获得的状态
}
}
在方法最开始定义的transactionStatus 就相当于一个重生标记,当我后续的操作如果出现了问题就"回溯"到这个时刻,而最开始定义的dataSourceTransactionManager相当于一个启动装置用于去手动的"回滚"或者"提交"事务, 这里就先通过Postman给这一个端口发送一个"userName="张三",password = "123456""的请求看一下响应结果如何
Spring控制台这也是显示SQL语句没有问题的,那来看看数据库中user表数据有没有增加吧.
由于我上面是将事务进行了回滚,数据库中是没有储存数据的,但是是执行过这一条语句的,为什么这么说呢,当你的表中如果有自增的属性,即使"回滚"了还是触发了自增的,下一次插入数据的时候会默认的往后"退一步"的.
接下来就将事务进行提交
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
private TransactionDefinition transactionDefinition;//配置
@Autowired
private UserService userService;
@RequestMapping("/registry")
public String registry(String userName,String password) {
//开启事务 获取这个操作最开始的状态(如果我后面发生了错误就回到刚开始的状态,就相当于没进行过这个操作)
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
//回滚事务
//dataSourceTransactionManager.rollback(transactionStatus);//回滚到在开启事务时获得的状态
//提交事务
dataSourceTransactionManager.commit(transactionStatus);
return "注册成功";
}
}
回滚事务是使用rollback方法,提交事务是使用commit方法
此处的id就不是为1了,而是为2,而且观察一下控制台中当提交事务和回滚事务时 控制台输出的东西是否有差异
当提交事务成功时是会多出committing这一行的,这也就可以作为判断该事务是提交还是回滚的一个方法了.
这里模拟一下当方法中出现错误时的场景
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
int a = 10/0;
return 1;
}
}
此时会导致整体的进行"回滚"
2.2声明式事务
声明式事务主要是通过@Transactional这个注解来操作的,@Transactional如果是修饰一个方法则会自动的开启事务,提交事务,回滚事务,@Transactional触发回滚事务的条件是方法内抛出异常.
@Transactional修饰类的时候,就说明这个类下面的每一个方法都被@Transactional修饰.
下面就来看一下声明式事务在不同场景下所作出的响应
2.2.1 正常操作,没有抛出异常
//声明式事务
@RestController
@Slf4j
@RequestMapping("/trans")
public class TransController {
@Autowired
private UserService userService;
/**
* 正常操作,没有抛出异常
*/
//会自动的进行事务的创建 提交(或回滚)
@Transactional
@RequestMapping("/registry")
public String registry(String userName,String password) {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
return "注册成功";
}
}
事务是正常提交的,没有出现任何问题.
2.2.2 程序抛出异常,但不做处理
/**
* 抛出异常
* 程序报错,没有处理
*/
@Transactional
@RequestMapping("/registry2")
public String registry2(String userName,String password) {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
int a = 10/0;
return "注册成功";
}
由此可知声明式事务当被@Transactional修饰的方法中抛出遗产时,就会直接触发回滚,且请求方也会获得对应的请求失败的信息.
2.2.3 程序报错了,异常捕获
/**
* 程序报错了,异常捕获,但是不处理,重新抛出异常
*/
@Transactional
@RequestMapping("/registry4")
public String registry4(String userName,String password) {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
throw e;
}
return "注册成功";
}
PostMan接收的Body是显示注册成功的
后端控制台这块也是成功提交了事务
所以当方法内抛出异常时,有对异常进行捕获就不会触发事务的回滚
2.2.4 程序报错了,异常捕获,但是不处理,重新抛出异常
@Transactional
@RequestMapping("/registry4")
public String registry4(String userName,String password) {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
throw e;
}
return "注册成功";
}
此处将异常进行捕获之后再将异常抛出
这种方式一样会触发@Transactional的回滚机制的.
2.2.5 @Transactional手动回滚方法
@Transactional的自动回滚机制确实实现的很好,但是有些场景需要触发回滚的时候,当时却不满足 @Transactional的回滚触发机制怎么办,就例如当这个方法运行到一些结果的时候,在我的逻辑里面是错误的,需要触发回滚,此时有两种方法: 1.是抛出一个异常,达到Transactional触发回滚的条件,这种方法会有点粗暴. 2.Spring也是考虑到了这一点,提供了TransactionAspectSupport类以便我们进行回滚操作
/**
* 手动回滚
*/
@Transactional
@RequestMapping("/registry5")
public String registry5(String userName,String password) {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
//手动进行回滚 不报错
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "注册成功";
}
此处将异常捕获了之后,然后使用TransactionAspectSupport去触发回滚
此时的控制台就看上去会比较整洁一些
2.2.6 处理完一个内部的异常又出现一个新的异常
@Transactional
@RequestMapping("/registry6")
public String registry6(String userName,String password) throws IOException {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
throw new IOException();
}
return "注册成功";
}
此处先抛出一个IOException的异常
postman接收的body报错没问题,那来看看Transactional是否触发了回滚机制
此处Transactional很明显没有触发回滚机制,那我们把抛出的异常换一换看一下得到的结果是否会发生变化
@Transactional
@RequestMapping("/registry6")
public String registry6(String userName,String password) throws IOException {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
throw new NullPointerException();
}
return "注册成功";
}
postman不用看,直接去观察Spring控制台的结果
此时Transactional触发了回滚机制,导致这一现象的产生主要是Transactional中的rollbackFor在起作用.
@Transactional 默认只在遇到运行时异常和Error时才会回滚, 非运行时异常不回滚,即 Exception的子类中, 除了RuntimeException及其子类.
也就是说当我抛出一个非运行时异常的时候,Transactional是无法触发回滚的,如果想要抛出非运行异常的时候也一样触发回滚就要设置一下Transactional中的rollbackFor
@Transactional(rollbackFor = {Exception.class,Error.class})//去设置哪些异常抛出的时候需要进行回滚
@RequestMapping("/registry7")
public String registry7(String userName,String password) throws IOException {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
throw new IOException();
}
return "注册成功";
}
rollbackFor是一个集合,可以设置多个.
此时就再来试试,抛出IOException时是否会触发回滚机制了
成功触发了回滚机制
3.事务隔离级别
3.1数据库的隔离级别
在简介Spring事务隔离级别之前,先来回顾一下数据库时期学到的隔离级别
SQL 标准定义了四种隔离级别, MySQL 全都支持. 这四种隔离级别分别是:
1. 读未提交(READ UNCOMMITTED): 读未提交, 也叫未提交读. 该隔离级别的事务可以看到其他事务中
未提交的数据:
因为其他事务未提交的数据可能会发生回滚, 但是该隔离级别却可以读到, 我们把该级别读到的数据称之为脏数据, 这个问题称之为脏读.
2. 读提交(READ COMMITTED): 读已提交, 也叫提交读. 该隔离级别的事务能读取到已经提交事务的数据,
该隔离级别不会有脏读的问题.但由于在事务的执行中可以读取到其他事务提交的结果, 所以在不
同时间的相同 SQL 查询可能会得到不同的结果, 这种现象叫做不可重复读
3. 可重复读(REPEATABLE READ): 事务不会读到其他事务对已有数据的修改, 即使其他事务已提交. 也就可以确保同一事务多次查询的结果一致, 但是其他事务新插入的数据, 是可以感知到的. 这也就引发了幻读问题. 可重复读, 是 MySQL 的默认事务隔离级别.
比如此级别的事务正在执行时, 另一个事务成功的插入了某条数据, 但因为它每次查询的结果都是
一样的, 所以会导致查询不到这条数据, 自己重复插入时又失败(因为唯一约束的原因). 明明在事务
中查询不到这条信息,但自己就是插入不进去, 这个现象叫幻读.
4. 串行化(SERIALIZABLE): 序列化, 事务最高隔离级别. 它会强制事务排序, 使之不会发生冲突, 从而解决了脏读, 不可重复读和幻读问题, 但因为执行效率低, 所以真正使用的场景并不多.
3.2 Spring事务的隔离级别
Spring 中事务隔离级别有5 种:
1. Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.
2. Isolation.READ_UNCOMMITTED : 读未提交, 对应SQL标准中 READ UNCOMMITTED
3. Isolation.READ_COMMITTED : 读已提交,对应SQL标准中 READ COMMITTED
4. Isolation.REPEATABLE_READ : 可重复读, 对应SQL标准中 REPEATABLE READ
5. Isolation.SERIALIZABLE : 串行化, 对应SQL标准中 SERIALIZABLE
DEFAULT是Transactional默认的隔离级别,如果想要修改Spring事务的隔离级别可以通过Transactional中的isolation这个属性进行修改
@Transactional(rollbackFor = {Exception.class,Error.class},isolation = Isolation.DEFAULT)//去设置哪些异常抛出的时候需要进行回滚
@RequestMapping("/registry7")
public String registry7(String userName,String password) throws IOException {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
try {
int a = 10/0;
} catch (Exception e) {
log.info("程序出错");
throw new IOException();
}
return "注册成功";
}
4.Spring事务的传播机制
4.1事务传播机制的定义
事务传播机制就是: 多个事务方法存在调用关系时, 事务是如何在这些方法间进行传播的.
比如有两个方法A, B都被@Transactional 修饰, A方法调用B方法A方法运行时, 会开启一个事务. 当A调用B时, B方法本身也有事务, 此时B方法运行时, 是加入A的事务, 还是创建一个新的事务呢?
这个就涉及到了事务的传播机制.
而事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
4.2 Spring事务传播机制有哪些
@Transactional 注解支持事务传播机制的设置, 通过 propagation 属性来指定传播行为.
Spring 事务传播机制有以下 7 种:
1. Propagation.REQUIRED : 默认的事务传播级别. 如果当前存在事务, 则加入该事务. 如果当前没
有事务, 则创建一个新的事务.
2. Propagation.SUPPORTS : 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则以非事务的
方式继续运行.
3. Propagation.MANDATORY :强制性. 如果当前存在事务, 则加入该事务. 如果当前没有事务, 则
抛出异常.
4. Propagation.REQUIRES_NEW : 创建一个新的事务. 如果当前存在事务, 则把当前事务挂起. 也
就是说不管外部方法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部方法都会新开
启自己的事务, 且开启的事务相互独立, 互不干扰.
5. Propagation.NOT_SUPPORTED : 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起(不
用).
6. Propagation.NEVER : 以非事务方式运行, 如果当前存在事务, 则抛出异常.
7. Propagation.NESTED : 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行.
如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED .
4.2.1 Required
先来看一下默认情况下(REQUIRED)的表现情况
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional
public Integer insertUser(String userName,String password) {
return userInfoMapper.insert(userName,password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional
public Integer insertLog (String userName,String op){
return logInfoMapper.insert(userName,op);
}
}
@Slf4j
@RequestMapping("/proga")
@RestController
public class ProController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Transactional
@RequestMapping("/p1")
public String registry(String userName,String password) {
Integer result = userService.insertUser(userName,password);
log.info("用户插入成功,result:" + result);
Integer result2 = logService.insertLog(userName,"用户自行注册");
log.info("日志表插入成功,result:" + result2);
return "注册成功";
}
}
先看看事务中没有抛出异常的情况
此时两张表都更新了数据,那如果我在insertLog故意制造一个错误会是怎么样的.
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
int a = 10/0;
return 1;
}
}
此时两个表的数据都没有进行更新,也就是说insertLog里面出现了错误触发了回滚影响到了insertUser事务的提交了,REQUIRED:如果其中一个失败,那就整体失败.insertUser 和 insertLog 都是使用了默认的Transactional,而且都到了同一个方法中,所以它们中任何一个事务失败就会导致整体事务的失败(insertUser 和 insertLog 都集中到了一个事务中)
4.2.2 Requies_new
先来看一下介绍,Requies_new:创建一个新的事务. 如果当前存在事务, 则把当前事务挂起.从字面上的意思就是,管你有没有事务,反正我自己独立出来,不归你管,
这里就把insertUser方法的propagation给设置成Requies_new,先看一下正常情况下的情况
正常情况下Requies_new和Required的唯一区别就是,Required是将两个事务统一提交的(毕竟是将它们放到了一个集合中),Requies_new的时候insertUser和insertLog的事务是分开提交的(这里insertUser和insertLog不处于同一个集合中).
那来看一下insertLog再次出现错误是否会影响到insertUser事务的提交
返回的body报错
这里就很明显的可以看出insertUser的事务正常提交了,而insertLog的事务由于抛出了异常进行了回滚,来看看数据库那边
符合场景
方法和insertLog还是使用默认的传播机制,就将这两个(方法和insertLog)视为一个整体,这两个其中一个抛出异常都是会触发回滚的,但是insertUser的事务传播机制为required_new 相当于自己去单开了一条线路,这与上面方法加insertLog形成的集合互不影响.
如果将insertLog的隔离级别也换成required_new的话,这三者的关系就相当于三个独立的个体 都是互不影响的
4.2.3 Never
先看看Never的介绍:以非事务方式运行如果当前存在事务,则抛出异常
这里Never的意思简单点就是这个方法不能以事务的方式进行运行(不能被Transactional注解修饰),如果有的话就都别玩
变动代码:
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NEVER)
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
int a = 10/0;
return 1;
}
}
再来看看运行起来是啥情况
这里可以看见insertUser的事务(insertUser的传播机制还是为required_new)是正常提交的,接下来来研究研究报错信息
insertLog也跟就没有运行起来就别说事务的回滚了
跟事务隔离级别为Nerve的方法所在同一个方法底下的东西都不能有事务
4.2.4 Nested(嵌套事务)
照例先看一下介绍:如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行.
如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED,
此处就将insertUser和insertLog的传播机制都设置为Nested
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertUser(String userName,String password) {
return userInfoMapper.insert(userName,password);
}
}
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
int a = 10/0;
return 1;
}
}
代码正常运行(没有抛出异常)的情况就不进行演示了,不用想都知道是两个事务正常提交的,下面就来试一下insertLog中抛出异常的场景.
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
int a = 10/0;
return 1;
}
}
这里就会发现这不跟默认(Required)的情况一样吗?对则全对,错则全错.
这里还有一种情况没有是探讨,就是当我insertUser或者insertLog里面抛出异常后,如果进行解决了会在怎么样,
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.NESTED)
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
try {
int a = 10/0;
} catch (Exception e) {
log.info("insertLog中的异常已进行处理,并且回滚");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return 1;
}
}
此时就会发现user_info表中是正常添加了数据的,而log_info由于回滚没有进行数据的添加,这就是Nested的特性,Nested修饰了insertUser和insertLog 且这两个方法在被事务修饰的方法里面执行,此时就相当于一个"大公司"(主方法)旗下的两个"子公司"(insertUser和insertLog),如果这两个子公司其中一个或者全部破产了(或出现严重事故了),子公司没有制定相应的对策进行解决,则后续一定会影响到我大公司(主方法)的运作,从而会导致大公司旗下的其他小公司("其他传播机制为Nested的方法"),导致整体的崩盘.如果我出事的小公司在出现问题时就将问题进行了处理("对异常进行捕获")的话就不会影响到其他模块.而如果传播机制为Required(默认情况的话),在一样的场景即使我对异常进行了捕获还是会导致整体的回滚.
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertUser(String userName,String password) {
return userInfoMapper.insert(userName,password);
}
}
@Slf4j
@Service
public class LogService {
@Autowired
private LogInfoMapper logInfoMapper;
@Transactional(propagation = Propagation.REQUIRED)
public Integer insertLog (String userName,String op){
logInfoMapper.insert(userName,op);
//模拟失败的情况
try {
int a = 10/0;
} catch (Exception e) {
log.info("insertLog中的异常已进行处理,并且回滚");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return 1;
}
}
这就是Nested与Required的区别,在没有异常抛出和抛出异常不进行处理的时候,这两者是一样的,但如果有对异常进行捕获或者其他处理的话,此时就有着天壤之别了.
NESTED和REQUIRED区别
1.整个事务如果全部执行成功,二者的结果是一样的
2.如果事务一部分执行成功REQUIRED加入事务会导致整个事务全部回滚NESTED嵌套事务可以实现局部回滚不会影响上一个方法中执行的结果
总结:
Spring事务的传播机制的学习对了解Spring起到了一定的作用,在传播机制这块主要掌握的机制大致就是我上述的4个Required,Requies_new,Never,Nested,这四种,事务的操作的话,就尽量使用声明式的方式来,Spring方便的地方就是在于有大量的注解以便简化我们的操作和代码量,最后如果本篇文章让你学到了东西就麻烦佬给个赞吧.