日升时奋斗,日落时自省
目录
1、事务
2、Spring针对事务的实现
2.1、Spring编程式事务(了解)
2.2、Spring声明事务(自动启动)
2.2.1、异常回滚
2.2.2、@Transactional作用范围
2.2.3、@Transactional相关参数
2.3、SpringBoot事务失效场景
2.3.1、try-catch事务失效
2.3.2、try-catch事务失效处理
2.3.3、其他事务失效场景
2.3.4、@Transactional工作原理
3、事务隔离级别
3.1、事务特性(ACID)
3.2、Spring中设置事务隔离级别
3.2.1、mysql事务基础
3.2.2、Spring事务隔离级别
4、Spring事务传播机制
4.1、事务传播机制概念
4.2、事务传播机制作用
4.3、事务传播机制种类
4.3.1、支持当前事务
4.3.2、不支持当前事务
4.3.3、嵌套事务
4.4、Spring事务传播机制场景
4.4.1、支持当前事务(REQUIRED)
4.4.2、支持当前事务(REQUIRED_NEW)
4.4.3、NESTED嵌套事务
4.4.4、(REQUIRED)加入和(NESTED)嵌套的区别
4.4.5、总结
首先了解一下事务基本是个什么(其实在mysql中也学习过)
事务是指在计算机系统中一系列的操作,这些操作要么全部执行成功,要么全部失败回滚。事务可以看作是一个原子性操作单元,即要么全部执行成功,要么全部失败
Spring中的事务操作分为两类:
<1>编程式事务(手动写代码操作事务,特定的语言和API来管理数据库操作的事务)
<2>声明式事务(利用注解自动开启和提交事务)
Spring手动操作事务和mysql操作事务类似,它也是有3个操作步骤:
<1>开启事务(获取事务)
<2>提交事务
<3>回滚事务
SpringBoot内置了两个对象:
DataSourceTransactionManager对象就是进行获取事务(开启事务,提交事务,回滚事务)相当于一个管理者,管理事务的操作
TransactionDefinition对象是事务的属性,在获取事务的时候需要将TransactionDefinition这个事务传递进DataSourceTransactionManager中此时获取事务就结束了
@RestController
@RequestMapping("/user")
public class UserController {
//事务操作对象引入
@Autowired
private StudentService studentService;
//编程式事务
@Autowired
private DataSourceTransactionManager transactionManager; //事务管理者
@Autowired
private TransactionDefinition transactionDefinition; //事务
//这里我们就写一个删除的事务
@RequestMapping("/del")
public int del(Integer id){
if(id==null||id<0){
return 0;
}
//定义 用来接收开启事务的
TransactionStatus transactionStatus =null;
int result=0;
try{
//开启事务
transactionStatus=transactionManager.getTransaction(transactionDefinition);
//业务操作,删除用户
result=studentService.del(id);
//业务操作 完成之后, 就可以进行提交事务 和 回滚事务
transactionManager.commit(transactionStatus);
//为什么这里加了一个 try catch 为的就是如果事务出错就进行回滚保持事务的一致性
}catch (Exception e){
//如果操作事务不是空的 那就进行回滚
if(transactionStatus!=null){
transactionManager.rollback(transactionStatus);
}
}
return result;
}
}
那接下来就看一下运行后的操作:
声明事务的操作很简单,只需要添加上@Transactional注解就可以实现了,无需手动开启事务和提交事务,进入方法时自动开启事务方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务(下面做一个演示还是以Student类进行删除操作)
注:@Transactional注解可以理解为把手动操作事务的代码进行封装,直接进行事务的业务逻辑操作就行
这里访问路由为 127.0.0.1:8080/user2/del
@RestController //返回字符串类型 同时 添加类注解
@RequestMapping("/user2") //设置访问路由
public class UserController2 {
@Autowired
private StudentService studentService;
@Transactional //执行方法前开启事务,方法正常执行结束之后提交事务,如果执行途中发现异常就进行回滚
@RequestMapping("/del")
public int del(Integer id){
if(id==null||id<=0){
return 0;
}
//进行业务逻辑 删除操作
int result= studentService.del(id);
System.out.println("删除:"+result);
//在操作中这里设置一个 异常 by zero异常 让事务进行回滚
int num=10/0;
return result;
}
}
访问路由效果(和手动效果是一样的,并且更方便,也比较常用):
那会不会回滚呢,会的,这里也演示一下回滚操作,这里就以by zero异常来演示回滚操作
@RestController //返回字符串类型 同时 添加类注解
@RequestMapping("/user2") //设置访问路由
public class UserController2 {
@Autowired
private StudentService studentService;
@Transactional //执行方法前开启事务,方法正常执行结束之后提交事务,如果执行途中发现异常就进行回滚
@RequestMapping("/del")
public int del(Integer id){
if(id==null||id<=0){
return 0;
}
//进行业务逻辑 删除操作
int result= studentService.del(id);
System.out.println("删除:"+result);
//在操作中这里设置一个 异常 by zero异常 让事务进行回滚
int num=10/0;
return result;
}
}
@Transactional可以用来修饰方法或者类
修饰方法时:需要注意只能应用到public方法上,否则不生效;如果该方法抛出异常,则事务会自动回滚
修饰类时:该注解对该类中所有的public方法都生效,表示该类的所有公共方法都需要被事务管理器所管理。如果该类中任何一个方法抛出异常,则整个类的事务都会回滚
这里可以先做一个简单的了解,下面还会针对propagation(传播行为)和isolation(隔离级别)这两个内容进行解析,其他内容看后面的作用就可以使用
@Transactional在异常被捕获的情况下,不会进行事务自动回滚
就在刚刚写的by zero异常中添加 try-catch来检测
@Transactional //执行方法前开启事务,方法正常执行结束之后提交事务,如果执行途中发现异常就进行回滚
@RequestMapping("/del")
public int del(Integer id){
if(id==null||id<=0){
return 0;
}
//进行业务逻辑 删除操作
int result= studentService.del(id);
System.out.println("删除:"+result);
//在操作中这里设置一个 异常 by zero异常 让事务进行回滚
try{
int num=10/0;
}catch (Exception e){
}
return result;
}
访问路由效果:
<1>方法一 :
抛出一个异常就它同样会进行回滚,效果和没有加try-catch是一样的,页面显示报错处理
<2>方法二:
手动回滚事务,在方法中使用
这里我们就演示手动回滚事务,因为抛出异常很简单(这里选择一种方法就可以)
try{
int num=10/0;
}catch (Exception e){
//方法一: 抛出一个异常, e记录错误信息提示
//e.getMessage();
/*
* 方法二: 手动回滚
* AOP支持 当前线程回滚
* try catch 中事务失效 我们此处演示手动回滚
* */
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
注:使用方法一 就把方法代码注释掉就行
注:因为这里数据库没有变化,也就没有再拿出来进行比对
<1>非public修饰方法加了@Transactional注解,事务是失效状态
<2>timeout是@Transactional注解的一个参数,可以设置事务超时时间,这是设置了事务的超时时间,但不是方法的限制时间,所以如果方法内部设置一个等待时间超过了事务的超时时间,此时事务也是失效状态
<3>@Transactional方法修饰内部类时也不会生效
<5>数据库不支持事务,那就没有用了(数据库mysql的InnoDB引擎出自于mysql5.6版本,并且支持事务操作)
show engines;
@Transactional是基于AOP实现的,AOP又是使用动态代理实现,默认情况下会采用JDK动态代理,如果目标对象没有实现接口,会使用CGLIB动态代理
@Transactional在执行业务之前,通过动态代理开启事务,业务操作之后再提交事务
@Transactional基本实现思路:
事务基本特性:原子性、一致性、隔离性、持久性
<1>原子性(Atomicity):事务是一个不可分割的工作单位,其中包含的所有操作要么全部成功完成,要么全部失败回滚,事务必须是不可分割的“原子”操作。
<2>一致性(Consistency):事务完成前后,系统的状态应该是一致的。在执行事务时,如果有任何错误发生,所有事务所做的修改将会被回滚到事务开始之前的状态。
<3>隔离性(Isolation):同一个时间段内,多个事务执行之间是互不干扰的,每个事务都认为自己是系统唯一的操作者。一个事务所做的修改在提交之前,对其他事务都是不可见的;级别划分包括读未提交(Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串⾏化
(Serializable)
<4>持久性(Durability):一旦事务提交成功,对数据的修改就是永久性的,即使系统崩溃,这些修改也不会丢失。实现持久性非常重要的一点是,在事务提交之前,需要将事务记录的日志写入主要存储器中。
注:当前这些都是事务的特性,Spring Boot同样就有着样的事务特性,这里如果使用声明式事务,这里的隔离性需要去设置,其他都是事务本身的特性,也没有层级划分
隔离性的好处:设置事务的隔离级别是用来保障多个并发事务执行可控,防止其他事务影响当前事务执行的一种策略
Spring中的事务隔离级别可以通过@Transactional中参数isolation属性进行设置
在了解参数设置之前可以先熟悉mysql中事务的隔离性,有助于我们对@Transactional设置参数
Spring中事务隔离级别包括以下5种(其实和mysql事务的区别不是很大,就是参数点级别格式):
<1>Isolation.DEFAULT:以连接的数据库的事务隔离级别为主
<2>Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读
<3>Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读
<4>Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级
别)
<5>Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低
注:Isolation.DEFAULT只有这个级别可能比较陌生,这是默认级别;默认的隔离级别是由底层事务管理器来决定的,底层事务管理器默认隔离级别是数据库的默认隔离级别(在spring没有设置的情况下,就是默认级别)
不同数据库有不同的默认级别,所以spring设置的默认级别根据数据库而定
这里暂时还做演示因为看不出效果,演示效果需要从事务传播机制开始
事务传播机制定义了当一个事务方法被另一个事务方法调用时,当前事务如何和被调用方法的事务关联的规则
注:官话有点绕,简单解释就是事务与事务之间的关系,是怎么联系起来的
事务的传播机制伴随着事务的隔离级别,事务隔离级别是保证多个并发事务执⾏的可控性的(稳定性的),⽽事务传播机制是保证⼀个事务在多个调⽤⽅法间的可控性的(稳定性的)
以下是多个事务同时调用数据库的问题,同时触发会影响数据库(数据产生问题)
事务传播机制解决的是一个事务在多个节点(方法)中传递的问题
Spring事务传播机制包含以下7种:
<1>Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该是何物;如果当前没有事务,则创建一个新的事务;
<2>Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行;
<3>Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
<1>Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起,实际意思就是不管当前是否有事务存在,我都要新创建一个事务,且开启的事务相互独立,互不干扰
<2>Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
<3>Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
<1>Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等价于Propagation.REQUIRED(相当于默认使用事务)
如果演示REQUIRED的效果,这里会先插入一条数据,同时在后续加入的事务中设置一个异常,观察事务是否会回滚
这里controller层,写一个操作student表的类,此时没有事务就会创建一个新的事务进行运行
@RestController
@RequestMapping("/user3")
public class UserController3 {
@Autowired
private StudentService studentService;
@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRED) //事务处理当前一定会有一个事务
public int add(String name,String email){
if(null==name||null==email
||name.equals("")||email.equals("")){
return 0;
}
Student student=new Student();
student.setName(name);
student.setQq_mail(email);
//添加一个学生
int result=studentService.add(student);
return result;
}
}
但是我们还想在加入插入一个学生的日志,所以这里有在接口处写一个日志添加,这里把添加的日志写到了Service层了(应该还是写到controller层的)
学生添加:添加一个学生信息同时也添加一个日志,学生添加事务设置为Propagation.REQUIRED,当前方法是被调用的,调用者UserController3已经创建一个事务了,这里直接加入到事务中即可(后面访问路由后展示效果,也会进行解释)
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
@Autowired
private LogService logService; //日志的对象注入
@Transactional(propagation = Propagation.REQUIRED)
public int add(Student student){
//添加 到数据库
int addStudent= studentMapper.add(student);
System.out.println("添加学生:"+addStudent);
//添加日志
Log log=new Log();
log.setMessage("添加学生信息");
//添加日志
logService.add(log);
return addStudent;
}
}
日志添加:同时设置事务为Propagation.REQUIRED,日志添加方法被学生添加方法所调用,所以事务也是已经存在了的,不需要创建事务直接添加到已有事务中
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRED)
public int add(Log log){
//添加日志到数据库
int result=logMapper.add(log);
System.out.println("添加日志结果:"+result);
//这里设置一个 报错 为了 展示事务的加入是如何加入的
int num=10/0;
return result;
}
}
这里画一个图给友友们解析以下:
支持当前事务,这里原来的代码不用改变什么,仅仅只需要改变添加学生和添加日志事务设置为支持当前事务(REQUIRED_NEW)
添加学生:(将事务传播机制设置为Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(Student student){
//添加 到数据库
int addStudent= studentMapper.add(student);
System.out.println("添加学生:"+addStudent);
//添加日志
Log log=new Log();
log.setMessage("添加学生信息");
//添加日志
logService.add(log);
return addStudent;
}
添加日志:(将事务传播机制设置为Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(Log log){
//添加日志到数据库
int result=logMapper.add(log);
System.out.println("添加日志结果:"+result);
//这里设置一个 报错 为了 展示事务的加入是如何加入的
int num=10/0;
return result;
}
预期结果是不是:学生添加应该是存到数据库的,日志添加应该回滚,访问路由后:
结果是什么数据没有添加数据,不管是学生还是日志:所示事务传播机制已经设置每个事务都是一个新建事务,大家互不影响,但是事务同样会检测到服务器错误,所以都会进行回滚,这里就需要我们自己去抛异常自己解决(手动回滚就不会影响到其他的单独的事务)
因为这里是给日志写了一个异常,所以这里就将日志添加中写一个回滚
日志添加(修改):
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(Log log){
//添加日志到数据库
int result=logMapper.add(log);
System.out.println("添加日志结果:"+result);
//手动添加回滚操作
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return result;
}
}
现在访问路由后看结果:
注:结果就是学生添加事务成功,日志添加事务失败
这里我们仅仅修改日志的事务传播机制,将机制设置为NESTED(嵌套)
其他的设置为REQUIRED即可
这里的嵌套:就是把多个事务连在一起,是一个整体,但是却有可以进行部分处理,下面进行演示
@Transactional(propagation = Propagation.NESTED)
public int add(Log log){
//添加日志到数据库
int result=logMapper.add(log);
System.out.println("添加日志结果:"+result);
//手动添加回滚操作
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return result;
}
访问路由结果:
(REQUIRED)加入:就是之多个事务加入到第一个外部事务中,让多个事务成为一个整体,如果其中一个事务出现问题,整体就会进行回滚
(NESTED)嵌套:虽然也是将多个事务放在一起成为了一个整体,但是可以进行部分事务的回滚,嵌套到外部事务,内部事务出现异常回滚后,不会影响外部事务的进行
注:嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点,嵌套事务进入之后相当于新建了一个保存点,而回滚时值回滚到当前保存点,因此之前的事务是不受影响的。
REQUIRED 给用户添加的同时 后面加入的事务也不能有问题,一旦有问题,所有操作全部 回滚:
内部事务 和 外部事务 是一体的
REQUIRES_NEW 给用户添加的同时 后面新的事务有问题,只会回滚新事务 同时也会报错:
内部事务 和 外部事务 相互影响
NESTED(嵌套) 给用户添加的同时 相比加入事务不会报错:
内部事务 和 外部事务 互不影响