什么是事务处理事务是计算机应用中不可或缺的组件模型，它保证了用户操作的原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）和持久性（Durability）。关于事务最经典的示例莫过于信用卡转账：将用户 A 账户中的 500 元人民币转移到用户 B 的账户中，其操作流程如下：将 A 账户中的金额减少 500；将 B 账户中的金额增加 500。这两个操作必须保证 ACID 的事务属性：即要么全部成功，要么全部失败。假若没有事务保障，用户的账号金额将可能发生问题：假如第一步操作成功而第二步失败，那么用户 A 账户中的金额就将减少 500 元，而用户 B 的账号却没有任何增加（不翼而飞）；同样如果第一步出错而第二步成功，那么用户 A 的账户金额不变，而用户 B 的账号将增加 500 元（凭空而生）。上述任何一种错误都会产生严重的数据不一致问题，事务的缺失对于一个稳定的生产系统是不可接受的。J2EE 事务处理方式1. 本地事务本地事务紧密依赖于底层资源管理器（例如数据库连接），事务处理局限在当前事务资源内。此种事务处理方式不存在对应用服务器的依赖，因而部署灵活，但无法支持多数据源的分布式事务。在数据库连接中使用本地事务示例如下：清单 1. 本地事务处理实例public void transferAccount() { Connection conn = null; Statement stmt = null; try { conn = getDataSource().getConnection(); // 将自动提交设置为 false， // 若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交 conn.setAutoCommit(false); stmt = conn.createStatement(); // 将 A 账户中的金额减少 500 stmt.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmt.execute("update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 conn.commit(); // 事务提交：转账的两步操作同时成功 } catch (SQLException sqle) { try { // 发生异常，回滚在本事务中的操作 conn.rollback(); // 事务回滚：转账的两步操作完全撤销 stmt.close(); conn.close(); } catch (Exception ignore) { // 忽略关闭异常 } sqle.printStackTrace(); } }2. 分布式事务处理Java 事务编程接口（JTA：Java Transaction API）和 Java 事务服务（JTS：Java Transaction Service）为 J2EE 平台提供了分布式事务服务。分布式事务（Distributed Transaction）包括事务管理器（Transaction Manager）和一个或多个支持 XA 协议的资源管理器（Resource Manager）。我们可以将资源管理器看做任意类型的持久化数据存储；事务管理器承担着所有事务参与单元的协调与控制。JTA 事务有效地屏蔽了底层事务资源，使应用可以以透明的方式参与到事务处理中。但是与本地事务相比，XA 协议的系统开销大，在系统开发过程中应慎重考虑是否确实需要分布式事务。若确实需要分布式事务以协调多个事务资源，则应实现和配置所支持 XA 协议的事务资源，如 JMS、JDBC 数据库连接池等。使用 JTA 处理事务的示例如下（注意：connA 和 connB 是来自不同数据库的连接）：清单 2. JTA 事务处理public void transferAccount() { UserTransaction userTx = null; Connection connA = null; Statement stmtA = null; Connection connB = null; Statement stmtB = null; try { // 获得 Transaction 管理对象 userTx = (UserTransaction) getContext().lookup("java:comp/UserTransaction"); // 从数据库 A 中取得数据库连接 connA = getDataSourceA().getConnection(); // 从数据库 B 中取得数据库连接 connB = getDataSourceB().getConnection(); // 启动事务 userTx.begin(); // 将 A 账户中的金额减少 500 stmtA = connA.createStatement(); stmtA.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmtB = connB.createStatement(); stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 userTx.commit(); // 事务提交：转账的两步操作同时成功（数据库 A 和数据库 B 中的数据被同时更新） } catch (SQLException sqle) { try { // 发生异常，回滚在本事务中的操作 userTx.rollback(); // 事务回滚：转账的两步操作完全撤销 // (数据库 A 和数据库 B 中的数据更新被同时撤销） stmtA.close(); connA.close(); stmtB.close(); connB.close(); } catch (Exception ignore) { // 忽略关闭异常 } sqle.printStackTrace(); } catch (Exception ne) { ne.printStackTrace(); } }JTA 实现原理很多开发人员都会对 JTA 的内部工作机制感兴趣：我编写的代码没有任何与事务资源（如数据库连接）互动的代码，但是我的操作（数据库更新）却实实在在的被包含在了事务中，那 JTA 究竟是通过何种方式来实现这种透明性的呢？要理解 JTA 的实现原理，首先需要了解其架构：它包括事务管理器（Transaction Manager）和一个或多个支持 XA 协议的资源管理器（Resource Manager）两部分。我们可以将资源管理器看做任意类型的持久化数据存储；事务管理器则承担着所有事务参与单元的协调与控制。根据所面向对象的不同，我们可以将 JTA 的事务管理器和资源管理器理解为两个方面：面向开发人员的使用接口（事务管理器）和面向服务提供商的实现接口（资源管理器）。其中开发接口的主要部分即为上述示例中引用的 UserTransaction 对象，开发人员通过此接口在信息系统中实现分布式事务；而实现接口则用来规范提供商（如数据库连接提供商）所提供的事务服务，它约定了事务的资源管理功能，使得 JTA 可以在异构事务资源之间执行协同沟通。以数据库为例，IBM 公司提供了实现分布式事务的数据库驱动程序，Oracle 也提供了实现分布式事务的数据库驱动程序。在同时使用 DB2 和 Oracle 两种数据库连接时，JTA 即可以根据约定的接口协调这两种事务资源从而实现分布式事务。正是基于统一规范的不同实现使得 JTA 可以协调与控制不同数据库或者 JMS 厂商的事务资源，其架构如下图所示：图 1. JTA 体系架构点击查看大图开发人员使用开发人员接口，实现应用程序对全局事务的支持；各提供商（数据库，JMS 等）依据提供商接口的规范提供事务资源管理功能；事务管理器（TransactionManager）将应用对分布式事务的使用映射到实际的事务资源并在事务资源间进行协调与控制。下面，本文将对包括 UserTransaction、Transaction 和 TransactionManager 在内的三个主要接口以及其定义的方法进行介绍。面向开发人员的接口：UserTransaction面向开发人员的接口为 UserTransaction（使用方法如上例所示），开发人员通常只使用此接口实现 JTA 事务管理，其定义了如下的方法：begin() - 开始一个分布式事务（在后台 TransactionManager 会创建一个 Transaction 事务对象并把此对象通过 ThreadLocal 关联到当前线程上）commit() - 提交事务（在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务提交）rollback() - 回滚事务（在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务回滚）getStatus() - 返回关联到当前线程的分布式事务的状态（Status 对象里边定义了所有的事务状态，感兴趣的读者可以参考 API 文档）setRollbackOnly() - 标识关联到当前线程的分布式事务将被回滚面向提供商的实现接口：Transaction 与 TransactionManager面向提供商的实现接口主要涉及到 TransactionManager 和 Transaction 两个对象。Transaction 代表了一个物理意义上的事务，在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象（标志着事务的开始）并把此对象通过 ThreadLocal 关联到当前线程。UserTransaction 接口中的 commit()、rollback()，getStatus() 等方法都将最终委托给 Transaction 类的对应方法执行。Transaction 接口定义了如下的方法：commit() - 协调不同的事务资源共同完成事务的提交rollback() - 协调不同的事务资源共同完成事务的回滚setRollbackOnly() - 标识关联到当前线程的分布式事务将被回滚getStatus() - 返回关联到当前线程的分布式事务的状态enListResource(XAResource xaRes, int flag) - 将事务资源加入到当前的事务中（在上述示例中，在对数据库 A 操作时其所代表的事务资源将被关联到当前事务中，同样，在对数据库 B 操作时其所代表的事务资源也将被关联到当前事务中）delistResource(XAResource xaRes, int flag) - 将事务资源从当前事务中删除registerSynchronization(Synchronization sync) - 回调接口。Hibernate 等 ORM 工具都有自己的事务控制机制来保证事务，但同时它们还需要一种回调机制以便在事务完成时得到通知从而触发一些处理工作，如清除缓存等。这就涉及到了 Transaction 的回调接口 registerSynchronization。工具可以通过此接口将回调程序注入到事务中，当事务成功提交后，回调程序将被激活。TransactionManager 本身并不承担实际的事务处理功能，它更多的是充当用户接口和实现接口之间的桥梁。下面列出了 TransactionManager 中定义的方法，可以看到此接口中的大部分事务方法与 UserTransaction 和 Transaction 相同。在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象（标志着事务的开始）并把此对象通过 ThreadLocal 关联到当前线程上；同样 UserTransaction.commit() 会调用 TransactionManager.commit()，方法将从当前线程下取出事务对象 Transaction 并把此对象所代表的事务提交，即调用 Transaction.commit()。begin() - 开始事务commit() - 提交事务rollback() - 回滚事务getStatus() - 返回当前事务状态setRollbackOnly()getTransaction() - 返回关联到当前线程的事务setTransactionTimeout(int seconds) - 设置事务超时时间resume(Transaction tobj) - 继续当前线程关联的事务suspend() - 挂起当前线程关联的事务在系统开发过程中会遇到需要将事务资源暂时排除的操作，此时就需要调用 suspend() 方法将当前的事务挂起：在此方法后面所做的任何操作将不会被包括在事务中，在非事务性操作完成后调用 resume() 以继续事务（注：要进行此操作需要获得 TransactionManager 对象，其获得方式在不同的 J2EE 应用服务器上是不一样的）。下面将通过具体的代码向读者介绍 JTA 实现原理。下图列出了示例实现中涉及到的 Java 类，其中 UserTransactionImpl 实现了 UserTransaction 接口，TransactionManagerImpl 实现了 TransactionManager 接口，TransactionImpl 实现了 Transaction 接口。图 2. JTA 实现类图点击查看大图清单 3. 开始事务 - UserTransactionImpl implements UserTransactionpublic void begin() throws NotSupportedException, SystemException { // 将开始事务的操作委托给 TransactionManagerImpl TransactionManagerImpl.singleton().begin(); }清单 4. 开始事务 - TransactionManagerImpl implements TransactionManager// 此处 transactionHolder 用于将 Transaction 所代表的事务对象关联到线程上 private static ThreadLocal<TransactionImpl> transactionHolder = new ThreadLocal<TransactionImpl>(); // TransactionManager 必须维护一个全局对象，因此使用单实例模式实现 private static TransactionManagerImpl singleton = new TransactionManagerImpl(); private TransactionManagerImpl() { } public static TransactionManagerImpl singleton() { return singleton; } public void begin() throws NotSupportedException, SystemException { // XidImpl 实现了 Xid 接口，其作用是唯一标识一个事务 XidImpl xid = new XidImpl(); // 创建事务对象，并将对象关联到线程 TransactionImpl tx = new TransactionImpl(xid); transactionHolder.set(tx); }现在我们就可以理解 Transaction 接口上没有定义 begin 方法的原因了：Transaction 对象本身就代表了一个事务，在它被创建的时候就表明事务已经开始，因此也就不需要额外定义 begin() 方法了。清单 5. 提交事务 - UserTransactionImpl implements UserTransactionpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 检查是否是 Roll back only 事务，如果是回滚事务 if (rollBackOnly) { rollback(); return; } else { // 将提交事务的操作委托给 TransactionManagerImpl TransactionManagerImpl.singleton().commit(); } }清单 6. 提交事务 - TransactionManagerImpl implements TransactionManagerpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 取得当前事务所关联的事务并通过其 commit 方法提交 TransactionImpl tx = transactionHolder.get(); tx.commit(); }同理，rollback、getStatus、setRollbackOnly 等方法也采用了与 commit() 相同的方式实现。UserTransaction 对象不会对事务进行任何控制，所有的事务方法都是通过 TransactionManager 传递到实际的事务资源即 Transaction 对象上。上述示例演示了 JTA 事务的处理过程，下面将为您展示事务资源（数据库连接，JMS）是如何以透明的方式加入到 JTA 事务中的。首先需要明确的一点是，在 JTA 事务代码中获得的数据库源（DataSource）必须是支持分布式事务的。在如下的代码示例中，尽管所有的数据库操作都被包含在了 JTA 事务中，但是因为 MySql 的数据库连接是通过本地方式获得的，对 MySql 的任何更新将不会被自动包含在全局事务中。清单 7. JTA 事务处理（混合资源示例）public void transferAccount() { UserTransaction userTx = null; Connection mySqlConnection = null; Statement mySqlStat = null; Connection connB = null; Statement stmtB = null; try { // 获得 Transaction 管理对象 userTx = (UserTransaction) getContext().lookup("java:comp/UserTransaction"); // 以本地方式获得 mySql 数据库连接 mySqlConnection = DriverManager.getConnection("localhost:1111"); // 从数据库 B 中取得数据库连接，getDataSourceB 返回应用服务器的数据源 connB = getDataSourceB().getConnection(); // 启动事务 userTx.begin(); // 将 A 账户中的金额减少 500 // mySqlConnection 是从本地获得的数据库连接，不会被包含在全局事务中 mySqlStat = mySqlConnection.createStatement(); mySqlStat.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // connB 是从应用服务器得的数据库连接，会被包含在全局事务中 stmtB = connB.createStatement(); stmtB.execute("update t_account set amount = amount + 500 where account_id = 'B'"); // 事务提交：connB 的操作被提交，mySqlConnection 的操作不会被提交 userTx.commit(); } catch (SQLException sqle) { // 处理异常代码 } catch (Exception ne) { ne.printStackTrace(); } }为什么必须从支持事务的数据源中获得的数据库连接才支持分布式事务呢？其实支持事务的数据源与普通的数据源是不同的，它实现了额外的 XADataSource 接口。我们可以简单的将 XADataSource 理解为普通的数据源（继承了 java.sql.PooledConnection），只是它为支持分布式事务而增加了 getXAResource 方法。另外，由 XADataSource 返回的数据库连接与普通连接也是不同的，此连接除了实现 java.sql.Connection 定义的所有功能之外还实现了 XAConnection 接口。我们可以把 XAConnection 理解为普通的数据库连接，它支持所有 JDBC 规范的数据库操作，不同之处在于 XAConnection 增加了对分布式事务的支持。通过下面的类图读者可以对这几个接口的关系有所了解：图 3. 事务资源类图点击查看大图应用程序从支持分布式事务的数据源获得的数据库连接是 XAConnection 接口的实现，而由此数据库连接创建的会话（Statement）也为了支持分布式事务而增加了功能，如下代码所示：清单 8. JTA 事务资源处理public void transferAccount() { UserTransaction userTx = null; Connection conn = null; Statement stmt = null; try { // 获得 Transaction 管理对象 userTx = (UserTransaction) getContext().lookup("java:comp/UserTransaction"); // 从数据库中取得数据库连接，getDataSourceB 返回支持分布式事务的数据源 conn = getDataSourceB().getConnection(); // 会话 stmt 已经为支持分布式事务进行了功能增强 stmt = conn.createStatement(); // 启动事务 userTx.begin(); stmt.execute("update t_account ... where account_id = 'A'"); userTx.commit(); } catch (SQLException sqle) { // 处理异常代码 } catch (Exception ne) { ne.printStackTrace(); } }我们来看一下由 XAConnection 数据库连接创建的会话（Statement）部分的代码实现（不同的 JTA 提供商会有不同的实现方式，此处代码示例只是向您演示事务资源是如何被自动加入到事务中）。我们以会话对象的 execute 方法为例，通过在方法开始部分增加对 associateWithTransactionIfNecessary 方法的调用，即可以保证在 JTA 事务期间，对任何数据库连接的操作都会被透明的加入到事务中。清单 9. 将事务资源自动关联到事务对象 - XAStatement implements Statementpublic void execute(String sql) { // 对于每次数据库操作都检查此会话所在的数据库连接是否已经被加入到事务中 associateWithTransactionIfNecessary(); try { // 处理数据库操作的代码 .... } catch (SQLException sqle) { // 处理异常代码 } catch (Exception ne) { ne.printStackTrace(); } } public void associateWithTransactionIfNecessary() { // 获得 TransactionManager TransactionManager tm = getTransactionManager(); Transaction tx = tm.getTransaction(); // 检查当前线程是否有分布式事务 if (tx != null) { // 在分布式事务内，通过 tx 对象判断当前数据连接是否已经被包含在事务中， // 如果不是那么将此连接加入到事务中 Connection conn = this.getConnection(); // tx.hasCurrentResource, xaConn.getDataSource() 不是标准的 JTA // 接口方法，是为了实现分布式事务而增加的自定义方法 if (!tx.hasCurrentResource(conn)) { XAConnection xaConn = (XAConnection) conn; XADataSource xaSource = xaConn.getDataSource(); // 调用 Transaction 的接口方法，将数据库事务资源加入到当前事务中 tx.enListResource(xaSource.getXAResource(), 1); } } }XAResource 与 XidXAResource 是 Distributed Transaction Processing: The XA Specification 标准的 Java 实现，它是对底层事务资源的抽象，定义了分布式事务处理过程中事务管理器和资源管理器之间的协议，各事务资源提供商（如 JDBC 驱动，JMS）将提供此接口的实现。使用此接口，开发人员可以通过自己的编程实现分布式事务处理，但这些通常都是由应用服务器实现的（服务器自带实现更加高效，稳定）。为了说明，我们将举例说明它的使用方式。在使用分布式事务之前，为了区分事务使之不发生混淆，必须实现一个 Xid 类用来标识事务，可以把 Xid 想象成事务的一个标志符，每次在新事务创建时都会为事务分配一个 Xid，Xid 包含三个元素：formatID、gtrid（全局事务标识符）和 bqual（分支修饰词标识符）。formatID 通常是零，这意味着你将使用 OSI CCR（Open Systems Interconnection Commitment, Concurrency 和 Recovery 标准）来命名；如果你要使用另外一种格式，那么 formatID 应该大于零，-1 值意味着 Xid 为无效。gtrid 和 bqual 分别包含 64 个字节二进制码来分别标识全局事务和分支事务，唯一的要求是 gtrid 和 bqual 必须是全局唯一的。XAResource 接口中主要定义了如下方法：commit() - 提交事务isSameRM(XAResource xares) - 检查当前的 XAResource 与参数是否同一事务资源prepare() - 通知资源管理器准备事务的提交工作rollback() - 通知资源管理器回滚事务在事务被提交时，Transaction 对象会收集所有被当前事务包含的 XAResource 资源，然后调用资源的提交方法，如下代码所示：清单 10. 提交事务 - TransactionImpl implements Transactionpublic void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 得到当前事务中的所有事务资源 List<XAResource> list = getAllEnlistedResources(); // 通知所有的事务资源管理器，准备提交事务 // 对于生产级别的实现，此处需要进行额外处理以处理某些资源准备过程中出现的异常 for (XAResource xa : list) { xa.prepare(); } // 所有事务性资源，提交事务 for (XAResource xa : list) { xa.commit(); } }结束语通过如上介绍相信读者对 JTA 的原理已经有所了解，本文中的示例代码都是理想情况下的假设实现。一款完善成熟的 JTA 事务实现需要考虑与处理的细节非常多，如性能（提交事务的时候使用多线程方式并发提交事务）、容错（网络，系统异常）等，其成熟也需要经过较长时间的积累。感兴趣的读者可以阅读一些开源 JTA 实现以进一步深入学习。说明：本文基于经典的 J2EE 与 JTA 规范编写。随着 Java EE 移交至 Eclipse 基金会并更名为 Jakarta EE，相关包名（如 javax.transaction 变更为 jakarta.transaction）及实现细节可能有所调整。文中部分外部链接（如 IBM developerWorks）可能已归档或跳转，请参考官方最新文档获取时效信息。相关主题Java 官方网站"Java Transaction API Specification"提供了详细的 JTA 信息、包括版本，规范等，感兴趣的读者可以参考这篇文章"Java Transaction API 概述"( 编程中国 ) 详细描述了 XA 资源的使用原理，感兴趣的读者可以参照学习"JOTM"是一款开源的 JTA 实现，读者可以参照其源代码深入学习developerWorks Java 技术专区：这里有数百篇关于 Java 编程各个方面的文章。
全面分析 Spring 的编程式事务管理及声明式事务管理
开始之前关于本教程本教程将深入讲解 Spring 简单而强大的事务管理功能，包括编程式事务和声明式事务。通过对本教程的学习，您将能够理解 Spring 事务管理的本质，并灵活运用之。先决条件本教程假定您已经掌握了 Java 基础知识，并对 Spring 有一定了解。您还需要具备基本的事务管理知识，比如事务的定义、隔离级别的概念等。本文将直接使用这些概念而不做详细解释。另外，您最好掌握数据库的基础知识，虽然这不是必须的。系统需求要试验这份教程中的工具和示例，硬件配置需求为：至少带有 512MB 内存（推荐 1GB）的系统。需要安装以下软件：Sun JDK 5.0 或更新版本，或 IBM Developer Kit for the Java 5 platform 版本。Spring Framework 2.5。本教程附带的示例代码已经在 Spring 2.5.6 上测试过。MySQL 5.0 或更新版本。Spring 事务属性分析事务管理对于企业应用而言至关重要。它保证了用户的每一次操作都是可靠的，即便出现了异常的访问情况，也不至于破坏后台数据的完整性。就像银行的自助取款机，通常都能正常为客户服务，但是也难免遇到操作过程中机器突然出故障的情况。此时，事务就必须确保出故障前对账户的操作不生效，就像用户刚才完全没有使用过取款机一样，以保证用户和银行的利益都不受损失。在 Spring 中，事务是通过 TransactionDefinition 接口来定义的。该接口包含与事务属性有关的方法，具体如清单 1 所示：清单 1. TransactionDefinition 接口中定义的主要方法public interface TransactionDefinition { int getIsolationLevel(); int getPropagationBehavior(); int getTimeout(); boolean isReadOnly(); }也许你会奇怪，为什么接口只提供了获取属性的方法，而没有提供相关设置属性的方法。其实道理很简单，事务属性的设置完全是程序员控制的，因此程序员可以自定义任何设置属性的方法，而且保存属性的字段也没有任何要求。唯一的要求是：Spring 进行事务操作的时候，通过调用以上接口提供的方法必须能够返回事务相关的属性取值。事务隔离级别隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量。在典型的应用程序中，多个事务并发运行，经常会操作相同的数据来完成各自的任务。并发虽然是必须的，但可能会导致以下问题：脏读（Dirty Read）： 当一个事务正在访问数据并且对数据进行了修改，而这种修改还没有提交到数据库中，这时另外一个事务也访问了这个数据，然后使用了这个数据。因为这个数据是还没有提交的数据，那么另外一个事务读到的这个数据是“脏数据”，依据“脏数据”所做的操作可能是不正确的。丢失修改（Lost Update）： 指在一个事务读取一个数据时，另外一个事务也访问了该数据，那么在第一个事务中修改了这个数据后，第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失，因此称为丢失修改。例如： 事务 1 读取某表中的数据 A=20，事务 2 也读取 A=20，事务 1 修改 A=A-1，事务 2 也修改 A=A-1，最终结果 A=19，事务 1 的修改被丢失。不可重复读（Unrepeatable Read）： 指在一个事务内多次读同一数据。在这个事务还没有结束时，另一个事务也访问该数据。那么，在第一个事务中的两次读数据之间，由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况。幻读（Phantom Read）： 幻读与不可重复读类似。它发生在一个事务（T1）读取了几行数据，接着另一个并发事务（T2）插入了一些数据时。在随后的查询中，第一个事务（T1）就会发现多了一些原本不存在的记录，就好像发生了幻觉一样。不可重复读和幻读的区别：不可重复读的重点是修改，幻读的重点在于新增或者删除。例 1（不可重复读）： 事务 1 中的 A 先生读取自己的工资为 1000 的操作还没完成，事务 2 中的 B 先生就修改了 A 的工资为 2000，导致 A 再读自己的工资时工资变为 2000。例 2（幻读）： 假某工资单表中工资大于 3000 的有 4 人，事务 1 读取了所有工资大于 3000 的人，共查到 4 条记录，这时事务 2 又插入了一条工资大于 3000 的记录，事务 1 再次读取时查到的记录就变为了 5 条。TransactionDefinition 接口中定义的五个隔离级别常量如下：TransactionDefinition.ISOLATION_DEFAULT： 这是默认值，表示使用底层数据库的默认隔离级别。对大部分数据库而言，通常这值就是 TransactionDefinition.ISOLATION_READ_COMMITTED。注意： MySQL 默认采用的 REPEATABLE_READ 隔离级别，Oracle 默认采用的 READ_COMMITTED 隔离级别。TransactionDefinition.ISOLATION_READ_UNCOMMITTED： 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读，因此很少使用该隔离级别。可能会导致脏读、幻读或不可重复读。TransactionDefinition.ISOLATION_READ_COMMITTED： 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读，这也是大多数情况下的推荐值。可以阻止脏读，但是幻读或不可重复读仍有可能发生。TransactionDefinition.ISOLATION_REPEATABLE_READ： 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询，并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询，这些新增的记录也会被忽略。可以阻止脏读和不可重复读，但幻读仍有可能发生。TransactionDefinition.ISOLATION_SERIALIZABLE： 所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰。该级别可以防止脏读、不可重复读以及幻读。 但是这将严重影响程序的性能，通常情况下也不会用到该级别。事务传播行为所谓事务的传播行为是指，如果在开始当前事务之前，一个事务上下文已经存在，此时有若干选项可以指定一个事务性方法的执行行为。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量：TransactionDefinition.PROPAGATION_REQUIRED： 如果当前存在事务，则加入该事务；如果当前没有事务，则创建一个新的事务。TransactionDefinition.PROPAGATION_REQUIRES_NEW： 创建一个新的事务，如果当前存在事务，则把当前事务挂起。TransactionDefinition.PROPAGATION_SUPPORTS： 如果当前存在事务，则加入该事务；如果当前没有事务，则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED： 以非事务方式运行，如果当前存在事务，则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER： 以非事务方式运行，如果当前存在事务，则抛出异常。TransactionDefinition.PROPAGATION_MANDATORY： 如果当前存在事务，则加入该事务；如果当前没有事务，则抛出异常。TransactionDefinition.PROPAGATION_NESTED： 如果当前存在事务，则创建一个事务作为当前事务的嵌套事务来运行；如果当前没有事务，则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。这里需要指出的是，前面的六种事务传播行为是 Spring 从 EJB 中引入的，它们共享相同的概念。而 PROPAGATION_NESTED 是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中（如果存在外部事务的话），此时，内嵌事务并不是一个独立的事务，它依赖于外部事务的存在。只有通过外部的事务提交，才能引起内部事务的提交，嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点（SavePoint）的概念，那嵌套事务就很容易理解了。其实嵌套的子事务就是保存点的一个应用，一个事务中可以包括多个保存点，每一个嵌套子事务。另外，外部事务的回滚也会导致嵌套子事务的回滚。事务超时所谓事务超时，就是指一个事务所允许执行的最长时间。如果超过该时间限制但事务还没有完成，则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间，其单位是秒。事务的只读属性事务的只读属性是指，对事务性资源进行只读操作或者是读写操作。所谓事务性资源就是指那些被事务管理的资源，比如数据源、JMS 资源，以及自定义的事务性资源等等。如果确定只对事务性资源进行只读操作，那么我们可以将事务标志为只读的，以提高事务处理的性能。在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。事务的回滚规则通常情况下，如果在事务中抛出了未检查异常（继承自 RuntimeException 的异常），则默认将回滚事务。如果没有抛出任何异常，或者抛出了已检查异常，则仍然提交事务。这通常也是大多数开发者希望的处理方式，也是 EJB 中的默认处理方式。但是，我们可以根据需要人为控制事务在抛出某些未检查异常时仍然提交事务，或者在抛出某些已检查异常时回滚事务。Spring 事务管理 API 分析Spring 框架中，涉及到事务管理的 API 大约有 100 个左右，其中最重要的有三个：TransactionDefinition、PlatformTransactionManager、TransactionStatus。所谓事务管理，其实就是“按照给定的事务规则来执行提交或者回滚操作”。“给定的事务规则”就是用 TransactionDefinition 表示的，“按照……来执行提交或者回滚操作”便是用 PlatformTransactionManager 来表示，而 TransactionStatus 用于表示一个运行着的事务的状态。打一个不恰当的比喻，TransactionDefinition 与 TransactionStatus 的关系就像程序和进程的关系。TransactionDefinition 接口该接口在前面已经介绍过，它用于定义一个事务。它包含了事务的静态属性，比如：事务传播行为、超时时间等等。Spring 为我们提供了一个默认的实现类：DefaultTransactionDefinition，该类适用于大多数情况。如果该类不能满足需求，可以通过实现 TransactionDefinition 接口来实现自己的事务定义。PlatformTransactionManager 接口PlatformTransactionManager 用于执行具体的事务操作。接口定义如清单 2 所示：清单 2. PlatformTransactionManager 接口中定义的主要方法public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }根据底层所使用的不同的持久化 API 或框架，PlatformTransactionManager 的主要实现类大致如下：DataSourceTransactionManager： 适用于使用 JDBC 和 iBatis 进行数据持久化操作的情况。HibernateTransactionManager： 适用于使用 Hibernate 进行数据持久化操作的情况。JpaTransactionManager： 适用于使用 JPA 进行数据持久化操作的情况。另外还有 JtaTransactionManager、JdoTransactionManager、JmsTransactionManager 等等。如果我们使用 JTA 进行事务管理，我们可以通过 JNDI 和 Spring 的 JtaTransactionManager 来获取一个容器管理的 DataSource。JtaTransactionManager 不需要知道 DataSource 和其他特定的资源，因为它将使用容器提供的全局事务管理。而对于其他事务管理器，比如 DataSourceTransactionManager，在定义时需要提供底层的数据源作为其属性，也就是 DataSource。与 HibernateTransactionManager 对应的是 SessionFactory，与 JpaTransactionManager 对应的是 EntityManagerFactory 等等。TransactionStatus 接口PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象。返回的 TransactionStatus 对象可能代表一个新的或已经存在的事务（如果在当前调用堆栈有一个符合条件的事务）。TransactionStatus 接口提供了一个简单的控制事务执行和查询事务状态的方法。该接口定义如清单 3 所示：清单 3. TransactionStatus 接口中定义的主要方法public interface TransactionStatus { boolean isNewTransaction(); void setRollbackOnly(); boolean isRollbackOnly(); }编程式事务管理Spring 的编程式事务管理概述在 Spring 出现以前，编程式事务管理对基于 POJO 的应用来说是唯一选择。用过 Hibernate 的人都知道，我们需要在代码中显式调用 beginTransaction()、commit()、rollback() 等事务管理相关的方法，这就是编程式事务管理。通过 Spring 提供的事务管理 API，我们可以在代码中灵活控制事务的执行。在底层，Spring 仍然将事务操作委托给底层的持久化框架来执行。基于底层 API 的编程式事务管理根据 PlatformTransactionManager、TransactionDefinition 和 TransactionStatus 三个核心接口，我们完全可以通过编程的方式来进行事务管理。示例代码如清单 4 所示：清单 4. 基于底层 API 的事务管理示例代码public class BankServiceImpl implements BankService { private BankDao bankDao; private TransactionDefinition txDefinition; private PlatformTransactionManager txManager; // ... 省略 setter 方法 public boolean transfer(Long fromId, Long toId, double amount) { TransactionStatus txStatus = txManager.getTransaction(txDefinition); boolean result = false; try { result = bankDao.transfer(fromId, toId, amount); txManager.commit(txStatus); } catch (Exception e) { result = false; txManager.rollback(txStatus); System.out.println("Transfer Error!"); } return result; } }相应的配置文件如清单 5 所示：清单 5. 基于底层 API 的事务管理示例配置文件<bean id="bankService" class="footmark.spring.core.tx.programmatic.origin.BankServiceImpl"> <property name="bankDao" ref="bankDao"/> <property name="txManager" ref="transactionManager"/> <property name="txDefinition"> <bean class="org.springframework.transaction.support.DefaultTransactionDefinition"> <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/> </bean> </property> </bean>如上所示，我们在类中增加了两个属性：一个是 TransactionDefinition 类型的属性，它用于定义一个事务；另一个是 PlatformTransactionManager 类型的属性，用于执行事务管理操作。如果方法需要实施事务管理，我们首先需要在方法开始执行前启动一个事务，调用 PlatformTransactionManager.getTransaction(...) 方法便可启动一个事务。创建并启动了事务之后，便可以开始编写业务逻辑代码，然后在适当的地方执行事务的提交或者回滚。基于 TransactionTemplate 的编程式事务管理通过前面的示例可以发现，这种事务管理方式很容易理解，但令人头疼的是，事务管理的代码散落在业务逻辑代码中，破坏了原有代码的条理性，并且每一个业务方法都包含了类似的启动事务、提交/回滚事务的样板代码。幸好，Spring 也意识到了这些，并提供了简化的方法，这就是 Spring 在数据访问层非常常见的模板回调模式。如清单 6 所示：清单 6. 基于 TransactionTemplate 的事务管理示例代码public class BankServiceImpl implements BankService { private BankDao bankDao; private TransactionTemplate transactionTemplate; // ... 省略 setter 方法 public boolean transfer(final Long fromId, final Long toId, final double amount) { return (Boolean) transactionTemplate.execute(new TransactionCallback() { public Object doInTransaction(TransactionStatus status) { Object result; try { result = bankDao.transfer(fromId, toId, amount); } catch (Exception e) { status.setRollbackOnly(); result = false; System.out.println("Transfer Error!"); } return result; } }); } }相应的 XML 配置如下：清单 7. 基于 TransactionTemplate 的事务管理示例配置文件<bean id="bankService" class="footmark.spring.core.tx.programmatic.template.BankServiceImpl"> <property name="bankDao" ref="bankDao"/> <property name="transactionTemplate" ref="transactionTemplate"/> </bean>TransactionTemplate 的 execute() 方法有一个 TransactionCallback 类型的参数，该接口中定义了一个 doInTransaction() 方法，通常我们以匿名内部类的方式实现 TransactionCallback 接口，并在其 doInTransaction() 方法中书写业务逻辑代码。这里可以使用默认的事务提交和回滚规则，这样在业务代码中就不需要显式调用任何事务管理的 API。doInTransaction() 方法有一个 TransactionStatus 类型的参数，我们可以在方法的任何位置调用该参数的 setRollbackOnly() 方法将事务标识为回滚的，以执行事务回滚。根据默认规则，如果在执行回调方法的过程中抛出了未检查异常，或者显式调用了 TransactionStatus.setRollbackOnly() 方法，则回滚事务；如果事务执行完成或者抛出了 checked 类型的异常，则提交事务。TransactionCallback 接口有一个子接口 TransactionCallbackWithoutResult，该接口中定义了一个 doInTransactionWithoutResult() 方法，TransactionCallbackWithoutResult 接口主要用于事务过程中不需要返回值的情况。当然，对于不需要返回值的情况，我们仍然可以使用 TransactionCallback 接口，并在方法中返回任意值即可。声明式事务管理Spring 的声明式事务管理概述Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截，然后在目标方法开始之前创建或者加入一个事务，在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务，这样就不需要在业务逻辑代码中掺杂事务管理的代码，只需在配置文件中做相关的事务规则声明（或通过等价的基于标注的方式），便可以将事务规则应用到业务逻辑中。因为事务管理本身就是一个典型的横切逻辑，正是 AOP 的用武之地。Spring 开发团队也意识到了这一点，为声明式事务提供了简单而强大的支持。声明式事务管理曾经是 EJB 引以为傲的一个亮点，如今 Spring 让 POJO 在事务管理方面也拥有了和 EJB 一样的待遇，让开发人员在 EJB 容器之外也用上了强大的声明式事务管理功能，这主要得益于 Spring 依赖注入容器和 Spring AOP 的支持。依赖注入容器为声明式事务管理提供了基础设施，使得 Bean 对于 Spring 框架而言是可管理的；而 Spring AOP 则是声明式事务管理的直接实现者，这一点通过清单 8 可以看出来。通常情况下，笔者强烈建议在开发中使用声明式事务，不仅因为其简单，更主要是因为这样使得纯业务代码不被污染，极大方便后期的代码维护。和编程式事务相比，声明式事务唯一不足的地方是，后者的最细粒度只能作用到方法级别，无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求，也存在很多变通的方法，比如，可以将需要进行事务管理的代码块独立为方法等等。下面就来看看 Spring 为我们提供的声明式事务管理功能。基于 TransactionInterceptor 的声明式事务管理最初，Spring 提供了 TransactionInterceptor 类来实施声明式事务管理功能。先看清单 8 的配置文件：清单 8. 基于 TransactionInterceptor 的事务管理示例配置文件<beans...> ...... <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="transfer">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="bankServiceTarget" class="footmark.spring.core.tx.declare.origin.BankServiceImpl"> <property name="bankDao" ref="bankDao"/> </bean> <bean id="bankService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="bankServiceTarget"/> <property name="interceptorNames"> <idref bean="transactionInterceptor"/> </property> </bean> ...... </beans>首先，我们配置了一个 TransactionInterceptor 来定义相关的事务规则，它有两个主要的属性：一个是 transactionManager，用来指定一个事务管理器，并将具体事务相关的操作委托给它；另一个是 Properties 类型的 transactionAttributes 属性，它主要用来定义事务规则，该属性的每一个键值对中，键指定的是方法名，方法名可以使用通配符，而值就表示相应方法的所应用的事务属性。指定事务属性的取值有较复杂的规则，这在 Spring 中算得上是一件让人头疼的事。具体的书写规则如下：传播行为 [,隔离级别] [,只读属性] [,超时属性] [不影响提交的异常] [,导致回滚的异常]传播行为是唯一必须设置的属性，其他都可以忽略，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"等等。以下是两个示例：<property name="*Service"> PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,TIMEOUT_20, +AbcException,+DefException,-HijException </property>以上表达式表示，针对所有方法名以 Service 结尾的方法，使用 PROPAGATION_REQUIRED 事务传播行为，事务的隔离级别是 ISOLATION_READ_COMMITTED，超时时间为 20 秒，当事务抛出 AbcException 或者 DefException 类型的异常，则仍然提交，当抛出 HijException 类型的异常时必须回滚事务。这里没有指定"readOnly"，表示事务不是只读的。<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 的配置合二为一。如清单 9 所示：清单 9. 基于 TransactionProxyFactoryBean 的事务管理示例配置文件<beans......> ...... <bean id="bankServiceTarget" class="footmark.spring.core.tx.declare.classic.BankServiceImpl"> <property name="bankDao" ref="bankDao"/> </bean> <bean id="bankService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="target" ref="bankServiceTarget"/> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="transfer">PROPAGATION_REQUIRED</prop> </props> </property> </bean> ...... </beans>如此一来，配置文件与先前相比简化了很多。我们把这种配置方式称为 Spring 经典的声明式事务管理。相信在早期使用 Spring 的开发人员对这种配置声明式事务的方式一定非常熟悉。但是，显式为每一个业务类配置一个 TransactionProxyFactoryBean 的做法将使得代码显得过于刻板，为此我们可以使用自动创建代理的方式来将其简化。使用自动创建代理是纯 AOP 知识，请读者参考相关文档，不在此赘述。基于 <tx> 命名空间的声明式事务管理前面两种声明式事务配置方式奠定了 Spring 声明式事务管理的基石。在此基础上，Spring 2.x 引入了 <tx> 命名空间，结合使用 <aop> 命名空间，带给开发人员配置声明式事务的全新体验，配置变得更加简单和灵活。另外，得益于 <aop> 命名空间的切点表达式支持，声明式事务也变得更加强大。如清单 10 所示：清单 10. 基于 <tx> 的事务管理示例配置文件<beans......> ...... <bean id="bankService" class="footmark.spring.core.tx.declare.namespace.BankServiceImpl"> <property name="bankDao" ref="bankDao"/> </bean> <tx:advice id="bankAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="transfer" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="bankPointcut" expression="execution(* *.transfer(..))"/> <aop:advisor advice-ref="bankAdvice" pointcut-ref="bankPointcut"/> </aop:config> ...... </beans>如果默认的事务属性就能满足要求，那么代码简化为如清单 11 所示：清单 11. 简化后的基于 <tx> 的事务管理示例配置文件<beans......> ...... <bean id="bankService" class="footmark.spring.core.tx.declare.namespace.BankServiceImpl"> <property name="bankDao" ref="bankDao"/> </bean> <tx:advice id="bankAdvice" transaction-manager="transactionManager"> <aop:config> <aop:pointcut id="bankPointcut" expression="execution(* *.transfer(..))"/> <aop:advisor advice-ref="bankAdvice" pointcut-ref="bankPointcut"/> </aop:config> ...... </beans>由于使用了切点表达式，我们就不需要针对每一个业务类创建一个代理对象了。另外，如果配置的事务管理器 Bean 的名字取值为"transactionManager"，则我们可以省略 <tx:advice> 的 transaction-manager 属性，因为该属性的默认值即为"transactionManager"。基于 @Transactional 的声明式事务管理除了基于命名空间的事务配置方式，Spring 2.x 还引入了基于 Annotation 的方式，具体主要涉及 @Transactional 标注。@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时，该类的所有 public 方法将都具有该类型的事务属性，同时，我们也可以在方法级别使用该标注来覆盖类级别的定义。如清单 12 所示：清单 12. 基于 @Transactional 的事务管理示例代码@Transactional(propagation = Propagation.REQUIRED) public boolean transfer(Long fromId, Long toId, double amount) { return bankDao.transfer(fromId, toId, amount); }Spring 使用 BeanPostProcessor 来处理 Bean 中的标注，因此我们需要在配置文件中作如下声明来激活该后处理 Bean，如清单 13 所示：清单 13. 启用后处理 Bean 的配置<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 的声明式事务管理方式，但是，学习这两种方式非常有利于对底层实现的理解。虽然上面共列举了四种声明式事务管理方式，但是这样的划分只是为了便于理解，其实后台的实现方式是一样的，只是用户使用的方式不同而已。结束语本教程的知识点大致总结如下：基于 TransactionDefinition、PlatformTransactionManager、TransactionStatus 编程式事务管理是 Spring 提供的最原始的方式，通常我们不会这么写，但是了解这种方式对理解 Spring 事务管理的本质有很大作用。基于 TransactionTemplate 的编程式事务管理是对上一种方式的封装，使得编码更简单、清晰。基于 TransactionInterceptor 的声明式事务是 Spring 声明式事务的基础，通常也不建议使用这种方式，但是与前面一样，了解这种方式对理解 Spring 声明式事务有很大作用。基于 TransactionProxyFactoryBean 的声明式事务是上中方式的改进版本，简化的配置文件的书写，这是 Spring 早期推荐的声明式事务管理方式，但是在 Spring 2.0 中已经不推荐了。基于 <tx> 和 <aop> 命名空间的声明式事务管理是目前推荐的方式，其最大特点是与 Spring AOP 结合紧密，可以充分利用切点表达式的强大支持，使得管理事务更加灵活。基于 @Transactional 的方式将声明式事务管理简化到了极致。开发人员只需在配置文件中加上一行启用相关后处理 Bean 的配置，然后在需要实施事务管理的方法或者类上使用 @Transactional 指定事务规则即可实现事务管理，而且功能不必其他方式逊色。下载资源样例代码 (spring-tx-src.zip | 38KB)相关主题Spring 官方参考文档: 极佳的 Spring 学习指南。Spring 框架简介: 优秀的 Spring 入门教程。说明： 本文基于 Spring 2.5 版本编写，部分配置方式（如 XML 配置）在现代 Spring 版本（5.x/6.x）中已逐渐被 Java Config 和注解配置取代，但事务管理的核心概念（隔离级别、传播行为等）依然适用。
概述Spring 最成功、最吸引人的特性莫过于轻量级的声明式事务管理。仅此一点，它就宣告了重量级 EJB 容器的覆灭。Spring 声明式事务管理将开发者从繁复的事务管理代码中解脱出来，使其能专注于业务逻辑的开发，这是一件值得推崇的事情。但是，世界并未从此消停。开发人员需要面对的是层出不穷的应用场景，这些场景往往逾越了普通 Spring 技术书籍的理想界定。因此，随着应用开发的深入，在使用经过 Spring 层层封装的声明式事务时，开发人员越来越觉得自己坠入了迷雾，陷入了沼泽，体会不到外界所宣称的那种畅快淋漓。本系列文章的目标旨在整理并剖析实际应用中种种让我们迷茫的场景，让阳光照进云遮雾障的山头。DAO 和事务管理的牵绊很少有使用 Spring 但不使用 Spring 事务管理器的应用，因此常常有人会问：是否用了 Spring，就一定要用 Spring 事务管理器？否则就无法进行数据的持久化操作吗？事务管理器和 DAO 是什么关系呢？也许是 DAO 和事务管理如影随行的缘故，这个看似简单的问题实实在在地存在着，从初学者心中涌出，萦绕在开发老手的脑际。答案当然是否定的！我们都知道：事务管理是保证数据操作的事务性（即原子性、一致性、隔离性、持久性，也即所谓的 ACID）。脱离了事务性，DAO 照样可以顺利地进行数据操作。下面，我们来看一段使用 Spring JDBC 进行数据访问的代码：清单 1. UserJdbcWithoutTransManagerService.javapackage user.withouttm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.apache.commons.dbcp.BasicDataSource; @Service("service1") public class UserJdbcWithoutTransManagerService { @Autowired private JdbcTemplate jdbcTemplate; public void addScore(String userName, int toAdd) { String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?"; jdbcTemplate.update(sql, toAdd, userName); } public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("user/withouttm/jdbcWithoutTransManager.xml"); UserJdbcWithoutTransManagerService service = (UserJdbcWithoutTransManagerService) ctx.getBean("service1"); JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate"); BasicDataSource basicDataSource = (BasicDataSource) jdbcTemplate.getDataSource(); // ①. 检查数据源 autoCommit 的设置 System.out.println("autoCommit:" + basicDataSource.getDefaultAutoCommit()); // ②. 插入一条记录，初始分数为 10 jdbcTemplate.execute( "INSERT INTO t_user(user_name,password,score) VALUES('tom','123456',10)"); // ③. 调用工作在无事务环境下的服务类方法，将分数添加 20 分 service.addScore("tom", 20); // ④. 查看此时用户的分数 int score = jdbcTemplate.queryForInt( "SELECT score FROM t_user WHERE user_name ='tom'"); System.out.println("score:" + score); jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'"); } }jdbcWithoutTransManager.xml 的配置文件如下所示：清单 2. jdbcWithoutTransManager.xml<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:component-scan base-package="user.withouttm"/> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="oracle.jdbc.driver.OracleDriver" p:url="jdbc:oracle:thin:@localhost:1521:orcl" p:username="test" p:password="test"/> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> </beans>运行 UserJdbcWithoutTransManagerService，在控制台上打出如下的结果：defaultAutoCommit:true score:30在 jdbcWithoutTransManager.xml 中，没有配置任何事务管理器，但是数据已经成功持久化到数据库中。在默认情况下，dataSource 数据源的 autoCommit 被设置为 true —— 这也意味着所有通过 JdbcTemplate 执行的语句马上提交，没有事务。如果将 dataSource 的 defaultAutoCommit 设置为 false，再次运行 UserJdbcWithoutTransManagerService，将抛出错误，原因是新增及更改数据的操作都没有提交到数据库，所以 ④ 处的语句因无法从数据库中查询到匹配的记录而引发异常。对于强调读速度的应用，数据库本身可能就不支持事务，如使用 MyISAM 引擎的 MySQL 数据库。这时，无须在 Spring 应用中配置事务管理器，因为即使配置了，也是没有实际用处的。不过，对于 Hibernate 来说，情况就有点复杂了。因为 Hibernate 的事务管理拥有其自身的意义，它和 Hibernate 一级缓存有密切的关系：当我们调用 Session 的 save、update 等方法时，Hibernate 并不直接向数据库发送 SQL 语句，而是在提交事务（commit）或 flush 一级缓存时才真正向数据库发送 SQL。所以，即使底层数据库不支持事务，Hibernate 的事务管理也是有一定好处的，不会对数据操作的效率造成负面影响。所以，如果是使用 Hibernate 数据访问技术，没有理由不配置 HibernateTransactionManager 事务管理器。但是，不使用 Hibernate 事务管理器，在 Spring 中，Hibernate 照样也可以工作，来看下面的例子：清单 3. UserHibernateWithoutTransManagerService.javapackage user.withouttm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.orm.hibernate3.HibernateTemplate; import org.apache.commons.dbcp.BasicDataSource; import user.User; @Service("service2") public class UserHibernateWithoutTransManagerService { @Autowired private HibernateTemplate hibernateTemplate; public void addScore(String userName, int toAdd) { User user = (User) hibernateTemplate.get(User.class, userName); user.setScore(user.getScore() + toAdd); hibernateTemplate.update(user); } public static void main(String[] args) { // 参考 UserJdbcWithoutTransManagerService 相应代码 … } }此时，采用 hiberWithoutTransManager.xml 的配置文件，其配置内容如下：清单 4. hiberWithoutTransManager.xml<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> … <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" p:dataSource-ref="dataSource"> <property name="annotatedClasses"> <value>user.User</value> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> org.hibernate.dialect.Oracle10gDialect </prop> <prop key="hibernate.show_sql">true</prop> </props> </property> </bean> <bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate" p:sessionFactory-ref="sessionFactory"/> </beans>运行 UserHibernateWithoutTransManagerService，程序正确执行，并得到类似于 UserJdbcWithoutTransManagerService 的执行结果，这说明 Hibernate 在 Spring 中，在没有事务管理器的情况下，依然可以正常地进行数据的访问。应用分层的迷惑Web、Service 及 DAO 三层划分就像西方国家的立法、行政、司法三权分立一样被奉为金科玉律，甚至有开发人员认为如果要使用 Spring 的事务管理就一定先要进行三层的划分。这个看似荒唐的论调在开发人员中颇有市场。更有甚者，认为每层必须先定义一个接口，然后再定义一个实现类。其结果是：一个很简单的功能，也至少需要 3 个接口，3 个类，再加上视图层的 JSP 和 JS 等，代码结构变得显著臃肿，这种误解贻害不浅。对将“面向接口编程”奉为圭臬，认为放之四海而皆准的论调，笔者深不以为然。是的，“面向接口编程”是 Martin Fowler、Rod Johnson 这些大师提倡的行事原则。如果拿这条原则去开发架构，开发产品，怎么强调都不为过。但是，对于我们一般的开发人员来说，做的最多的是普通工程项目，往往最多的只是一些对数据库增、删、查、改的功能。此时，“面向接口编程”除了带来更多的类文件外，看不到更多其它的好处。Spring 框架提供的所有附加的好处（AOP、注解增强、注解 MVC 等）唯一的前提就是让 POJO 的类变成一个受 Spring 容器管理的 Bean，除此以外没有其它任何的要求。下面的实例用一个 POJO 完成所有的功能，既是 Controller，又是 Service，还是 DAO：清单 5. MixLayerUserService.javapackage user.mixlayer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; // ①. 将 POJO 类通过注解变成 Spring MVC 的 Controller @Controller public class MixLayerUserService { // ②. 自动注入 JdbcTemplate @Autowired private JdbcTemplate jdbcTemplate; // ③. 通过 Spring MVC 注解映射 URL 请求 @RequestMapping("/logon.do") public String logon(String userName, String password) { if(isRightUser(userName, password)) { String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?"; jdbcTemplate.update(sql, 20, userName); return "success"; } else { return "fail"; } } private boolean isRightUser(String userName, String password) { // do sth... return true; } }通过 @Controller 注解将 MixLayerUserService 变成 Web 层的 Controller，同时也是 Service 层的服务类。此外，由于直接使用 JdbcTemplate 访问数据，所以 MixLayerUserService 还是一个 DAO。来看一下对应的 Spring 配置文件：清单 6. applicationContext.xml<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:component-scan base-package="user.mixlayer"/> <bean class="org.springframework.web.servlet.mvc.annotation .AnnotationMethodHandlerAdapter"/> <bean class="org.springframework.web.servlet.view .InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="oracle.jdbc.driver.OracleDriver" p:url="jdbc:oracle:thin:@localhost:1521:orcl" p:username="test" p:password="test"/> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/> <bean id="jdbcManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <aop:config proxy-target-class="true"> <aop:pointcut id="serviceJdbcMethod" expression="execution(public * user.mixlayer.MixLayerUserService.*(..))"/> <aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/> </aop:config> <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> </beans>在 ① 处，我们定义配置了 AnnotationMethodHandlerAdapter，以便启用 Spring MVC 的注解驱动功能。而 ② 处通过 Spring 的 aop 及 tx 命名空间，以及 AspectJ 的切点表达式语法进行事务增强的定义，对 MixLayerUserService 的所有公有方法进行事务增强。要使程序能够运行起来还必须进行 web.xml 的相关配置：清单 7. web.xml<?xml version="1.0" encoding="GB2312"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:user/mixlayer/applicationContext.xml</param-value> </context-param> <context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/classes/log4j.properties</param-value> </context-param> <listener> <listener-class> org.springframework.web.util.Log4jConfigListener </listener-class> </listener> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <servlet-name>user</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:user/mixlayer/applicationContext.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>user</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>这个配置文件很简单，唯一需要注意的是 DispatcherServlet 的配置。默认情况下 Spring MVC 根据 Servlet 的名字查找 WEB-INF 下的 <servletName>-servlet.xml 作为 Spring MVC 的配置文件，在此，我们通过 contextConfigLocation 参数显式指定 Spring MVC 配置文件的确切位置。将 org.springframework.jdbc 及 org.springframework.transaction 的日志级别设置为 DEBUG，启动项目，并访问 http://localhost:8088/logon.do?userName=tom 应用，MixLayerUserService#logon 方法将作出响应，查看后台输出日志：清单 8. 执行日志13:24:22,625 DEBUG (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.mixlayer.MixLayerUserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT 13:24:22,906 DEBUG (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] for JDBC transaction 13:24:22,921 DEBUG (DataSourceTransactionManager.java:222) - Switching JDBC Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] to manual commit 13:24:22,921 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update 13:24:22,921 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] 13:24:23,140 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows 13:24:23,140 DEBUG (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit 13:24:23,140 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] 13:24:23,140 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@6e1cbf] after transaction 13:24:23,156 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource日志中粗体部分说明了 MixLayerUserService#logon 方法已经正确运行在事务上下文中。Spring 框架本身不应该是复杂化代码的理由，使用 Spring 的开发者应该是无拘无束的：从实际应用出发，去除掉那些所谓原则性的接口，去除掉强制分层的束缚，简单才是硬道理。事务方法嵌套调用的迷茫Spring 事务一个被讹传很广的说法是：一个事务方法不应该调用另一个事务方法，否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚，生怕一不小心就踩到地雷。其实这是不认识 Spring 事务传播机制而造成的误解。Spring 对事务控制的支持统一在 TransactionDefinition 类中描述，该类有以下几个重要的接口方法：int getPropagationBehavior()：事务的传播行为int getIsolationLevel()：事务的隔离级别int getTimeout()：事务的过期时间boolean isReadOnly()：事务的读写特性很明显，除了事务的传播行为外，事务的其它特性 Spring 是借助底层资源的功能来完成的，Spring 无非只充当个代理的角色。但是事务的传播行为却是 Spring 凭借自身的框架提供的功能，是 Spring 提供给开发者最珍贵的礼物，讹传的说法玷污了 Spring 事务框架最美丽的光环。所谓事务传播行为就是多个事务方法相互调用时，事务如何在这些方法间传播。Spring 支持 7 种事务传播行为：PROPAGATION_REQUIRED：如果当前没有事务，就新建一个事务，如果已经存在一个事务中，加入到这个事务中。这是最常见的选择。PROPAGATION_SUPPORTS：支持当前事务，如果当前没有事务，就以非事务方式执行。PROPAGATION_MANDATORY：使用当前的事务，如果当前没有事务，就抛出异常。PROPAGATION_REQUIRES_NEW：新建事务，如果当前存在事务，把当前事务挂起。PROPAGATION_NOT_SUPPORTED：以非事务方式执行操作，如果当前存在事务，就把当前事务挂起。PROPAGATION_NEVER：以非事务方式执行，如果当前存在事务，则抛出异常。PROPAGATION_NESTED：如果当前存在事务，则在嵌套事务内执行。如果当前没有事务，则执行与 PROPAGATION_REQUIRED 类似的操作。Spring 默认的事务传播行为是 PROPAGATION_REQUIRED，它适合于绝大多数的情况。假设 ServiveX#methodX() 都工作在事务环境下（即都被 Spring 事务增强了），假设程序中存在如下的调用链：Service1#method1() -> Service2#method2() -> Service3#method3()，那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。下面，我们来看一下实例，UserService#logon() 方法内部调用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法，这两个类都继承于 BaseService。它们之间的类结构说明如下：图 1. UserService 和 ScoreService具体的代码如下所示：清单 9. UserService.java@Service("userService") public class UserService extends BaseService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private ScoreService scoreService; public void logon(String userName) { updateLastLogonTime(userName); scoreService.addScore(userName, 20); } public void updateLastLogonTime(String userName) { String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); } }UserService 中注入了 ScoreService 的 Bean，ScoreService 的代码如下所示：清单 10. ScoreService.java@Service("scoreUserService") public class ScoreService extends BaseService { @Autowired private JdbcTemplate jdbcTemplate; public void addScore(String userName, int toAdd) { String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?"; jdbcTemplate.update(sql, toAdd, userName); } }通过 Spring 的事务配置为 ScoreService 及 UserService 中所有公有方法都添加事务增强，让这些方法都工作于事务环境下。下面是关键的配置代码：清单 11. 事务增强配置 <aop:config proxy-target-class="true"> <aop:pointcut id="serviceJdbcMethod" expression="within(user.nestcall.BaseService+)"/> <aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/> </aop:config> <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice>将日志级别设置为 DEBUG，启动 Spring 容器并执行 UserService#logon() 的方法，仔细观察如下的输出日志：清单 12. 执行日志16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.nestcall.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ① 为 UserService#logon 方法启动一个事务 16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65] for JDBC transaction logon method... updateLastLogonTime... ② 直接执行 updateLastLogonTime 方法 16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update 16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?] 16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows 16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) - Participating in existing transaction ③ ScoreService#addScore 方法加入到 UserService#logon 的事务中 addScore... 16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update 16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] 16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows 16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit ④ 提交事务 16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@32bd65] 16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@32bd65] after transaction 16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource从上面的输入日志中，可以清楚地看到 Spring 为 UserService#logon() 方法启动了一个新的事务，而 UserService#updateLastLogonTime() 和 UserService#logon() 是在相同的类中，没有观察到有事务传播行为的发生，其代码块好像“直接合并”到 UserService#logon() 中。接着，当执行到 ScoreService#addScore() 方法时，我们就观察到了发生了事务传播的行为：Participating in existing transaction，这说明 ScoreService#addScore() 添加到 UserService#logon() 的事务上下文中，两者共享同一个事务。所以最终的结果是 UserService 的 logon(), updateLastLogonTime() 以及 ScoreService 的 addScore 都工作于同一事务中。多线程的困惑由于 Spring 的事务管理器是通过线程相关的 ThreadLocal 来保存数据访问基础设施，再结合 IOC 和 AOP 实现高级声明式事务的功能，所以 Spring 的事务天然地和线程有着千丝万缕的联系。我们知道 Web 容器本身就是多线程的，Web 容器为一个 Http 请求创建一个独立的线程，所以由此请求所牵涉到的 Spring 容器中的 Bean 也是运行于多线程的环境下。在绝大多数情况下，Spring 的 Bean 都是单实例的（singleton），单实例 Bean 的最大的好处是线程无关性，不存在多线程并发访问的问题，也即是线程安全的。一个类能够以单实例的方式运行的前提是“无状态”：即一个类不能拥有状态化的成员变量。我们知道，在传统的编程中，DAO 必须执有一个 Connection，而 Connection 即是状态化的对象。所以传统的 DAO 不能做成单实例的，每次要用时都必须 new 一个新的实例。传统的 Service 由于将有状态的 DAO 作为成员变量，所以传统的 Service 本身也是有状态的。但是在 Spring 中，DAO 和 Service 都以单实例的方式存在。Spring 是通过 ThreadLocal 将有状态的变量（如 Connection 等）本地线程化，达到另一个层面上的“线程无关”，从而实现线程安全。Spring 不遗余力地将状态化的对象无状态化，就是要达到单实例化 Bean 的目的。由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化，所以 Spring 中单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行于多线程环境中，Service 本身还可以自由地启动独立线程以执行其它的 Service。下面，通过一个实例对此进行描述：清单 13. UserService.java 在事务方法中启动独立线程运行另一个事务方法@Service("userService") public class UserService extends BaseService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private ScoreService scoreService; // ① 在 logon 方法体中启动一个独立的线程，在该独立的线程中执行 ScoreService#addScore() 方法 public void logon(String userName) { System.out.println("logon method..."); updateLastLogonTime(userName); Thread myThread = new MyThread(this.scoreService, userName, 20); myThread.start(); } public void updateLastLogonTime(String userName) { System.out.println("updateLastLogonTime..."); String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?"; jdbcTemplate.update(sql, System.currentTimeMillis(), userName); } // ② 封装 ScoreService#addScore() 的线程 private class MyThread extends Thread { private ScoreService scoreService; private String userName; private int toAdd; private MyThread(ScoreService scoreService, String userName, int toAdd) { this.scoreService = scoreService; this.userName = userName; this.toAdd = toAdd; } public void run() { scoreService.addScore(userName, toAdd); } } }将日志级别设置为 DEBUG，执行 UserService#logon() 方法，观察以下输出的日志：清单 14. 执行日志[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ① [main] (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transaction logon method... updateLastLogonTime... [main] (JdbcTemplate.java:785) - Executing prepared SQL update [main] (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?] [main] (JdbcTemplate.java:794) - SQL update affected 0 rows [main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit [Thread-2](AbstractPlatformTransactionManager.java:365) - Creating new transaction with name [user.multithread.ScoreService.addScore]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ② [main] (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③ [main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@1353249] after transaction [main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource [Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transaction addScore... [main] (JdbcTemplate.java:416) - Executing SQL statement [DELETE FROM t_user WHERE user_name='tom'] [main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource [Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update [Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?] [main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource [Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows [Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit [Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④ [Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction在 ① 处，在主线程（main）执行的 UserService#logon() 方法的事务启动，在 ③ 处，其对应的事务提交，而在子线程（Thread-2）执行的 ScoreService#addScore() 方法的事务在 ② 处启动，在 ④ 处对应的事务提交。所以，我们可以得出这样的结论：在 相同线程中 进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中，不同线程下的事务方法工作在独立的事务中。小结Spring 声明式事务是 Spring 最核心，最常用的功能。由于 Spring 通过 IOC 和 AOP 的功能非常透明地实现了声明式事务的功能，一般的开发者基本上无须了解 Spring 声明式事务的内部细节，仅需要懂得如何配置就可以了。但是在实际应用开发过程中，Spring 的这种透明的高阶封装在带来便利的同时，也给我们带来了迷惑。就像通过流言传播的消息，最终听众已经不清楚事情的真相了，而这对于应用开发来说是很危险的。本系列文章通过剖析实际应用中给开发者造成迷惑的各种难点，通过分析 Spring 事务管理的内部运作机制将真相还原出来。在本文中，我们通过剖析了解到以下的真相：在没有事务管理的情况下，DAO 照样可以顺利进行数据操作；将应用分成 Web，Service 及 DAO 层只是一种参考的开发模式，并非是事务管理工作的前提条件；Spring 通过事务传播机制可以很好地应对事务方法嵌套调用的情况，开发者无须为了事务管理而刻意改变服务方法的设计；由于单实例的对象不存在线程安全问题，所以进行事务管理增强的 Bean 可以很好地工作在多线程环境下。版本说明本文示例代码基于 Spring 3.0 版本编写（如 XML schema 版本及 AnnotationMethodHandlerAdapter 的使用）。在新版本 Spring（如 Spring 5.x/6.x）中，部分配置类（如 AnnotationMethodHandlerAdapter）已被废弃或移除，建议在实际项目中参考官方最新文档进行配置，但文中关于事务传播、线程模型及分层设计的核心原理依然适用。