事务
1.事务的概念
事务我们往往说的是数据库事务,其实还有一种事务是系统事务。
1数据库事务: 数据库事务通常指对数据库进行读或写的一个操作序列。
a数据库事务可以认为是一种容错机制,提供数据库即使在异常状态下仍能保持一致性的方法。
b当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
2系统中的事务: 处理一系列业务处理的执行逻辑单元,该单元里的一系列类操作要么全部成功,要么全部失败
我们开发的的Java服务端应用说要保障事务,要保障数据一致其实是系统事务,但是系统事务的实现需要依靠数据库这个底层事务,明白了吧。
2.事务的特性
事务有四大特性,简称ACID
●原子性(Atomicity):一个事务是一系列sql的封装,这个封装不可再拆分了,这一组操作操作全部成功或者全部失败
●一致性(Consistency):数据库从一个一致性状态变到另一个一致性状态
○一系列操作后,所有的操作和更新全部提交成功,数据库只包含全部成功后的数据就是数据的一致性
○由于系统异常或数据库系统出现故障导致只有部分数据更新成功,但是这不是我们需要的最终数据,这就是数据的不一致
●隔离性(Isolation):事务互相隔离互不干扰,隔离级别可以配置的
○事务内部操作的数据对其它事务是隔离的,在一个事务执行完之前不会被其他事务影响和操作
●持久性(Durability):事务提交后数据应该被永久的保存下来,出现宕机等故障后可以恢复数据
3.事务隔离级别
ANSI SQL标准定义了4种隔离级别:分别为:READ UNCOMMITTED(未提交读)、READ COMMITTED(提交读)、REPEATABLE READ(可重复读)、SERIALIZABLE(可串行化)
READ UNCOMMITTED
在READ UNCOMMITTED级别,在事务中可以查看其他事务中还没有提交的修改。这个隔离级别会导致很多问题,从性能上来说,READ UNCOMMITTED不会比其他级别好太多,却缺乏其他级别的很多好处,除非有非常必要的理由,在实际应用中一般很少使用。读取未提交的数据,也称为脏读(dirty read)。
READ COMMITTED
大多数数据库系统的默认隔离级别是READ COMMITTED(但MySQL不是)。READ COMMITTED满足前面提到的隔离性的简单定义:一个事务可以看到其他事务在它开始之后提交的修改,但在该事务提交之前,其所做的任何修改对其他事务都是不可见的。这个级别仍然允许不可重复读(nonrepeatable read),这意味着同一事务中两次执行相同语句,可能会看到不同的数据结果。
REPEATABLE READ
REPEATABLE READ解决了READ COMMITTED级别的不可重复读问题,保证了在同一个事务中多次读取相同行数据的结果是一样的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读(phantom read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(phantom row)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。REPEATABLE READ是MySQL默认的事务隔离级别。
SERIALIZABLE
SERIALIZABLE是最高的隔离级别。该级别通过强制事务按序执行,使不同事务之间不可能产生冲突,从而解决了前面说的幻读问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中很少用到这个隔离级别,除非需要严格确保数据安全且可以接受并发性能下降的结果。
4.数据库事务支持
单体应用
所有的操作都在同一个项目中,不存在远程调用,可以在方法中调用其他方法,通过编码的方式实现事务控制
分布式事务
在分布式系统中,都是在不同的服务之间进行数据操作,如果涉及到三个服务,其中两个成功,一个失败整体来说都应该失败,需要将其他服务的数据恢复,整体来说要保障数据的一致性。
恢复数据的方式一般有两种:
●事务都直接提交,最终出错的话,再次恢复数据,可能多次操作数据库
●事务都先不提交,如果都成功之后,统一提交,出现时效性问题
整体来说分布式事务就是通过将一个个的单体事务串联起来
5.Spring事务控制
操作的过程中,大家时时刻刻去带入上边的理论,Spring实现事务有两种方式:
●编程式事务:在代码中写事务的操作,一个是提交,一个是回滚,这种方式对业务侵入较高【不推荐】
●声明式事务:在spring4之前是通过xml文件配置,在spring4之后通过注解配置比较方便【推荐使用注解】
注意:事务是保障数据准确性的,只需要在写数据的时候控制好事务,读数据因为不会对数据造成影响,不需要添加事务,操作两次或以上的写【添加、修改、删除】数据添加事务
6.Spring事务实现
Spring的声明式事务通过@Transactional注解实现,该注解可以使用到类或者方法上,它有以下几个核心属性:
●value/transactionManager:这两个属性互为别名作用一样,当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器
●propagation:指定事务传播行为,默认值为 Propagation.REQUIRED
○REQUIRED:如果当前存在事务,则加入该事务,否则新建一个事务。这是最常见的传播行为,也是默认的传播行为。
○SUPPORTS:支持当前事务,如果当前不存在事务,则以非事务方式执行。
○MANDATORY:强制要求当前存在事务,如果不存在事务,则抛出异常。(mandatory:adj.强制的)
○REQUIRES_NEW:重新开启一个新的事务,如果当前存在事务,则挂起该事务。(注意:默认的事务隔离级别——可重复读中 ,开启的新事务读不到之前挂起事务的操作,而且如果操作相同的表会导致锁表,一定要谨慎使用!)
○NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则挂起该事务。
○NEVER:以非事务方式执行操作,如果当前存在事务,则抛出异常。
○NESTED:如果当前存在事务,则嵌套事务中执行。嵌套事务是相对于外部事务而言的,它可以独立提交或回滚,但是嵌套事务的提交或回滚并不会对外部事务产生影响。如果外部事务不存在,那么 NESTED 与 REQUIRED 的效果是一样的。该传播行为只有在使用 JDBC 事务时才有效。
●isolation:事务隔离级别,默认值为 Isolation.DEFAULT
○Isolation.DEFAULT,使用底层数据库默认的隔离级别。
○Isolation.READ_UNCOMMITTED:
○Isolation.READ_COMMITTED:
○Isolation.REPEATABLE_READ:
○Isolation.SERIALIZABLE:
●timeout:事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务,如果要用的话基本上使用在大事务上。
●readOnly:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true
●rollbackFor:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
●noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
使用@Transactional正常回滚事务
@Transactional
@Override
public int updateStudent() {
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getUsername,"修改名字1");
wrapper.eq(Student::getId,1817507453815693315L);
studentMapper.update(wrapper);
// 修改第二条数据
wrapper.clear();
wrapper.set(Student::getUsername,"修改名字1");
wrapper.eq(Student::getId,1817507453815693316L);
studentMapper.update(wrapper);
// 报错
int i = 1 / 0;
return 0;
}
指定异常不回滚
/**
* 指定异常回滚:一帮都会直接指定Exception,运行时异常/非受查异常【RunntimeException】,受查异常异常
* 指定异常不回滚:不常用
*/
// @Transactional(noRollbackFor = ArithmeticException.class)
@Transactional(rollbackFor = Exception.class)
@Override
public int updateStudent2() {
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getUsername,"忽略算术异常");
wrapper.eq(Student::getId,1817507453815693315L);
studentMapper.update(wrapper);
// 修改第二条数据
wrapper.clear();
wrapper.set(Student::getUsername,"忽略算术异常");
wrapper.eq(Student::getId,1817507453815693316L);
studentMapper.update(wrapper);
// 报错
int i = 1 / 0;
return 0;
}
本类方法调用
本类调用都是this.。并不会使用代理对象,比使用代理对象的方法即使有事务配置也不会生效,调用本类方法并不是嵌套事务!
/**
* 调用本类的方法,本类的方法调用一般都是this.
* 本类调用就没有嵌套事务,调用其他类中的方法才会有嵌套事务【调用本类都是this,调用其他类使用的就是代理对象了】
* @return
*/
@Transactional
@Override
public int updateStudent3() {
// 开启事务
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getUsername,"updateStudent3");
wrapper.eq(Student::getId,1817507453815693315L);
studentMapper.update(wrapper);
// 调用method方法,this.调用的就是原方法
method();
// 提交/回滚
return 0;
}
/**
* 抽取,被复用,受不受事务控制呢?只需要考虑是否被同一个事务包裹
* 事务若生效必须是代理的方法
* @return
*/
@Transactional
public int method() {
// 开启事务
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.clear();
wrapper.set(Student::getUsername,"method");
wrapper.eq(Student::getId,1817507453815693316L);
studentMapper.update(wrapper);
// 报错
int i = 1 / 0;
// 提交事务或者回滚事务
return 0;
}
不同类调用
操作学生的时候,添加对应的日志,会存在嵌套事务,嵌套事务执行方式与事务传播行为有关
@Transactional
@Override
public int updateStudent4() {
// 开启事务
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.clear();
wrapper.set(Student::getUsername,"method");
wrapper.eq(Student::getId,1817507453815693316L);
studentMapper.update(wrapper);
// 记录日志,这边就不是this调用啦,调用的是代理对象的代理方法
operationService.insert("updateStudent4");
// 提交或回滚事务
return 0;
}
日志服务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override
public int insert(String methodName) {
// 开启事务
OperationLog operationLog = new OperationLog();
operationLog.setOperationName(methodName);
operationMapper.insert(operationLog);
int i = 1 /0 ;
// 提交或回滚
return 0;
}
7.Spring事务原理
Spring的事务管理通过AOP实现,而AOP通过代理模式生成新的代理类实现的,也就是我们添加了@Transactional注解之后在生成的代理类对应的方法中会加上事务控制,
注意注意:用不用事务Spring都会通过代理模式生成原实现类的代理类,使用事务只不过是在代理类上又添加了事务控制【提交和回滚】
8.Spring事务失效
Spring中的事务失效有12种情况,可参考文章:https://blog.csdn.net/hanjiaqian/article/details/120501741
访问权限问题
众所周知,Java 的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。如果方法不是public修饰事务就会失效,在AbstractFallbackTransactionAttributeSource.computeTransactionAttribute()
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
方法用fian修饰
方法内部调用
m2的事务失效
@Override
public int updateStudent() {
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getUsername,"修改名字1");
wrapper.eq(Student::getId,1817507453815693315L);
studentMapper.update(wrapper);
m2();
return 0;
}
@Transactional
public int m2() {
// 修改第二条数据
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getUsername,"修改名字1");
wrapper.eq(Student::getId,1817507453815693316L);
studentMapper.update(wrapper);
int i = 1 / 0;
return 0;
}
未被Spring管理
使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例
多线程
表不支持事务
未开启事务
上边介绍了Spring的事务传播行为,如果传播行为设置了NOT_SUPPORTED,那么事务就会失效,设置NEVER如果有事务就会抛出异常。
事务传播行为不正确
捕获异常
异常在程序中捕获了,事务识别不到发生了异常,就会失效
@Transactional
@Override
public int catchException() {
Student student = new Student();
student.setUsername("catchException张三");
student.setPasword("11111");
student.setStudentNo("张三-");
student.setMajorId(111L);
student.setClassId(111L);
// 插入数据
studentMapper.insert(student);
try {
int i = 1 / 0;
}catch (ArithmeticException e) {
// 捕获异常
}
return 0;
}
解决方案:不要捕获,如果非要捕获,出完之后重新抛出
@Transactional
@Override
public int catchException() {
Student student = new Student();
student.setUsername("catchException张三重抛");
student.setPasword("11111");
student.setStudentNo("张三-");
student.setMajorId(111L);
student.setClassId(111L);
// 插入数据
studentMapper.insert(student);
try {
int i = 1 / 0;
}catch (ArithmeticException e) {
// 捕获异常
log.info("抓到了算术异常======》");
// 重新抛出
throw new ArithmeticException();
}
return 0;
}
抛异常不正确
@Transactional注解默认会回滚RunntimeException,如果我们抛出的异常不属于RunntimeException就不会回滚事务,此时可以通过自定义rollBack,扩大异常范围抓取到异常回滚。
自定义异常回滚
嵌套事务
大事务
@Transactional
@Override
public int bigTran() {
List<Student> students = studentMapper.selectList(null);
// 可以将service导入,有事务就挂起
List<Major> majors = majorMapper.selectList(null);
LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(Student::getUsername,"李四").like(Student::getUsername,"张三1");
studentMapper.update(wrapper);
// 其他的操作
return 0;
}
有时候可能引发bbug,我们将查询的单独封装出来,有事务将其挂起。