Spring事务介绍
[toc]
1.核心接口
Spring事务管理的实现有许多细节,下图是Spring的事务接口以及Spring实现事务的具体策略。Spring事务管理涉及的接口的联系如下:
1.1 事务管理器
Spring的事务属于逻辑事务。不是物理事务。
Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 例如JDBC的事物管理器就是DataSourceTransactionManager。
Spring事务管理器的接口是PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。接口如下:
1 | public interface PlatformTransactionManager { |
1.1.1 JDBC事务
如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会处理事务边界。实际上,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务,而后者是通过DataSource获取到的。通过调用连接的commit()方法来提交事务,同样,事务失败则通过调用rollback()方法进行回滚。
1.1.2 Hibernate事务
如果应用程序的持久化是通过Hibernate实现的,需要使用HibernateTransactionManager。sessionFactory属性需要装配一个Hibernate的session工厂,HibernateTransactionManager的实现细节是它将事务管理的职责委托给org.hibernate.Transaction对象,而后者是从Hibernate Session中获取到的。当事务成功完成时,HibernateTransactionManager将会调用Transaction对象的commit()方法,反之,将会调用rollback()方法。
1.1.3 Java持久化API事务(JPA)
如果使用JPA的话,那你需要使用Spring的JpaTransactionManager来处理事务。JpaTransactionManager只需要装配一个JPA实体管理工厂(javax.persistence.EntityManagerFactory接口的任意实现)。JpaTransactionManager将与由工厂所产生的JPA EntityManager合作来构建事务。
1.1.4 Java原生API事务
如果没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),就需要使用JtaTransactionManager。JtaTransactionManager将事务管理的责任委托给javax.transaction.UserTransaction和javax.transaction.TransactionManager对象,其中事务成功完成通过UserTransaction.commit()方法提交,事务失败通过UserTransaction.rollback()方法回滚。
1.2 基本事务属性的定义
事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法来得到事务,这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。
事务属性包含了5个方面:
(1)传播行为
(2)隔离规则
(3)是否只读
(4)事务超时
(5)回滚规则
TransactionDefinition接口内容如下:
1 | public interface TransactionDefinition { |
1.2.1 传播行为
事务属性的第一个方面是传播行为。当一个事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,在新事物中运行。
Spring定义了七种传播行为:
| 传播行为 | 含义 |
|---|---|
| PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。 |
| PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行。 |
| PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
| PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
| PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
| PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
| PROPAGATION_NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
(1)PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
1 | //事务属性 PROPAGATION_REQUIRED |
1 | //事务属性 PROPAGATION_REQUIRED |
使用spring声明式事务,spring使用AOP来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。
单独调用methodB方法相当于:
1 | Connection con=null; |
Spring保证在methodB方法中所有的调用都会得到一个相同的连接。在调用methodB时,环境中没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务,让methodB在这个事务中运行。
调用methodA时,在methodA内又会调用methodB。执行效果相当于:
1 | Connection con = null; |
调用methodA时,环境中没有事务,所以开启一个新的事务。当在methodA中调用methodB时,环境中已经有了一个事务,所以methodB就加入当前事务,不需要新开一个事务。
(2)PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行(不创建事务)。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
(3)PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
(4)PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
1 | //事务属性 PROPAGATION_REQUIRED |
1 | //事务属性 PROPAGATION_REQUIRES_NEW |
调用A方法相当于:
1 | TransactionManager tm = null; |
在这里,把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了methodB之外的其它代码导致的结果却被回滚了。使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
(5)PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。
(6)PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。
(7)PROPAGATION_NESTED 如果有一个活动的事务存在,则运行在一个嵌套的事务中;如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。需要JDBC 驱动的java.sql.Savepoint类。有一些JTA的事务管理器实现可能也提供了同样的功能。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true;而 nestedTransactionAllowed属性值默认为false。
1 | //事务属性 PROPAGATION_REQUIRED |
1 | //事务属性 PROPAGATION_NESTED |
如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:
1 | Connection con = null; |
当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。
嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
• 它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
• 使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTA TrasactionManager实现可能有不同的支持方式。
• PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务。这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
• 另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.
PROPAGATION_REQUIRED应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。
1.2.2 隔离级别
事务的第二个维度就是隔离级别。隔离级别定义了一个事务可能受其他并发事务影响的程度。
1.2.2.1并发事务引起的问题
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致以下的问题。
脏读(Dirty reads)
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
不可重复读(Nonrepeatable read)
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,数据被另一个事务修改并提交了。例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了另一个事务提交的数据。
幻读(Phantom read)
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。
幻读和不可重复读都是读取了另一条已经提交的事务(这点脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
不可重复读与幻读的区别
1)不可重复读的重点是修改: 同样的条件, 读取过的数据, 再次读取出来发现值不一样了 。
2)幻读的重点在于新增或者删除: 同样的条件, 第1次和第2次读出来的记录数不一样(针对的是一批数据) 。
从总的结果来看, 似乎不可重复读和幻读都表现为两次读取的结果不一致。但如果从控制的角度来看, 两者的区别就比较大。
对于不可重复读, 只需要锁住满足条件的记录。 对于幻读, 要锁住满足条件及其相近的记录。
1.2.2.2隔离级别
| 隔离级别 | 含义 |
|---|---|
| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
| ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读 |
| ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据。可以阻止脏读,但是幻读或不可重复读仍有可能发生 |
| ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发 |
| ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 |
1.2.3 只读
事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读。
1.2.4 事务超时
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
1.2.5 回滚规则
事务的回滚规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。
默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚。
但是可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。
1.3 事务状态
调用PlatformTransactionManager接口的getTransaction()的方法得到的是TransactionStatus接口的一个实现,这个接口的内容如下:
1 | public interface TransactionStatus { |
可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。
2.编程式事务
2.1 编程式和声明式事务的区别
Spring提供了对编程式事务和声明式事务的支持。
编程式事务 :允许用户在代码中精确定义事务的边界。
声明式事务(基于AOP):有助于用户将操作与事务规则进行解耦。
简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。
2.2 如何实现编程式事务?
Spring提供两种方式的编程式事务管理,分别是:
(1)使用TransactionTemplate
(2)直接使用PlatformTransactionManager
2.2.1 使用TransactionTemplate
采用TransactionTemplate和采用其他Spring模板,如JdbcTempalte和HibernateTemplate是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate是线程安全的。代码片段:
1 | // 新建一个TransactionTemplate |
使用TransactionCallback()可以返回一个值。如果使用TransactionCallbackWithoutResult则没有返回值。
2.2.2 使用PlatformTransactionManager
示例代码如下:
1 | // 定义一个某个框架平台的TransactionManager,如JDBC、Hibernate |
3.声明式事务
一般使用@Transactional注解的方式,当然需要先注册TransactionManager 这个Bean。
@Transactional注解
1 | @Target({ElementType.METHOD, ElementType.TYPE}) |
@Transactional(propagation=Propagation.REQUIRED) 指定事务传播行为。Propagation取值:
REQUIRED(默认值):代表当前方法支持当前的事务,且与调用者处于同一事务上下文中,回滚统一回滚(如果当前方法是被其他方法调用的时候,且调用者本身即有事务),如果没有事务,则自己新建事务,
SUPPORTS:代表当前方法支持当前的事务,且与调用者处于同一事务上下文中,回滚统一回滚(如果当前方法是被其他方法调用的时候,且调用者本身即有事务),如果没有事务,则该方法在非事务的上下文中执行
MANDATORY:代表当前方法支持当前的事务,且与调用者处于同一事务上下文中,回滚统一回滚(如果当前方法是被其他方法调用的时候,且调用者本身即有事务),如果没有事务,则抛出异常
REQUIRES_NEW:创建一个新的事务上下文,如果当前方法的调用者已经有了事务,则挂起调用者的事务,这两个事务不处于同一上下文,如果各自发生异常,各自回滚。
NOT_SUPPORTED:该方法以非事务的状态执行,如果调用该方法的调用者有事务则先挂起调用者的事务。
NEVER: 该方法以非事务的状态执行,如果调用者存在事务,则抛出异常。
NESTED:如果当前上下文中存在事务,则以嵌套事务执行该方法,也就说,这部分方法是外部方法的一部分,调用者回滚,则该方法回滚,但如果该方法自己发生异常,则自己回滚,不会影响外部事务,如果不存在事务,则与PROPAGATION_REQUIRED一样。
timeout 设置事物超时设置,默认是30秒
isolation 设置事务隔离级别。Isolation.READ_UNCOMMITTED读取未提交数据(会出现脏读, 不可重复读) 基本不使用;Isolation.READ_COMMITTED 读取已提交数据(会出现不可重复读和幻读);Isolation.REPEATABLE_READ可重复读(会出现幻读);Isolation.SERIALIZABLE)串行化。
readOnly 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。
rollbackFor 该属性用于设置需要进行回滚的异常类,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:@Transactional(rollbackFor=RuntimeException.class)。
rollbackForClassNam 该属性用于设置需要进行回滚的异常类名称,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。@Transactional(rollbackForClassName=”RuntimeException”)。
noRollbackFor该属性用于设置不需要进行回滚的异常类,当方法中抛出指定异常数组中的
noRollbackForClassName该属性用于设置不需要进行回滚的异常类名称,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。
注意的几点:
1、@Transactional 只能被应用到public方法上, 对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能.
2、用 Spring 事务管理器,由Spring来负责数据库的打开,提交,回滚.默认遇到运行期例外(throw new RuntimeException();)会回滚,而遇到需要捕获的例外(throw new Exception(“”);)不会回滚。当需要用指定方式来让事务回滚要想所有异常都回滚,要加上 @Transactional( rollbackFor={Exception.class})。
3、Spring团队的建议是你在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。你当然可以在接口上使用 @Transactional 注解,但是这将只能当你设置了基于接口的代理时它才生效。因为注解是不能继承的,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(将被确认为严重的)。
Spring Boot 使用事务非常简单,首先使用注解@EnableTransactionManagement开启事务支持后,然后在Service方法上添加注解@Transactional便可。@EnableTransactionManagement,启注解事务管理等同于xml配置方式的 <tx:annotation-driven />
@Transactional注解工作原理
运行配置@Transactional注解的类的时候,具体会发生如下步骤:
1)事务开始时,通过AOP机制,生成一个代理connection对象,并将其放入DataSource实例的某个与DataSourceTransactionManager相关的某处容器中。在接下来的整个事务中,客户代码都应该使用该connection连接数据库,执行所有数据库命令[不使用该connection连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚]。
2)事务结束时,回滚在第1步骤中得到的代理connection对象上执行的数据库命令,然后关闭该代理connection对象。
@Transactional注解使用示例
1 | /** |