Spring事务
文章目录
0.事物的概念
事务:
事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。
事务特性ACID:
原子性(atomicity),一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
一致性(consistency),数据库总是从一个一致性的状态转换到另外一个一致性的状态。
隔离性(isolation),通常来说,一个事务所做的修改在最终提交之前,对其他事务是不可见的。
持久性(durability),一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。
4种隔离级别:
READ UNCOMMITTED(未提交读),事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读。很少使用。
READ COMMITTED(提交读),大多数数据库系统的默认隔离级别(MySQL不是)。一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。也叫不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
REPEATABLE READ(可重复读),(MySQL的默认事务隔离级别),解决了脏读问题。保证了在同一个事务中多次读取同样记录的结果是一致的。
SERIALIZABLE(可串行化),最高的隔离级别。它通过强制事务串行执行,避免了幻读问题(幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行【与不可重复读的区别就在于换行重点为新增或删除,不可重复读重点为修改】)。简单来说,可串行化会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。只有在非常需要确保数据一致且可以接受没有并发的情况下使用该级别。
1.Spring事务的配置方式
Spring支持编程式事务管理以及声明式事务管理两种方式。
1.1编程式事务管理
编程式事务管理是侵入性事务管理,一般使用TransactionTemplate或者直接使用PlatformTransactionManager
1.2声明式事务管理
编程式事务管理每次都要单独实现,当业务量大且功能复杂时,使用编程式事务管理就会非常复杂。而声明式事务管理属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件种做相关的事务规则声明或者直接通过注解的方式,便可以将事务规则应用到业务逻辑中。
因此,Spring推荐使用声明式事务管理。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理可以到代码块级别,不过可以通过将代码块提取为方法的方式实现声明式事务管理的配置。
声明式事务本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
2.Spring事务的传播机制
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
常用的事务传播机制如下:
- PROPAGATION_REQUIRED 【REQUIRED:你有我就用你的,你没有我就新建一个用】
Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行。 - PROPAGATION_REQUIRES_NEW 【REQUIRES_NEW:你有没有我都要新建一个用,我用的时候你不许动】
该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可。 - PROPAGATION_SUPPORT 【SUPPORT:你有我能用,你没有我就不用了】
如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务。 - PROPAGATION_NOT_SUPPORT 【NOT_SUPPORT:我不能用,我动的时候你也不能用】
该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码。 - PROPAGATION_NEVER 【NEVER:我没有,你也不许有】
该传播机制不支持外层事务,即如果外层有事务就抛出异常。 - PROPAGATION_MANDATORY 【MANDATORY:我没有,我要用你的,你不行没有】
与NEVER相反,如果外层没有事务,则抛出异常。 - PROPAGATION_NESTED 【NESTED:你没有我就新建一个用;你有我就在你的里面新建一个小的】
如果当前存在事务,则创建一个事务作为当前事务嵌套事务执行,嵌套事务开始执行时,它将取得一个 savepoint,如果这个嵌套事务失败,我们将回滚到此 savepoint,从而避免全部事务回滚。嵌套事务是外部事务的一部分(子事务),只有外部事务结束后它才会被提交。如果当前不存在事务则新建一个事务执行
3.事物的隔离级别
事务的隔离级别定义一个事务可能受其他并发务活动活动影响的程度,可以把事务的隔离级别想象为这个事务对于事物处理数据的自私程度。
在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常涉及到锁定在数据库中的记录(甚至有时是锁表)。完全隔离要求事务相互等待来完成工作,会阻碍并发。因此,可以根据业务场景选择不同的隔离级别。
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT(默认隔离级别) | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITTED(未提交读) | 允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读。 |
ISOLATION_READ_COMMITTED(提交读) | (Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生。 |
ISOLATION_REPEATABLE_READ(可重复读) | (MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生。 |
ISOLATION_SERIALIZABLE(可串行化) | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
4.只读
如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
5.事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
6.回滚规则
在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。
7.Spring声明式事务配置参考
表 1. @Transactional 注解的属性信息
属性名 | 说明 |
---|---|
value或transactionManager | 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。@Transactional(transactionManager = “DataSourceA”) @Transactional(value = “DataSourceA”) |
propagation | 事务的传播行为,默认值为 Propagation.REQUIRED。 |
isolation | 事务的隔离度,默认值采用 Isolation.DEFAULT。 |
timeout | 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
read-only | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 |
rollback-for | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。@Transactional(rollbackFor={RuntimeException.class, MyException.class})(默认运行时异常RuntimeException及其子类和Error) |
no-rollback- for | 抛出 no-rollback-for 指定的异常类型,不回滚事务。 |
8.Spring事务实现原理
事务实现主要流程:
- 通过@Transactional注解的配置,获取事务的属性。例如事务管理器、传播机制、隔离级别、超时限制、是否只读、回滚异常与回滚类。
- 加载属性中的事务管理器TransactionManager,或使用默认的事务管理器。事务管理器保存着当前数据源连接,提供对该数据源的事务的提交、回滚、获取事务状态、挂起事务、开始事务等方法。如果代码中有多个数据库,最好指定事务管理器。
- 记录事务信息TransactionInfo,事务信息包括TransactionStatus(TransactionStatus作用:当前运行的是哪个事务,当前事务是否是一个新的事务,是否需要加入到一个已经存在的事务中,当前事务是否存在保存点savepoint,事务是否已经提交或回滚)、TransactionManager事务管理器、事务属性以及外层事务信息。
- try代码块:执行目标方法,即带有事务的方法。
- 如果执行失败,catch捕获异常并回滚事务。
- finally代码块:清理当前线程的事务的信息TransactionInfo。
- 如果执行成功,提交事务。
具体实现原理:
在应用系统调用声明@Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,(Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 两种,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptor 的 intercept 方法。对于 JdkDynamicAopProxy,需要调用其 invoke 方法)
在代码运行时生成一个代理对象,根据@Transactional 的属性配置信息,这个代理对象决定该声明@Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务,如图 1 所示。
图 1. Spring 事务实现机制
事务管理的框架是由抽象事务管理器 AbstractPlatformTransactionManager 来提供的,而具体的底层事务处理实现,由 PlatformTransactionManager 的具体实现类来实现,如事务管理器 DataSourceTransactionManager。不同的事务管理器管理不同的数据资源 DataSource,比如 DataSourceTransactionManager 管理 JDBC 的 Connection。
PlatformTransactionManager,AbstractPlatformTransactionManager 及具体实现类关系如图 2 所示。
图 2. TransactionManager 类结构
@Transactional 只能应用到 public 方法才有效
只有@Transactional 注解应用到 public 方法,才能进行事务管理。这是因为在使用 Spring AOP 代理时,Spring 在调用在图 1 中的 TransactionInterceptor 在目标方法执行前后进行拦截之前,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法。
Spring 通过AbstractFallbackTransactionAttributeSource类获取@Transactional 注解的事务属性配置属性信息
computeTransactionAttribute 方法会检查目标方法的修饰符是不是 public,若不是 public,就不会获取@Transactional 的属性配置信息,最终会造成不会用 TransactionInterceptor 来拦截该目标方法进行事务管理。
避免 Spring 的 AOP 的自调用问题
在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,这会造成自调用问题。若同一类中的其他没有@Transactional 注解的方法内部调用有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会发生回滚。见举例代码展示。
@Service
public class OrderService {
private void insert() {
insertOrder();
}
@Transactional
public void insertOrder() {
//...
}
}
insertOrder 尽管有@Transactional 注解,但它被内部方法 insert 调用,事务被忽略,出现异常事务不会发生回滚。
@Transactional 注解只应用到 public 方法和自调用问题,是由于使用 Spring AOP 代理造成的。为解决这两个问题,可以使用 AspectJ 取代 Spring AOP 代理。
9.什么时候事务会失效?
- 当事务传播机制是SUPPORTS且外层没有事务时失效,是NOT_SUPPORTED和NEVER时一定失效。
- 没有用rollbackFor指定回滚异常类。
- 使用SpringAOP时@Transactional 只能应用到 public 方法才有效。
- 数据库存储引擎Innodb支持事务,MyISAM不支持事务。
- 一个事务中涉及到了多个数据库,没有指定transactionManager事务管理器参数,默认的并不是所期望的。
- SpringAOP的自调用问题。
参考:
《高性能MySQL》