1. 事务相关知识回顾
事务的四大(ACID)特性:
- 原子性(Atomicity) :要么都成功要么都失败。
- 一致性(Consistency) :AID都是数据库处理的特征,一致性是强调应用系统从一个正确的状态到另一个正确的状态,可以说AID是为了保证C。
- 隔离性(Isolation) :多个事务同时处理相同数据,事务间会互相隔离,不会互相影响。
- 持久性(Durability) :事务完成,会将结果持久化到磁盘。
对于事务来说,我们最常举的例子就是转账的案例,我们的账户有1000元,小明的账户有1000元,我们给小明转了100块钱,这是我们的钱扣了剩下900元。但是服务出现故障,小明没有收到100块钱的转账,这时我们两个人的账户就剩1900,本来两个人应该是有2000元的,转账这件事情发生之后,我们两个人金额总和发生了变化,这时就前后不一致了,这就是没有事务的情况会发生的。如果我们加上事务,当小明没有收到转账时,会事务回滚,将恢复到转账之前的状态,及我们各持有1000元,我们的总额也没有变化,这样就是一致性的体现,也是事务的目的。
数据库如何开启事务:
# 方式一
# 事务开启
START TRANSACTION;
# 事务内操作
# 事务提交
commit;
# 方式二
# 关闭事务自动提交
set autocommit=0
# 事务内操作
# 事务提交
commit;
# 查看当前自动提交状态的命令
show VARIABLES like 'autocommit';
使用JDBC 控制事务
Connection conn = DriverManager.getConnection();
try {
conn.setAutoCommit(false); //将自动提交设置为false
// 执行CRUD操作
conn.commit(); //当两个操作成功后手动提交
} catch (Exception e) {
conn.rollback(); //一旦其中一个操作出错都将回滚,所有操作都不成功
e.printStackTrace();
} finally {
conn.colse();
}
2. Spring中事务的管理方式
Spring支持编程式事务管理以及声明式事务管理两种方式,一般情况下,我们都是使用声明式事务。
2.1 编程式事务管理
编程式事务管理是侵入性事务管理,通过使用TransactionTemplate或者使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。这种事务方式对代码侵入性大,与业务紧紧耦合在一起,容易出错,但是可控性强,能够更加细粒度的管理事务。
2.2 声明式事务管理
声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,通过代理,在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。声明式事务的最大劣势就是它对事务的粒度最小是控制在方法级别的,而编程式事务的粒度能够到代码块。
3. Spring中的事务管理
Spring事务的本质其实就是数据库对事务的支持。
Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器。
从这个类图上我们就能分析出Spring的事务管理器是如何设计实现的。
4. 事务管理器的五大设置条件
事务管理器会依据我们应用中设置的事务隔离级别,事务传播机制,超时,是否只读,回滚机制等五大条件进行事务的管理,他们相当于事务管理的入参。
4.1 Spring中的事务隔离级别
事务隔离级别是因为并发事务引起的,对应事务的隔离性,也就是说事务的隔离性是有强弱之分的,我们可以自己去设置。
事务并发可能引起的问题:
- 脏读(Dirty reads) 脏读发生在一个事务读取了另一个事务修改但尚未提交的数据时。如果修改在稍后被回滚了,那么第一个事务获取的数据就是无效的。
- 不可重复读(Nonrepeatable read) 不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。
- 幻读(Phantom read) 幻读与不可重复读类似。它发生在事务T1读取了几行数据,接着另一个并发事务T2插入了一些数据时。在随后的查询中,事务T1就会发现多了一些原本不存在的记录,就很梦幻。
Spring 支持的隔离级别:
- ISOLATION_DEFAULT 使用数据库默认的隔离级别
- ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生,MySQL默认的隔离级别。
- ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是效率最低的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的,一般我们不会使用如此高的隔离级别。
4.2 Spring中的事务传播机制
我们就是通过Spring中的事务传播机制来控制,当前事务的范围,及哪些方法在事务的范围之内,
- PROPAGATION_REQUIRED 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会开启一个新的事务
- PROPAGATION_SUPPORTS 表示当前方法不需要开启事务,但是如果当前存在事务的话,那么该方法会在这个事务中运行
- PROPAGATION_MANDATORY 表示该方法强制在事务中运行,如果当前事务不存在,则会抛出一个异常
- PROPAGATION_REQUIRED_NEW 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
- PROPAGATION_NOT_SUPPORTED 表示该方法不需要运行在事务中。如果存在当前事务,则在该方法执行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
- PROPAGATION_NEVER 表示当前方法不能运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
- PROPAGATION_NESTED 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务
4.3 Sping中的事务超时
事务的超时设置是为了解决什么问题呢?
在数据库中,如果一个事务长时间执行,这样的事务会占用不必要的数据库资源,还可能会锁定数据库的部分资源,这样在生产环境是非常危险的。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
事务超时时间的设置
由于超时时间在一个事务开启的时候创建的,因此,只有对于那些具有启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED),声明事务超时才有意义。
4.4 Spring中的事务只读
如果一个事务只对数据库进行读操作,数据库可以利用事务的只读特性来进行一些特定的优化。我们可以通过将事务声明为只读,让数据库对我们的事务操作进行优化。
4.5 Spring中的事务回滚规则
回滚规则,就是程序发生了什么会造成回滚,这里我们可以进行设置RuntimeException或者Error。
默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚
我们可以声明事务在遇到特定的异常进行回滚。同样,我们也可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。
5. @Transactional 注解式事务声明方式原理介绍
@Transactional 使我们最长用的一种声明事务的方式,所以本篇文章我们只讲这种方式
5.1 @Transactional 注解使用
@Transactional 可以用在方法上也可以用在类上
- 事务的传播性: @Transactional(propagation=Propagation.REQUIRED)
- 事务的隔离级别: @Transactional(isolation = Isolation.READ_UNCOMMITTED)
- 只读: @Transactional(readOnly=true)
true表示只读,false则表示可读写,默认值为false。
- 事务的超时性: @Transactional(timeout=30)
- 回滚: 指定单一异常类:@Transactional(rollbackFor=RuntimeException.class) 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
5.2 @Transactional 原理
注解式声明事务是基于AOP实现的,AOP又是基于动态代理实现的,不熟悉动态代理的同学,需要先了解一下动态代理,本文就不过多讲解动态代理的内容了。
- AOP 会扫描到 @Transactional 注解,获取注解信息,创建代理对象
- 根据注解信息,AOP 会在开启事务
- 执行AOP的前置增强
- 调用目标对象的目标方法
- 获取目标方法的返回
- 执行AOP的后置增强
- 提交事务,或者触发回滚
正是由于Spring的这种实现方式,就引出了事务失效的场景。
5.3 事务失效的场景
八大事务失效场景是非常重要的,我们在开发中要极力避免,容易出现的已加粗。
- 数据库引擎不支持事务(Spring的事务本质上是数据库事务)
- 没有被Sping管理(Spring只会处理Spring中的Bean,不会处理没有注册的Bean)
- 方法不是public (代理模式的原因,开启AspectJ可以试试)
- 自身调用(因为自身调用没有经过Spring的代理类,所以事务是不生效的)
- 数据源没有配置事务管理器
- 事务传播形式定义了不支持事务
- 异常被try catch掉,也就是没有异常,没有异常就无法触发正常的回滚
- 异常类型,默认是RuntimeException,如果抛出了其他异常也是无法正常回滚的