事务

事务概述

事务是一组单元化的操作,这组操作可以保证要么全部成功,要么全部失败(只要有一个失败的操作,就会把其他已经成功的操作回滚)。
这样的解释还是不够直观,看下面一个经典的例子。假设有两个银行账户A和B,现在A要给B转10块钱,也就是转账。在银行系统中A和B是两个独立的账户,所以转账操作会被分解:

  • 从A的账户中扣掉10块钱
  • 在B的账户中添加10块钱

转账中数据库的处理会涉及到两条 SQL 语句:

  1. update Acoount set balance = balance - 10 where id = 'A';
  2. update Acoount set balance = balance + 10 where id = 'B'

上面这两条 SQL 就可以要看成是一个事务,必须都执行,或都不执行。如何保证呢,一般这样表示:

  1. # 开启事务
  2. begin transaction
  3. # 执行业务逻辑
  4. update Account set balance = balance - money where id = 'A';
  5. update Account set balance = balance + money where id = 'B'
  6. # 提交事务
  7. commit transaction
  8. Exception
  9. # 回滚事务
  10. rollback transaction

事务的特性

  • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
  • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
  • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

以上 4 个属性常被简称为 acid(酸的)。

事务并发的问题

在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致以下问题:

  • 脏读:事务二读取到事务一中已经更新但是还没有提交的数据,这就是脏读。

  • 不可重复读:一个事务两次读取同一个行数据结果不同,因为有其它的事务对数据进行了更新。此时的数据即为不可重复读数据。

    不可重复读重点在修改。

  • 幻读:同一事务执行两次查询,结果不一致,因为中间有其它的事务对数据进行更改。

    幻读重点在新增或删除。

如何解决这些问题呢?数据库系统为事务设置了 4 种不同的隔离级别。

事务的隔离级别

  • 读未提交(read uncommitted):最低级别,该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。可能导致脏读、幻读或不可重复读。

  • 读已提交(read committed):该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,且具有良好的性能,但是不能避免不可重复读和幻读。这也是大多数情况下的推荐值。


  • 可重复读(repeatable):该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读,但可能会出现幻读。

  • 串行化(serializable):所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。

另外,MySQL 中默认的隔离级别是可重复读。Oracle 中默认的事务隔离级别是读已提交。

Spring 事务

想想我们曾经为了处理事务而写过的那些代码,在 JDBC 时代我们需要这样手动的处理事务:

  1. // 获取连接 conn
  2. conn.setAutoCommit(false); 设置提交方式为手工提交
  3. // 业务代码
  4. // 减钱
  5. // 加钱
  6. conn.commit(); 提交事务
  7. // 出现异常
  8. conn.rollback(); 回滚事务

在 Spring 中,事务是通过 TransactionDefinition 接口来定义的。该接口包含与事务属性有关的方法:

  1. public interface TransactionDefinition{
  2. // 事务隔离级别
  3. int getIsolationLevel();
  4. // 事务传播行为
  5. int getPropagationBehavior();
  6. // 事务超时时间
  7. int getTimeout();
  8. // 事务的只读状态
  9. boolean isReadOnly();
  10. }

也许你会奇怪,为什么接口只提供了获取属性的方法,而没有提供相关设置属性的方法。其实道理很简单,事务属性的设置完全是程序员控制的,因此程序员可以自定义任何设置属性的方法,而且保存属性的字段也没有任何要求。唯一的要求的是,Spring 进行事务操作的时候,通过调用以上接口提供的方法必须能够返回事务相关的属性取值。

事务的隔离级别

事务传播行为

Spring中事务的7个传播行为

事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
在TransactionDefinition定义中包括了如下几个表示传播行为的常量:

事务行为 说明
PROPAGATION_REQUIRED 支持当前事务,假设当前没有事务。就新建一个事务
PROPAGATION_SUPPORTS 支持当前事务,假设当前没有事务,就以非事务方式运行
PROPAGATION_MANDATORY 支持当前事务,假设当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW 新建事务,假设当前存在事务。把当前事务挂起
PROPAGATION_NOT_SUPPORTED 以非事务方式运行操作。假设当前存在事务,就把当前事务挂起
PROPAGATION_NEVER 以非事务方式运行,假设当前存在事务,则抛出异常
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
  • TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。

  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。对应事务隔离级别的

  • TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。

  • TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。

  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别

    传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。

举例说明

案例代码

ServiceA

  1. ServiceA {
  2. void methodA() {
  3. ServiceB.methodB();
  4. }
  5. }

ServiceB

  1. ServiceB {
  2. void methodB() {
  3. }
  4. }

PROPAGATION_REQUIRED

假如当前正要运行的事务不在另外一个事务里,那么就起一个新的事务 比方说, ServiceB.methodB 的事务级别定义 PROPAGATION_REQUIRED , 那么因为执行 ServiceA.methodA 的时候, ServiceA.methodA 已经起了事务。这时调用 ServiceB.methodBServiceB.methodB 看到自己已经执行在 ServiceA.methodA 的事务内部。就不再起新的事务。而假如 ServiceA.methodA 执行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在 ServiceA.methodA 或者在 ServiceB.methodB 内的不论什么地方出现异常。事务都会被回滚。即使 ServiceB.methodB 的事务已经被提交,可是 ServiceA.methodA 在接下来 fail 要回滚, ServiceB.methodB 也要回滚:
Spring事务 - 图1

PROPAGATION_SUPPORTS

假设当前在事务中。即以事务的形式执行。假设当前不在一个事务中,那么就以非事务的形式执行

PROPAGATION_MANDATORY

必须在一个事务中执行。也就是说,他仅仅能被一个父事务调用。否则,他就要抛出异常

PROPAGATION_REQUIRES_NEW

这个就比较绕口了。 比方我们设计 ServiceA.methodA 的事务级别为 PROPAGATION_REQUIREDServiceB.methodB 的事务级别为 PROPAGATION_REQUIRES_NEW 。那么当运行到 ServiceB.methodB 的时候, ServiceA.methodA 所在的事务就会挂起。 ServiceB.methodB 会起一个新的事务。等待 ServiceB.methodB 的事务完毕以后,他才继续运行。
他与 PROPAGATION_REQUIRED 的事务差别在于事务的回滚程度了。由于 ServiceB.methodB 是新起一个事务,那么就是存在两个不同的事务。假设 ServiceB.methodB 已经提交,那么 ServiceA.methodA 失败回滚。 ServiceB.methodB 是不会回滚的。假设 ServiceB.methodB 失败回滚,假设他抛出的异常被 ServiceA.methodA 捕获, ServiceA.methodA 事务仍然可能提交。
Spring事务 - 图2

PROPAGATION_NOT_SUPPORTED

当前不支持事务。比方 ServiceA.methodA 的事务级别是 PROPAGATION_REQUIRED 。而 ServiceB.methodB 的事务级别是 PROPAGATION_NOT_SUPPORTED ,那么当执行到 ServiceB.methodB 时。 ServiceA.methodA 的事务挂起。而他以非事务的状态执行完,再继续 ServiceA.methodA 的事务。

PROPAGATION_NEVER

不能在事务中执行。
如果 ServiceA.methodA 的事务级别是 PROPAGATION_REQUIRED 。 而 ServiceB.methodB 的事务级别是 PROPAGATION_NEVER ,那么 ServiceB.methodB 就要抛出异常了。

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

事务超时

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。

假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。

在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。

事务的只读属性

如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。

事务的回滚规则

通常情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。这通常也是大多数开发者希望的处理方式,也是 EJB 中的默认处理方式。但是,我们可以根据需要人为控制事务在抛出某些未检查异常时任然提交事务,或者在抛出某些已检查异常时回滚事务。

Spring 中的如何管理事务

所谓事务管理,其实就是”按照给定的事务规则来执行提交或者回滚操作”。Spring 框架中,涉及到事务管理的 API 大约有100个左右,其中最重要的有三个:TransactionDefinition、PlatformTransactionManager、TransactionStatus。
“给定的事务规则”就是用 TransactionDefinition 表示的,按照……来执行提交或者回滚操作”便是用 PlatformTransactionManager 来表示,而 TransactionStatus 用于表示一个运行着的事务的状态。打一个不恰当的比喻,TransactionDefinition 与 TransactionStatus 的关系就像程序和进程的关系。


TransactionDefinition

该接口在前面已经介绍过,它用于定义一个事务。它包含了事务的静态属性,比如:事务传播行为、超时时间等等。Spring 为我们提供了一个默认的实现类:DefaultTransactionDefinition,该类适用于大多数情况。如果该类不能满足需求,可以通过实现 TransactionDefinition 接口来实现自己的事务定义。

PlatformTransactionManager

我们知道 Spring 是一个容器,不同的框架在处理事务时用到的对象不同,原生的 JDBC 使用 Connection ,而 Mybatis 中使用 SqlSession 对象。而 Spring 为了整合这些不同的框架,定义了一个 PlatformTransactionManager 接口来统一标准,对不同的框架又有不同的实现类。

PlatformTransactionManager 接口中定义的主要方法:
  1. Public interface PlatformTransactionManager{
  2. TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
  3. void commit(TransactionStatus status)throws TransactionException;
  4. void rollback(TransactionStatus status)throws TransactionException;
  5. }

根据底层所使用的不同的持久化 API 或框架,PlatformTransactionManager 的主要实现类大致如下:

  • DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况。
  • HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
  • JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。
  • 另外还有JtaTransactionManager 、JdoTransactionManager、JmsTransactionManager等等。

TransactionStatus

PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象。返回的TransactionStatus 对象可能代表一个新的或已经存在的事务(如果在当前调用堆栈有一个符合条件的事务)。TransactionStatus 接口提供了一个简单的控制事务执行和查询事务状态的方法。该接口定义所示:

  1. public interface TransactionStatus{
  2. boolean isNewTransaction();
  3. void setRollbackOnly();
  4. boolean isRollbackOnly();
  5. }

Spring事务的配置方式

编程式事务管理

在 Spring 出现以前,编程式事务管理对基于 POJO 的应用来说是唯一选择。用过 Hibernate 的人都知道,我们需要在代码中显式调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。

编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。

通过 Spring 提供的事务管理 API,我们可以在代码中灵活控制事务的执行。在底层,Spring 仍然将事务操作委托给底层的持久化框架来执行。

基于底层 API 的编程式事务管理

根据PlatformTransactionManager、TransactionDefinition 和 TransactionStatus 三个核心接口,我们完全可以通过编程的方式来进行事务管理。示例代码如下:

  1. public class BankServiceImpl implements BankService {
  2. private BankDao bankDao;
  3. private TransactionDefinition txDefinition;
  4. private PlatformTransactionManager txManager;
  5. ......
  6. public boolean transfer(Long fromId Long toId double amount) {
  7. TransactionStatus txStatus = txManager.getTransaction(txDefinition);
  8. boolean result = false;
  9. try {
  10. result = bankDao.transfer(fromId toId amount);
  11. txManager.commit(txStatus);
  12. } catch (Exception e) {
  13. result = false;
  14. txManager.rollback(txStatus);
  15. System.out.println("Transfer Error!");
  16. }
  17. return result;
  18. }
  19. }

相应的配置文件如下:

  1. <bean id="bankService" class="footmark.spring.core.tx.programmatic.origin.BankServiceImpl">
  2. <property name="bankDao" ref="bankDao"/>
  3. <property name="txManager" ref="transactionManager"/>
  4. <property name="txDefinition">
  5. <bean class="org.springframework.transaction.support.DefaultTransactionDefinition">
  6. <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
  7. </bean>
  8. </property>
  9. </bean>

如上所示,我们在类中增加了两个属性:一个是 TransactionDefinition 类型的属性,它用于定义一个事务;另一个是 PlatformTransactionManager 类型的属性,用于执行事务管理操作。
如果方法需要实施事务管理,我们首先需要在方法开始执行前启动一个事务,调用PlatformTransactionManager.getTransaction(…) 方法便可启动一个事务。创建并启动了事务之后,便可以开始编写业务逻辑代码,然后在适当的地方执行事务的提交或者回滚。

基于 TransactionTemplate 的编程式事务管理

通过前面的示例可以发现,这种事务管理方式很容易理解,但令人头疼的是,事务管理的代码散落在业务逻辑代码中,破坏了原有代码的条理性,并且每一个业务方法都包含了类似的启动事务、提交/回滚事务的样板代码。
幸好,Spring 也意识到了这些,并提供了简化的方法,这就是 Spring 在数据访问层非常常见的模板回调模式。如下所示:

  1. public class BankServiceImpl implements BankService {
  2. private BankDao bankDao;
  3. private TransactionTemplate transactionTemplate;
  4. ......
  5. public boolean transfer(final Long fromId final Long toId final double amount) {
  6. return (Boolean) transactionTemplate.execute(new TransactionCallback(){
  7. public Object doInTransaction(TransactionStatus status) {
  8. Object result;
  9. try {
  10. result = bankDao.transfer(fromId toId amount);
  11. } catch (Exception e) {
  12. status.setRollbackOnly();
  13. result = false;
  14. System.out.println("Transfer Error!");
  15. }
  16. return result;
  17. }
  18. });
  19. }
  20. }

相应的XML配置如下:

  1. <bean id="bankService" class="footmark.spring.core.tx.programmatic.template.BankServiceImpl">
  2. <property name="bankDao" ref="bankDao"/>
  3. <property name="transactionTemplate" ref="transactionTemplate"/>
  4. </bean>

TransactionTemplate 的 execute() 方法有一个 TransactionCallback 类型的参数,该接口中定义了一个 doInTransaction() 方法,通常我们以匿名内部类的方式实现 TransactionCallback 接口,并在其 doInTransaction() 方法中书写业务逻辑代码。这里可以使用默认的事务提交和回滚规则,这样在业务代码中就不需要显式调用任何事务管理的 API。doInTransaction() 方法有一个TransactionStatus 类型的参数,我们可以在方法的任何位置调用该参数的 setRollbackOnly() 方法将事务标识为回滚的,以执行事务回滚。
根据默认规则,如果在执行回调方法的过程中抛出了未检查异常,或者显式调用了TransacationStatus.setRollbackOnly() 方法,则回滚事务;如果事务执行完成或者抛出了 checked 类型的异常,则提交事务。
TransactionCallback 接口有一个子接口 TransactionCallbackWithoutResult,该接口中定义了一个 doInTransactionWithoutResult() 方法,TransactionCallbackWithoutResult 接口主要用于事务过程中不需要返回值的情况。当然,对于不需要返回值的情况,我们仍然可以使用 TransactionCallback 接口,并在方法中返回任意值即可。

声明式事务管理

Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。因为事务管理本身就是一个典型的横切逻辑,正是 AOP 的用武之地。Spring 开发团队也意识到了这一点,为声明式事务提供了简单而强大的支持。

声明式事务管理曾经是 EJB 引以为傲的一个亮点,如今 Spring 让 POJO 在事务管理方面也拥有了和 EJB 一样的待遇,让开发人员在 EJB 容器之外也用上了强大的声明式事务管理功能,这主要得益于 Spring 依赖注入容器和 Spring AOP 的支持。依赖注入容器为声明式事务管理提供了基础设施,使得 Bean 对于 Spring 框架而言是可管理的;而 Spring AOP 则是声明式事务管理的直接实现者,这一点通过清单8可以看出来。

通常情况下,笔者强烈建议在开发中使用声明式事务,不仅因为其简单,更主要是因为这样使得纯业务代码不被污染,极大方便后期的代码维护。
和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

基于 TransactionInterceptor 的事务管理

最初,Spring 提供了 TransactionInterceptor 类来实施声明式事务管理功能:

  1. <beans...>
  2. ......
  3. <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
  4. <property name="transactionManager" ref="transactionManager"/>
  5. <property name="transactionAttributes">
  6. <props>
  7. <prop key="transfer">PROPAGATION_REQUIRED</prop>
  8. </props>
  9. </property>
  10. </bean>
  11. <bean id="bankServiceTarget" class="footmark.spring.core.tx.declare.origin.BankServiceImpl">
  12. <property name="bankDao" ref="bankDao"/>
  13. </bean>
  14. <bean id="bankService"class="org.springframework.aop.framework.ProxyFactoryBean">
  15. <property name="target" ref="bankServiceTarget"/>
  16. <property name="interceptorNames">
  17. <list>
  18. <idref bean="transactionInterceptor"/>
  19. </list>
  20. </property>
  21. </bean>
  22. ......
  23. </beans>

首先,我们配置了一个 TransactionInterceptor 来定义相关的事务规则,他有两个主要的属性:一个是 transactionManager,用来指定一个事务管理器,并将具体事务相关的操作委托给它;另一个是 Properties 类型的 transactionAttributes 属性,它主要用来定义事务规则,该属性的每一个键值对中,键指定的是方法名,方法名可以使用通配符,而值就表示相应方法的所应用的事务属性。
指定事务属性的取值有较复杂的规则,这在 Spring 中算得上是一件让人头疼的事。具体的书写规则如下:

  1. 传播行为 [,隔离级别] [,只读属性] [,超时属性] [不影响提交的异常] [,导致回滚的异常]
  • 传播行为是唯一必须设置的属性,其他都可以忽略,Spring为我们提供了合理的默认值。
  • 传播行为的取值必须以”PROPAGATION_”开头,具体包括:PROPAGATION_MANDATORY、PROPAGATION_NESTED、PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED、PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_SUPPORTS,共七种取值。
  • 隔离级别的取值必须以”ISOLATION_”开头,具体包括:ISOLATION_DEFAULT、ISOLATION_READ_COMMITTED、ISOLATION_READ_UNCOMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE,共五种取值。
  • 如果事务是只读的,那么我们可以指定只读属性,使用”readOnly”指定。否则我们不需要设置该属性。
  • 超时属性的取值必须以”TIMEOUT_”开头,后面跟一个int类型的值,表示超时时间,单位是秒。
  • 不影响提交的异常是指,即使事务中抛出了这些类型的异常,事务任然正常提交。必须在每一个异常的名字前面加上”+”。异常的名字可以是类名的一部分。比如”+RuntimeException”、”+tion”等等。
  • 导致回滚的异常是指,当事务中抛出这些类型的异常时,事务将回滚。必须在每一个异常的名字前面加上”-”。异常的名字可以是类名的全部或者部分,比如”-RuntimeException”、”-tion”等等。

以下是两个示例:

  1. <property name="*Service">
  2. PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,TIMEOUT_20,+AbcException,+DefException,-HijException
  3. </property>

以上表达式表示,针对所有方法名以 Service 结尾的方法,使用 PROPAGATION_REQUIRED 事务传播行为,事务的隔离级别是 ISOLATION_READ_COMMITTED,超时时间为20秒,当事务抛出 AbcException 或者 DefException 类型的异常,则仍然提交,当抛出 HijException 类型的异常时必须回滚事务。这里没有指定”readOnly”,表示事务不是只读的。

  1. <property name="test">PROPAGATION_REQUIRED,readOnly</property>

以上表达式表示,针对所有方法名为 test 的方法,使用 PROPAGATION_REQUIRED 事务传播行为,并且该事务是只读的。除此之外,其他的属性均使用默认值。比如,隔离级别和超时时间使用底层事务性资源的默认值,并且当发生未检查异常,则回滚事务,发生已检查异常则仍提交事务。

配置好了 TransactionInterceptor,我们还需要配置一个 ProxyFactoryBean 来组装 target 和advice。这也是典型的 Spring AOP 的做法。通过 ProxyFactoryBean 生成的代理类就是织入了事务管理逻辑后的目标类。至此,声明式事务管理就算是实现了。我们没有对业务代码进行任何操作,所有设置均在配置文件中完成,这就是声明式事务的最大优点。

基于 TransactionProxyFactoryBean 的声明式事务管理

前面的声明式事务虽然好,但是却存在一个非常恼人的问题:配置文件太多。我们必须针对每一个目标对象配置一个 ProxyFactoryBean;另外,虽然可以通过父子 Bean 的方式来复用 TransactionInterceptor 的配置,但是实际的复用几率也不高;这样,加上目标对象本身,每一个业务类可能需要对应三个 <bean/> 配置,随着业务类的增多,配置文件将会变得越来越庞大,管理配置文件又成了问题。
为了缓解这个问题,Spring 为我们提供了 TransactionProxyFactoryBean,用于将TransactionInterceptor 和 ProxyFactoryBean 的配置合二为一。
基于 TransactionProxyFactoryBean 的事务管理示例配置文件:

  1. <beans......>
  2. ......
  3. <bean id="bankServiceTarget" class="footmark.spring.core.tx.declare.classic.BankServiceImpl">
  4. <property name="bankDao" ref="bankDao"/>
  5. </bean>
  6. <bean id="bankService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
  7. <property name="target" ref="bankServiceTarget"/>
  8. <property name="transactionManager" ref="transactionManager"/>
  9. <property name="transactionAttributes">
  10. <props>
  11. <prop key="transfer">PROPAGATION_REQUIRED</prop>
  12. </props>
  13. </property>
  14. </bean>
  15. ......
  16. </beans>

如此一来,配置文件与先前相比简化了很多。我们把这种配置方式称为 Spring 经典的声明式事务管理。相信在早期使用 Spring 的开发人员对这种配置声明式事务的方式一定非常熟悉。
但是,显式为每一个业务类配置一个 TransactionProxyFactoryBean 的做法将使得代码显得过于刻板,为此我们可以使用自动创建代理的方式来将其简化,使用自动创建代理是纯 AOP 知识,请读者参考相关文档,不在此赘述。

基于 <tx> 命名空间的声明式事务管理

前面两种声明式事务配置方式奠定了 Spring 声明式事务管理的基石。在此基础上,Spring 2.x 引入了 <tx> 命名空间,结合使用 <aop> 命名空间,带给开发人员配置声明式事务的全新体验,配置变得更加简单和灵活。另外,得益于 <aop> 命名空间的切点表达式支持,声明式事务也变得更加强大。

  1. <beans......>
  2. ......
  3. <bean id="bankService" class="footmark.spring.core.tx.declare.namespace.BankServiceImpl">
  4. <property name="bankDao" ref="bankDao"/>
  5. </bean>
  6. <tx:advice id="bankAdvice" transaction-manager="transactionManager">
  7. <tx:attributes>
  8. <tx:method name="transfer" propagation="REQUIRED"/>
  9. </tx:attributes>
  10. </tx:advice>
  11. <aop:config>
  12. <aop:pointcut id="bankPointcut" expression="execution(* *.transfer(..))"/>
  13. <aop:advisor advice-ref="bankAdvice" pointcut-ref="bankPointcut"/>
  14. </aop:config>
  15. ......
  16. </beans>

如果默认的事务属性就能满足要求,那么代码简化为:

  1. <beans......>
  2. ......
  3. <bean id="bankService"
  4. class="footmark.spring.core.tx.declare.namespace.BankServiceImpl">
  5. <property name="bankDao" ref="bankDao"/>
  6. </bean>
  7. <tx:advice id="bankAdvice" transaction-manager="transactionManager">
  8. <aop:config>
  9. <aop:pointcut id="bankPointcut" expression="execution(**.transfer(..))"/>
  10. <aop:advisor advice-ref="bankAdvice" pointcut-ref="bankPointcut"/>
  11. </aop:config>
  12. ......
  13. </beans>

由于使用了切点表达式,我们就不需要针对每一个业务类创建一个代理对象了。另外,如果配置的事务管理器 Bean 的名字取值为 “transactionManager”,则我们可以省略 <tx:advice> 的 transaction-manager 属性,因为该属性的默认值即为 “transactionManager”。

基于 @Transactional 的声明式事务管理

除了基于命名空间的事务配置方式,Spring 2.x 还引入了基于 Annotation 的方式,具体主要涉及@Transactional 标注。@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

  1. @Transactional(propagation = Propagation.REQUIRED)
  2. public boolean transfer(Long fromId Long toId double amount) {
  3. return bankDao.transfer(fromId toId amount);
  4. }

Spring 使用 BeanPostProcessor 来处理 Bean 中的标注,因此我们需要在配置文件中作如下声明来激活该后处理 Bean:

  1. <tx:annotation-driven transaction-manager="transactionManager"/>

与前面相似,transaction-manager 属性的默认值是 transactionManager,如果事务管理器 Bean 的名字即为该值,则可以省略该属性。

虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 小组建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

基于 <tx> 命名空间和基于 @Transactional 的事务声明方式各有优缺点。基于 <tx> 的方式,其优点是与切点表达式结合,功能强大。利用切点表达式,一个配置可以匹配多个方法,而基于 @Transactional 的方式必须在每一个需要使用事务的方法或者类上用 @Transactional 标注,尽管可能大多数事务的规则是一致的,但是对 @Transactional 而言,也无法重用,必须逐个指定。另一方面,基于 @Transactional 的方式使用起来非常简单明了,没有学习成本。开发人员可以根据需要,任选其中一种使用,甚至也可以根据需要混合使用这两种方式。

如果不是对遗留代码进行维护,则不建议再使用基于 TransactionInterceptor 以及基于TransactionProxyFactoryBean 的声明式事务管理方式,但是,学习这两种方式非常有利于对底层实现的理解。

虽然上面共列举了四种声明式事务管理方式,但是这样的划分只是为了便于理解,其实后台的实现方式是一样的,只是用户使用的方式不同而已。

前言

关于事务,简单来说,就是为了保证数据完整性而存在的一种工具,其主要有四大特性:原子性,一致性,隔离性和持久性。对于Spring事务,其最终还是在数据库层面实现的,而Spring只是以一种比较优雅的方式对其进行封装支持。本文首先会通过一个简单的示例来讲解Spring事务是如何使用的,然后会讲解Spring是如何解析xml中的标签,并对事务进行支持的。

使用示例

关于事务最简单的示例,就是其一致性,比如在整个事务执行过程中,如果任何一个位置报错了,那么都会导致事务回滚,回滚之后数据的状态将和事务执行之前完全一致。这里我们以用户数据为例,在插入用户数据的时候,如果程序报错了,那么插入的动作就会回滚。如下是用户的实体:

  1. public class User {
  2. private long id;
  3. private String name;
  4. private int age;
  5. // getter, setter...
  6. }

如下是模拟插入用户数据的业务代码:
接口:

  1. public interface UserService {
  2. void insert(User user);
  3. }

实现:

  1. @Service
  2. @Transactional
  3. public class UserServiceImpl implements UserService {
  4. @Autowired
  5. private JdbcTemplate jdbcTemplate;
  6. @Override
  7. public void insert(User user) {
  8. jdbcTemplate.update("insert into user (name, age) value (?, ?)",
  9. user.getName(), user.getAge());
  10. }
  11. }

在进行事务支持时,Spring只需要使用者在需要事务支持的bean上使用@Transactional注解即可,如果需要修改事务的隔离级别和传播特性的属性,则使用该注解中的属性进行指定。
这里默认的隔离级别与各个数据库一致,比如MySQL是Repeatable Read,而传播特性默认则为Propagation.REQUIRED,即只需要当前操作具有事务即可。如下是xml文件的配置:

  1. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
  2. <property name="url" value="jdbc:mysql://localhost/test?useUnicode=true"/>
  3. <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  4. <property name="username" value="****"/>
  5. <property name="password" value="******"/>
  6. </bean>
  7. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  8. <property name="dataSource" ref="dataSource"/>
  9. </bean>
  10. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  11. <property name="dataSource" ref="dataSource"/>
  12. </bean>
  13. <context:component-scan base-package="com.transaction"/>
  14. <tx:annotation-driven/>

上述数据库配置用户按照各自的设置进行配置即可。可以看到,这里对于数据库的配置,主要包括四个方面:

  • DataSource配置:设置当前应用所需要连接的数据库,包括链接,用户名,密码等
  • JdbcTemplate声明:封装了客户端调用数据库的方式,用户可以使用其他的方式,比如JpaRepositoryMybatis等等;
  • TransactionManager配置:指定了事务的管理方式,这里使用的是DataSourceTransactionManager,对于不同的链接方式,也可以进行不同的配置,比如对于JpaRepository使用JpaTransactionManager,对于Hibernate,使用HibernateTransactionManager
  • tx:annotation-driven:主要用于事务驱动,其会通过AOP的方式声明一个为事务支持的Advisor,通过该Advisor和事务的相关配置进行事务相关操作。

按照上述配置,我们的事务功能即配置完成,如下是我们的驱动类程序:

  1. public class TransactionApp {
  2. @Test
  3. public void testTransaction() {
  4. ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
  5. UserService userService = ac.getBean(UserService.class);
  6. User user = getUser(); //
  7. userService.insert(user);
  8. }
  9. private User getUser() {
  10. User user = new User();
  11. user.setName("Mary");
  12. user.setAge(27);
  13. return user;
  14. }
  15. }

运行上述程序之后,可以看到数据库中成功新增了一条数据。

这里如果我们将业务代码的插入语句之后手动抛出一个异常,那么,理论上插入语句是会回滚的。如下是修改后的service代码:

  1. @Service
  2. @Transactional
  3. public class UserServiceImpl implements UserService {
  4. @Autowired
  5. private JdbcTemplate jdbcTemplate;
  6. @Override
  7. public void insert(User user) {
  8. jdbcTemplate.update("insert into user (name, age) value (?, ?)",
  9. user.getName(), user.getAge());
  10. throw new RuntimeException(); //手动抛出异常
  11. }
  12. }

这里我们手动抛出了一个RuntimeException,再次运行上述程序之后我们发现数据库中是没有新增的数据的,这说明我们的事务在程序出错后是能够保证数据一致性的。

解析

关于事务的实现原理,我们首先讲解Spring是如何解析标签,并且封装相关bean的,后面我们会深入讲解Spring是如何封装数据库事务的

根据上面的示例,我们发现,其主要有三个部分:DataSourceTransactionManagertx:annotation-driven标签。这里前面两个部分主要是声明了两个bean,分别用于数据库连接的管理和事务的管理,而tx:annotation-driven才是Spring事务的驱动。
根据本人前面对Spring自定义标签的讲解(Spring自定义标签解析与实现),可以知道,这里tx:annotation-driven是一个自定义标签,我们根据其命名空间(www.springframework.org/schema/tx)在全局范围内搜索,可以找到其处理器指定文件spring.handlers,该文件内容如下:

  1. http\://www.springframework.org/schema/tx=
  2. org.springframework.transaction.config.TxNamespaceHandler

这里也就是说tx:annotation-driven标签的解析在TxNamespaceHandler中,我们继续打开该文件可以看到起init()方法如下:

  1. @Override
  2. public void init() {
  3. registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
  4. registerBeanDefinitionParser("annotation-driven",
  5. new AnnotationDrivenBeanDefinitionParser());
  6. registerBeanDefinitionParser("jta-transaction-manager",
  7. new JtaTransactionManagerBeanDefinitionParser());
  8. }

可以看到,这里的annotation-driven是在AnnotationDrivenBeanDefinitionParser中进行处理的,其parse()方法就是解析标签,并且注册相关bean的方法,如下是该方法的实现:

  1. public BeanDefinition parse(Element element, ParserContext parserContext) {
  2. // 注册事务相关的监听器,如果某个方法标注了TransactionalEventListener注解,
  3. // 那么该方法就是一个事务事件触发方法,即发生某种事务事件后,将会根据该注解的设置,回调指定
  4. // 类型的方法。常见的事务事件有:事务执行前和事务完成(包括报错后的完成)后的事件。
  5. registerTransactionalEventListenerFactory(parserContext);
  6. String mode = element.getAttribute("mode");
  7. // 获取当前事务驱动程序的模式,如果使用了aspectj模式,则会注册一个AnnotationTransactionAspect
  8. // 类型的bean,用户可以以aspectj的方式使用该bean对事务进行更多的配置
  9. if ("aspectj".equals(mode)) {
  10. registerTransactionAspect(element, parserContext);
  11. } else {
  12. // 一般使用的是当前这种方式,这种方式将会在Spring中注册三个bean,分别是
  13. // AnnotationTransactionAttributeSource,TransactionInterceptor
  14. // 和BeanFactoryTransactionAttributeSourceAdvisor,并通过Aop的方式实现事务
  15. AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
  16. }
  17. return null;
  18. }

可以看到,对于事务的驱动,这里主要做了两件事:
①注册事务相关的事件触发器,这些触发器由用户自行提供,在事务进行提交或事务完成时会触发相应的方法;
②判断当前事务驱动的模式,有默认模式和aspectj模式,对于aspectj模式,Spring会注册一个AnnotationTransactionAspect类型的bean,用于用户使用更亲近于aspectj的方式进行事务处理;对于默认模式,这里主要是声明了三个bean,最终通过Aop的方式进行事务切入。下面我们看一下Spring是如何注册这三个bean的,如下是AopAutoProxyConfigurer.configureAutoProxyCreator的源码:

  1. public static void configureAutoProxyCreator(Element element,
  2. ParserContext parserContext) {
  3. // 这个方法主要是在当前BeanFactory中注册InfrastructureAdvisorAutoProxyCreator这个
  4. // bean,这个bean继承了AbstractAdvisorAutoProxyCreator,也就是其实现原理与我们前面
  5. // 讲解的Spring Aop的实现原理几乎一致
  6. AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);
  7. // 这里的txAdvisorBeanName就是我们最终要注册的bean,其类型就是下面注册的
  8. // BeanFactoryTransactionAttributeSourceAdvisor,可以看到,其本质是一个
  9. // Advisor类型的对象,因而Spring Aop会将其作为一个切面织入到指定的bean中
  10. String txAdvisorBeanName = TransactionManagementConfigUtils
  11. .TRANSACTION_ADVISOR_BEAN_NAME;
  12. // 如果当前BeanFactory中已经存在了目标bean,则不进行注册
  13. if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) {
  14. Object eleSource = parserContext.extractSource(element);
  15. // 注册AnnotationTransactionAttributeSource,这个bean的主要作用是封装
  16. // @Transactional注解中声明的各个属性
  17. RootBeanDefinition sourceDef = new RootBeanDefinition(
  18. "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource");
  19. sourceDef.setSource(eleSource);
  20. sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
  21. String sourceName = parserContext.getReaderContext()
  22. .registerWithGeneratedName(sourceDef);
  23. // 注册TransactionInterceptor类型的bean,并且将上面的封装属性的bean设置为其一个属性。
  24. // 这个bean本质上是一个Advice(可查看其继承结构),Spring Aop使用Advisor封装实现切面
  25. // 逻辑织入所需的所有属性,但真正的切面逻辑却是保存在其Advice属性中的,也就是说这里的
  26. // TransactionInterceptor才是真正封装了事务切面逻辑的bean
  27. RootBeanDefinition interceptorDef =
  28. new RootBeanDefinition(TransactionInterceptor.class);
  29. interceptorDef.setSource(eleSource);
  30. interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
  31. registerTransactionManager(element, interceptorDef);
  32. interceptorDef.getPropertyValues().add("transactionAttributeSource",
  33. new RuntimeBeanReference(sourceName));
  34. String interceptorName = parserContext.getReaderContext()
  35. .registerWithGeneratedName(interceptorDef);
  36. // 注册BeanFactoryTransactionAttributeSourceAdvisor类型的bean,这个bean实现了
  37. // Advisor接口,实际上就是封装了当前需要织入的切面的所有所需的属性
  38. RootBeanDefinition advisorDef =
  39. new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class);
  40. advisorDef.setSource(eleSource);
  41. advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
  42. advisorDef.getPropertyValues().add("transactionAttributeSource",
  43. new RuntimeBeanReference(sourceName));
  44. advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
  45. if (element.hasAttribute("order")) {
  46. advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
  47. }
  48. parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef);
  49. // 将需要注册的bean封装到CompositeComponentDefinition中,并且进行注册
  50. CompositeComponentDefinition compositeDef =
  51. new CompositeComponentDefinition(element.getTagName(), eleSource);
  52. compositeDef.addNestedComponent(
  53. new BeanComponentDefinition(sourceDef, sourceName));
  54. compositeDef.addNestedComponent(
  55. new BeanComponentDefinition(interceptorDef, interceptorName));
  56. compositeDef.addNestedComponent(
  57. new BeanComponentDefinition(advisorDef, txAdvisorBeanName));
  58. parserContext.registerComponent(compositeDef);
  59. }
  60. }

如此,Spring事务相关的标签即解析完成,这里主要是声明了一个BeanFactoryTransactionAttributeSourceAdvisor类型的bean到BeanFactory中,其实际为Advisor类型,这也是Spring事务能够通过Aop实现事务的根本原因。

实现原理

关于Spring事务的实现原理,这里需要抓住的就是,其是使用Aop实现的,我们知道,Aop在进行解析的时候,最终会生成一个Adivsor对象,这个Advisor对象中封装了切面织入所需要的所有信息,其中就包括最重要的两个部分就是PointcutAdivce属性。这里Pointcut用于判断目标bean是否需要织入当前切面逻辑;Advice则封装了需要织入的切面逻辑。如下是这三个部分的简要关系图:

Spring事务 - 图3
同样的,对于Spring事务,其既然是使用Spring Aop实现的,那么也同样会有这三个成员。我们这里我们只看到了注册的AdvisorAdvice(即BeanFactoryTransactionAttributeSourceAdvisorTransactionInterceptor),那么Pointcut在哪里呢?这里我们查看BeanFactoryTransactionAttributeSourceAdvisor的源码可以发现,其内部声明了一个TransactionAttributeSourcePointcut类型的属性,并且直接在内部进行了实现,这就是我们需要找的Pointcut。这里这三个对象对应的关系如下:

Spring事务 - 图4
这样,用于实现Spring事务的AdvisorPointcut以及Advice都已经找到了。关于这三个类的具体作用,我们这里进行整体的上的讲解,后面我们将会深入其内部讲解其是如何进行bean的过滤以及事务逻辑的织入的。

  • BeanFactoryTransactionAttributeSourceAdvisor:封装了实现事务所需的所有属性,包括PointcutAdviceTransactionManager以及一些其他的在Transactional注解中声明的属性;
  • TransactionAttributeSourcePointcut:用于判断哪些bean需要织入当前的事务逻辑。这里可想而知,其判断的基本逻辑就是判断其方法或类声明上有没有使用@Transactional注解,如果使用了就是需要织入事务逻辑的bean;
  • TransactionInterceptor:这个bean本质上是一个Advice,其封装了当前需要织入目标bean的切面逻辑,也就是Spring事务是如果借助于数据库事务来实现对目标方法的环绕的。

来源:
微信公众号:《java基基》
文章:Spring 事务用法示例与实现原理