事务管理(Transaction Management)是一个很深的研究方向,而本章最终目的是为了阐述spring的事务管理抽象的理念以及相关内容,所以,你大可放心,我不会为你准备一本类似于“砖头”的书籍, 但为了能够在整个讲解的过程中始终有一个平滑的过渡,有关事务(Transaction)的一些基本概念还是有必要简单介绍一下的。
为了说明“什么是事务”,我觉得先从事务所针对的目的说起,会比较容易切入。
对于一个软件系统来说,需要相应的数据资源(比如,数据库,文件系统等)来保存系统状态,在对系统状态所依托的数据资源进行访问的时候,为了保证系统始终处于一个“正确”的状态[1], 我们就必须对这些访问操作进行一些必要的限定,以此来保证系统状态的完整性。
事务就是以可控的方式对数据资源进行访问的一组操作,为了保证事务执行前后数据资源所承载的系统状态始终处于“正确”状态,事务本身持有四个限定属性, 即原子性(Atomicity),一致性(Consistency),隔离性(Isolation)以及持久性(Durability),也就是常说的事务的ACID属性:
原子性要求事务所包含的全部操作是一个不可分割的整体,这些操作要么全部提交成功,要么只要其中一个操作失败,就全部“成仁”(“一颗老鼠屎搅坏一锅汤”好像形容这种情况比较贴切哦)。 如果把整个事务的操作比做“钢七连”,那我们的口号就得从“不抛弃,不放弃”改成“要么不抛弃,要么就全部放弃”了。
一致性要求事务所包含的操作不能违反数据资源的一致性检查,数据资源在事务执行之前处于一个数据的一致性状态,那么,事务执行之后也需要依然保持数据间的一致性状态。 对于一个证券系统来说,如果顾客银行账户和证券账户资金总和为10万的话(银行账户初始8万,证券账户初始2万),从银行账户的8万转账5万到证券账户的事务操作结束之后, 银行账户会剩余3万,证券账户为7万,两个账户的总和依然是10万,如果事务操作结束后,整个数据状态不是这个样子,那么就说系统处于不一致状态,而使用事务其中一个目的就是为了避免这种不一致性状态的产生。
事务的隔离性主要规定了各个事务之间相互影响的程度。隔离性概念主要面向对数据资源的并发访问(Concurrency),并兼顾影响事务的一致性。当两个事务或者更多事务同时访问同一数据资源的时候, 不同的隔离级别决定了各个事务对该数据资源访问的不同行为。
不出意外的话,我们可以为事务指定四种类型的隔离级别,隔离程度按照从弱到强分别为“Read Uncommitted”,“Read Committed”,“Repeatable Read”和“Serializable”:
-
Read Uncommitted. 最低的隔离级别,Read Uncommitted最直接的效果就是一个事务可以读取另一个事务并未提交的更新结果。
Read Uncommitted是以较低的隔离度来寻求较高的性能,其本身无法避免以下几个问题:
-
脏读(Dirty Read). 如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以“看到”该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所“看到”的数据就是一笔脏数据。
-
不可重复读取(Non-Repeatable Read). 不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2的更新操作之后再读取同一笔数据一次,两次结果是不同的,所以,Read Uncommitted也无法避免不可重复读取的问题。
-
幻读(Phantom Read)[3]. 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,Read Uncommitted同样无法避免幻读的问题。
-
-
Read Committed. Read Committed通常是大部分数据库采用的默认隔离级别,它在Read Uncommitted隔离级别基础上所做的限定更进一步, 在该隔离级别下,一个事务的更新操作结果只有在该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果。 所以,Read Committed可以避免Read Uncommitted隔离级别下存在的脏读问题, 但,无法避免不可重复读取和幻读的问题。
-
Repeatable Read. Repeatable Read隔离级别可以保证在整个事务的过程中,对同一笔数据的读取结果是相同的,不管其他事务是否同时在对同一笔数据进行更新,也不管其他事务对同一笔数据的更新提交与否。 Repeatable Read隔离级别避免了脏读和不可重复读取的问题,但无法避免幻读。
-
Serializable. 最为严格的隔离级别,所有的事务操作都必须依次顺序执行,可以避免其他隔离级别遇到的所有问题,是最为安全的隔离级别, 但同时也是性能最差的隔离级别,因为所有的事务在该隔离级别下都需要依次顺序执行,所以,并发度下降,吞吐量上不去,性能自然就下来了。 因为该隔离级别极大的影响系统性能,所以,很少场景会使用它。通常情况下,我们会使用其他隔离级别加上相应的并发锁的机制来控制对数据的访问,这样既保证了系统性能不会损失太大,也能够一定程度上保证数据的一致性。
不同的隔离级别设置会对系统的并发性以及数据一致性造成不同的影响,总的来说,隔离级别与系统并发性成反比,与数据一致性成正比。 也就是说,事务隔离度越高,系统并发性越差,进而造成系统性能就越差,不过,隔离度的增高,却可以更好地保证数据的一致性。隔离程度与并发性和一致性的关系如下图:
在具体的实践过程中,我们需要根据系统的具体情况来调整隔离度以保证系统性能与数据一致性之间有一个良好的平衡,但总的来说,保证数据的一致性的考虑应该优于对系统性能的考虑。
事务的持久性是指一旦整个事务操作成功提交完成,对数据所做的变更将被记载并不可逆转,多少有点儿“生米煮成熟饭”的意思。 即使发生某些系统灾难或者什么天灾人祸之类的事情,之前事务所做的变更也可以找回并恢复。纵使海枯石烂,我(数据库等资源管理系统)对你(事务)的真心永不变! 通常情况下,数据库等数据资源管理系统会通过冗余存储或者多数据网络备份等方式来保证事务的持久性。
在一个典型的事务处理场景中,有以下几个参与者:
ResourceManager简称RM,它负责存储并管理系统数据资源的状态,比如数据库服务器,JMS消息服务器等都是相应的Resource Manager。
Transaction Processing Monitor简称TPM或者TP Monitor,它的职责是在分布式事务场景中协调包含多个RM的事务处理。TP Monitor通常对应特定的软件中间件(Middleware), 随着软件开发技术的进步,TP Monitor的实现也由原来基于过程式的设计与实现转向面向对象的更趋模块化的设计和实现。J2EE规范[4]中的应用服务器(Application Server)通常担当的就是TP Monitor的角色。
Transaction Manager简称为TM,它可以认为是TP Monitor中的核心模块,直接负责多RM之间的事务处理的协调工作,并且提供事务界定(Transaction Demarcation)[5], 事务上下文传播(transaction context propagation)[6]等功能接口。
以独立形式存在的或者运行于容器中的应用程序,可以认为是事务边界的触发点。
-
全局事务(Global Transaction). 如果整个事务处理过程中有多个Resource Manager的参与,那么就需要引入TP Monitor来协调多个RM之间的事务处理,TP Monitor将采用“两阶段提交(2 Phase Commit)”协议来保证整个事务的ACID属性, 这种场景下的事务我们就称其为全局事务(Global Transaction)或者分布式事务(Distributed Transaction)。全局事务中各个参与者之间的关系如下图所示:
所有应用程序提交的事务请求需要通过TP Monitor的调配之后,直接由TM统一协调,TM将使用“两阶段提交(two-phase commit)”协议来协调处理多RM之间的事务处理。 针对“两阶段提交”的描述,最经典的比喻就是西方婚礼的过程,婚礼的牧师或者主持是TM,他会首先询问两位新人(两个RM),是否愿意娶对方为妻(嫁给对方), 如果双方的反馈都是“I do”的时候,牧师将宣布二者结为夫妻(即整个事务提交成功)。如果双方任何一方有疑议,那不好意思,婚礼无法继续进行,整个事务提交失败,双方都要回滚(rollback)到之前的单身状态。 -
局部事务(Local Transaction). 如果当前事务只有一个Resource Manager参与其中的话,我们就可以称当前事务为局部事务(Local Transaction)。 比如,你在当前事务中只对一个数据库进行更新,或者只向一个消息队列中发送消息的情况,都属于局部事务。
因为局部事务只包含一个Resource manager,所以,也就没有必要引入相应的TP Monitor来帮助协调管理多个Resource Manager之间的事务, 应用程序可以直接与RM打交道。通常情况下,相应的RM都有内置的事务支持,所以,在局部事务中,我们更倾向于直接使用RM的内置事务支持, 这样不仅可以极大的减少事务处理的复杂度,也避免了引入TP Monitor来协调多个Resource Manager之间事务的性能负担。
Caution
局部事务与全局事务的主要区分在于“事务”中牵扯多少RM,而不是“系统”中实际有多少RM,这是需要我们注意的地方。 即使你系统中存在多个数据库(即RM),只要你当前事务只更新一个数据库的数据,那当前事务就依然应该算作局部事务,而不是全局事务(虽然这种情况下,你也可以启用全局事务)。
实际上,针对单一事务资源的事务管理,你可以在局部事务中直接使用RM内置的事务支持来进行,你也可以引入TP Monitor在分布式事务场景中进行,通常情况下, 各TP Monitor在实现的时候会检测参与事务的RM数目,如果只有单一的RM参与,TP Monitor会做一定的优化,避免采用“两阶段提交”协议的负担, 但即使如此,针对单一事务资源参与的事务,直接采用局部事务中RM内置的事务支持,无论是从复杂度,还是从效率上来看,都要更胜一筹。
到此为止,我们所阐述的都是概念层面的东西,要真正的在系统开发中使用事务,我们需要相应的产品和API支持。 为了让事务“走进现实”,不同的组织或者不同的技术平台会有各自不同的API设计和实现, 但既然Spring(不是Spring .Net)归属于Java一族,其他的平台和组织的解决方案暂且放于一边,我们专门来看Java平台的事务解决方案如何?
对于应用程序的开发人员来说,更多时候,我们是通过相应产品提供的API接口来访问事务资源,考虑如何在应用的业务逻辑中界定事务边界,而对于各提供商如何在产品中实现事务支持, 则通常不是我们需要关心的问题。所以,以下内容将更多的围绕着各事务处理场景下我们可以通过哪些产品提供的事务处理接口或者标准的事务处理接口来进行事务控制为主线进行。 当然,期间我们也可能提及相应场景下比较受欢迎的几款事务处理的产品实现。
下面我们将按照从局部事务场景到全局事务场景的顺序来看一下,各场景中Java平台为我们都准备了哪些可用的事务处理API。
在Java的局部事务场景中,系统中事务管理的具体处理方式会随着所使用的数据访问技术的不同而各异,我们不是使用专用的事务API来管理事务, 而是通过当前使用的数据访问技术所提供的基于“connection” [7] 的API来管理事务。
-
数据库资源的局部事务管理. 要在对数据库的访问过程中进行事务管理,每一种数据访问技术都提供了特定于它自身的事务管理API,比如JDBC是Java平台访问关系数据库最基础的API,如果直接使用JDBC进行数据访问的话,我们可以将数据库连接(java.sql.Connection)的自动提交(AutoCommit)功能设置为false,改为手动提交来控制整个事务的提交或者回滚:
Connection connection = null; boolean rollback = false; try { connection = dataSource.getConnection(); connection.setAutoCommit(false); // do data access with JDBC connection.commit(); } catch(SQLException e) { e.printStackTrace(); // don't do this rollback = true; } finally { if(connection != null) { if(rollback) { try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); // don't do this } } else { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); // don't do this } } } }
而如果我们使用Hibernate进行数据访问,我们就得使用Hibernate的Session进行数据访问期间的事务管理[8]:Session session = null; Transaction transaction = null; try { session = sessionFactory.openSession(); transaction = session.beginTransaction(); // do data access with Hibernate session.flush(); transaction.commit(); } catch(HibernateException e) { transaction.rollback(); } finally { session.close(); }
同样的,如果我们使用JDO,TopLink甚至JPA进行数据访问的话,这些数据访问技术也都在他们数据访问API之上提供了相应的事务管理支持。 -
消息服务资源的局部事务管理. 在使用JMS进行消息处理的过程中,我们可以通过JMS的javax.jms.Session来控制整个处理过程的事务:
boolean rollback = false; Connection con = null; Session session = null; try { con = cf.createConnection(); session = con.createSession(true, Session.AUTO_ACKNOWLEDGE); // process Messages with JMS API session.commit(); } catch (JMSException e) { e.printStackTrace();// don't do this rollback = true; } finally { if(con != null) { if(rollback) { try { session.rollback(); } catch (JMSException e1) { e1.printStackTrace(); // don't do this } } else { try { con.close(); } catch (JMSException e) { e.printStackTrace(); // don't do this } } } }
我们在通过javax.jms.Connection的createSession方法创建javax.jms.Session的时候, 将该方法的第一个参数指定为true要求创建一个事务型的javax.jms.Session实例,然后就可以根据情况提交和回滚(rollback)事务了。
Java平台上的分布式事务管理主要是通过Java Transaction API(JTA)或者Java Connector Architecture(JCA)提供支持的。
JTA是Sun提出的标准化分布式事务访问的Java接口规范。不过,JTA规范定义的只是一套Java接口定义,具体的实现留给了相应的提供商去实现,各JavaEE应用服务器需要提供对JTA的支持, 另外,除了可以使用绑定到各JavaEE应用服务器的JTA实现之外,Java平台上也存在几个独立的并且比较成熟的JTA实现产品,这包括:
-
JOTM(http://jotm.objectweb.org/index.html)
-
AtomikosAtomikos(http://www.atomikos.com/home.html)
-
JBoss Transactions(http://labs.jboss.com/jbosstm/)
使用JTA进行分布式事务管理通常有两种方式,直接使用JTA接口的编程事务管理以及基于应用服务器的声明性事务管理。
使用JTA进行分布式事务的编程式事务管理通常使用javax.transaction.UserTransaction接口进行,各应用服务器都提供了针对它的JNDI查找服务。
下面是典型的使用UserTransaction进行事务管理的代码片断:
try { UserTransaction ut = (UserTransaction)ctx.lookup("javax.transaction.UserTransaction"); ut.begin(); // 事务操作 ut.commit(); } catch (NamingException e) { e.printStackTrace(); } catch (NotSupportedException e) { e.printStackTrace(); } catch (SystemException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (RollbackException e) { e.printStackTrace(); } catch (HeuristicMixedException e) { e.printStackTrace(); } catch (HeuristicRollbackException e) { e.printStackTrace(); }如果是在EJB中使用UserTransaction的话,你可以直接通过javax.ejb.EJBContext获得UserTransaction的引用:
UserTransaction ut = ctx.getUserTransaction(); ...在EJB中直接编程使用UserTransaction主要是BMT(Bean Managed Transaction)的情况下, 不过,在EJB中使用JTA进行分布式管理有比直接使用编程方式更吸引人的方式,那就是在CMT(Container Managed Transacton)情况下,EJB容器提供的声明性的事务管理。
如果使用EJB进行声明性的分布式事务管理的话(限于CMT的情况),JTA的使用则只限于EJB容器的内部,对于应用程序来说则完全就是透明的, 现在唯一需要做的工作实际上就是在相应的部署描述符中指定相应的事务属性即可:
<enterprise-beans> <session> <display-name>Your Enterprise Java Bean</display-name> <ejb-name>YourBean</ejb-name> <home>...</home> <remote>...</remote> <ejb-class>...YourBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> </session> </enterprise-beans> <assembly-descriptor> <container-transaction> <method> <ejb-name>YourBean</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor><transaction-type>指定了我们让EJB容器来管理的事务,并且,<trans-attribute>规定所有方法执行都需要相应事务, 现在,应用程序内部再也不用充斥着各种事务管理的代码了。
你也看到了,Java平台提供的事务管理API足够丰富,可谓高中低档一应俱全,这当然有助于我们根据合适场景选用合适的事务管理API, 但,在实际使用过程中,过多的事务管理策略的选择也会造成一些问题:
使用JDBC进行数据访问,你需要通过java.sql.Connection来控制局部事务; 使用Hibernate进行数据访问,你需要通过org.hibernate.Session和org.hibernate.Transaction来管理局部事务, 至于其他的数据访问方式,自然也是要使用它们特定的数据访问API来控制局部事务。
这样直接导致的问题就是事务管理代码与数据访问代码甚至业务逻辑代码混杂,因为局部事务场景下,我们使用的是数据访问API进行事务控制。 如果实际使用中不能通过合适的方式对事物管理的代码与数据访问代码或者业务逻辑代码进行逻辑上的隔离, 将直接导致数据访问代码和业务逻辑代码的可重用性降低,甚至事务管理代码在数据访问层和业务服务层的到处散落。
当前的情况是,各种数据访问方式只提供了简单的事务API,但没有更高层次的抽象来帮助我们隔离事务与数据访问两个方面的过紧耦合。
事务处理过程中出现的异常应该都是不可恢复的,所以,应该抛出“unchecked exception”,并且有一个统一的父类,便于客户端处理。 但是现在的情况是:
-
没有一个统一的事务相关异常体系,使用特定API管理事务的时候,你就需要捕捉这些 API特定的异常并处理;
-
许多事务管理代码在使用过程中抛出的依然还是“checked exception”强制客户端代码来捕捉并处理, 从UserTransaction的使用上你就可以看出来, 从JNDI查找到事务的开始和结束等操作,简单的事务界定操作,却引出七八个异常需要处理,任何一个人在使用UserTransaction进行编程式事务管理的时候也不会认为这样的API设计很好用吧?!
对于开发人员来说,所谓对事务的管理,最多也就是界定一下事务的边界,规定事务什么地方开始,什么地方结束,可是,要达到这一个目的,你却要在各种数据访问API或者JTA之间徘徊。
各种事务管理API的存在给了我们更多选择,但没有一个统一的方式来抽象单一的事务管理需求,反而让这种多种选择的优势变得繁杂而不易管理。
EJB容器提供的CMT特性是比较受欢迎的事务界定管理方式,因为业务对象中不需要混杂任何事务管理代码,所有的事务管理都通过一些简单的配置交由容器来管理。 CMT提供的声明式的事务管理很好的分离了事务管理与具体的数据资源访问之间的耦合,使得开发人员能够分别专心于业务逻辑和数据访问逻辑的开发。
但CMT形式的声明式事务管理有一个令人惋惜的限制,那就是,你必须借助于EJB容器才能得到这种声明式事务的好处。 如果你的应用程序想要使用声明式的事务管理,却不得不花费不少银子来购买应用服务器厂商的授权,或者,即使是使用开源的应用服务器, 但你的应用程序在此之前并没有特别强烈的必须应用服务器的需求,这些情况下,引入应用服务器的做法应该不太容易令人接受吧?
总之,要使用CMT的声明式事务管理,强制要求引入EJB容器的支持(当然,你也得以EJB的形式提供组件实现),某些情况下是不合适的。
-
我们能否对众多的基于数据访问API的局部事务操作进行一个合理的抽象,以便隔离事务管理与数据资源访问之间的过分耦合?
-
能否在合适的地方将各种场景下事务处理抛出的“checked exception”进行一个合适的转译,以屏蔽事务处理过程中因使用不同的事务API所造成的差异性?
-
既然对于我们来说,事务管理的需求很简单,基本上就是事物界定的工作,那我们能否对事务的界定操作进行统一抽象,以屏蔽各种事务管理API的差异性,使得事务管理能够以统一的编程模型来进行?
-
既然声明式的事务管理如此诱人,那么能否突破必须依赖EJB容器的限制,寻求一种能够为普通的Java对象(POJO)提供声明式事务的方式那?
spring的事务框架将开发过程中事务管理相关的关注点进行适当的分离,并对这些关注点进行合理的抽象,最终打造了一套使用方便却功能强大的事务管理“利器”。 通过spring的事务框架,我们可以按照统一的编程模型来进行事务编程,却不用关心所使用的数据访问技术以及具体要访问什么类型的事务资源; 并且,spring的事务框架与spring提供的数据访问支持可以紧密结合,更是让你在事务管理与数据访问之间游刃有余,而最主要的,结合spring的AOP框架, spring的事务框架为我们带来了只有CMT才有的使用声明式事务管理的待遇,却无需绑定到任何的应用服务器上。
其他溢美之词咱就先放一边,还是赶快进入正题吧!
spring的事务框架设计理念的基本原则在于:让事务管理的关注点与数据访问关注点相分离:
-
当你在业务层使用事务的抽象API进行事务界定的时候,你不需要关心事务将要加诸于上的事务资源是什么,对不同的事务资源的管理将由相应的框架实现类来操心;
-
当你在数据访问层对可能参与事务的数据资源进行访问的时候,只需要使用相应的数据访问API进行数据访问,却不需要关心当前的事务资源如何参与事务或者是否需要参与事务,这同样将由事务框架类来打理;
public class FooService { private PlatformTransactionManager transactionManager; public void serviceMethod() { TransactionDefinition definition = ...; TransactionStatus txStatus = getTransactionManager().getTransaction(definition); try { // dao1.doDataAccess(); // dao2.doDataAccess(); // ... } catch(DataAccessException e) { getTransactionManager().rollback(txStatus); throw e; } catch(OtherNecessaryException e) { getTransactionManager().rollback(txStatus); throw e; } getTransactionManager().commit(txStatus); } public PlatformTransactionManager getTransactionManager() { return transactionManager; } public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } }纵使你数据访问方式如何变换,我事务管理实现可以岿然不动。只要有了这样一个统一的事务管理编程模型,剩下的声明式事务管理自然就是锦上添花之作啦! 从此之后,事务管理就是事务管理,数据访问只关心数据访问,再也不用因为他们之间的纠缠而烦恼。
org.springframework.transaction.PlatformTransactionManager是spring事务抽象架构的核心接口,他的主要作用在于为应用程序提供事务界定的统一方式。 既然事务界定的需要很简单,那么PlatformTransactionManager的定义看起来也不会过于复杂:
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }PlatformTransactionManager是整个事务抽象策略的顶层接口,它就好象我们的战略蓝图,而战略的具体实施则将由相应的PlatformTransactionManager实现类来执行。
spring的事务框架针对不同的数据访问方式以及全局事务场景,提供了相应的PlatformTransactionManager实现类,当每一个实现类的职责完成之后,spring事务框架的“统一大业”就算完成了。 在深入了解各个PlatformTransactionManager实现类的奥秘之前,我们不妨先考虑一下,如果让我们来实现一个PlatformTransactionManager,要如何去做那?
不妨先以针对JDBC数据访问方式的局部事务管理为例。对于层次划分清晰的应用来说,我们通常都是将事务管理放在Service层,而将数据访问逻辑放在DAO层,这样做的目的在于可以不用因为将事务管理代码放在DAO层而降低数据访问逻辑的重用性, 也可以在Service层根据相应逻辑来决定提交(commit)或者回滚(rollback)事务。一般的Service对象可能需要在同一个业务方法中调用多个数据访问对象的方法,类似于这样的情况:
因为JDBC的局部事务控制是由同一个java.sql.Connection来控制的,所以,要保证两个DAO的数据访问方法处于一个事务中,我们就得保证他们使用的是同一个java.sql.Connection,要做到这一点, 通常采用称之为“ connection passing”的方式,即为同一个事务中的各个dao的数据访问方法传递当前事务对应的同一个java.sql.Connection,这样,我们的业务方法以及数据访问方法都得做一定的修改:
我们只要把java.sql.Connection的获取并设置autoCommit状态的代码以及使用java.sql.Connection提交事务的代码重构到原来的开启事务以及提交事务的方法中,针对JDBC的局部事务管理的整合看起来离成功也就是咫尺之遥了。 不过,这看起来的咫尺之遥,实际上却依然遥远。
使用这种方式,最致命的一个问题就在于,不但事务管理代码无法摆脱java.sql.Connection的纠缠,而且数据访问对象的定义要绑定到具体的数据访问技术上来。 现在是使用JDBC进行数据访问,你要在数据访问方法中声明对java.sql.Connection的依赖,那要是使用HIbernate的话,是不是要声明对Session的依赖那?显然,这样的做法是不可行的。 不过好消息是,传递Connection的理念是对的,只不过,我们具体实施过程中所采用的方法不对头。
要传递java.sql.Connection,我们可以将整个事务对应的java.sql.Connection实例放到统一的一个地方去,无论是谁,要使用该资源,都从这一个地方来获取,这样就解除了事务管理代码和数据访问代码之间通过java.sql.Connection的“直接”耦合。 具体一点儿说就是,我们在事务开始之前取得一个java.sql.Connection,然后将这个Connection绑定到当前的调用线程,之后,数据访问对象在使用Connection进行数据访问的时候,就可以从当前线程上去获得这个事务开始的时候绑定的Connection实例, 当所有的数据访问对象全部使用这个绑定到当前线程的Connection完成了数据访问工作,我们就使用这个Connection实例提交或者回滚事务,然后解除它到当前线程的绑定。
这个时候的java.sql.Connection就像那大河上的一条船,从启航(事务开始)到航程结束(事务完成),在这整个期间,大河沿岸都可以与该船打交道,而至于说你是发“ 木船”>还是发“ 铁轮”, 那由你来决定了,发JDBC的船,那就是Connection,发Hibernate的船,那就是Session...
假设TransactionResourceManager就是我们存放java.sql.Connection(或者其他事务资源)的地方,那么,它看起来可能的样子如下(过多的逻辑检验代码略去):
public class TransactionResourceManager { private static ThreadLocal resources = new ThreadLocal(); public static Object getResource() { return resources.get(); } public static void bindResource(Object resource) { resources.set(resource); } public static Object unbindResource() { Object res = getResource(); resources.set(null); return res; } }对于我们要实现的针对JDBC的PlatformTransactionManager,只需要在事务开始将java.sql.Connection通过我们的TransactionResourceManager绑定,然后在事务结束解除绑定即可:
public class JdbcTransactionManager implements PlatformTransactionManager { private DataSource dataSource; public JdbcTransactionManager(DataSource dataSource) { this.dataSource = dataSource; } public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { Connection connection; try { connection = dataSource.getConnection(); TransactionResourceManager.bindResource(connection); return new DefaultTransactionStatus(connection,true,true,false,true,null); } catch (SQLException e) { throw new CannotCreateTransactionException("can't get connection for tx",e); } } public void rollback(TransactionStatus txStatus) throws TransactionException { Connection connection = (Connection)TransactionResourceManager.unbindResource(); try { connection.rollback(); } catch (SQLException e) { throw new UnexpectedRollbackException("rollback failed with SQLException",e); } finally { try { connection.close(); } catch (SQLException e) { // log information but can do nothing } } } public void commit(TransactionStatus txStatus) throws TransactionException { Connection connection = (Connection)TransactionResourceManager.unbindResource(); try { connection.commit(); } catch (SQLException e) { throw new TransactionSystemException("commit failed with SQLException",e); } finally { try { connection.close(); } catch (SQLException e) { // log information but can do nothing } } } }因为Connection在事务开始和结束期间都可以通过我们的TransactionResourceManager获得,所以,所有的DAO层数据访问对象在使用JDBC进行数据访问的时候,就可以直接从TransactionResourceManager来获得数据库连接并进行数据访问,这样就可以保证在整个事务期间,所有的数据访问对象对应的同一个Connection。
public class FooJdbcDao implements IDao { public void doDataAccess() { Connection con = (Connection)TransactionResourceManager.getResource(); // ... } }至此,我们完成了PlatformTransactionManager具体实现类并解除了它与相应数据访问对象之间通过java.sql.Connection的直接耦合,进行事务控制的时候,我们只需要为Service对象提供相应的PlatformTransactionManager实现类, Service对象中的事务管理功能就算大功告成了,而不需要关心到底对应的是什么样的事务资源,甚至什么样的数据访问方式。
当然,为了便于读者理解spring抽象层的实现原理,以上的代码实例都是简化后的模型,所以,不要试图将他们应用于生产环境。 原型代码永远都是原型代码,要做的事情还有许多,比如:
-
“如何保证PlatformTransactionManager的相应方法被以正确的顺序调用,如果哪一个方法没有被正确调用,也会造成资源泄漏以及事务管理代码混乱的问题。”
在稍后为读者介绍使用spring进行编程事务管理的时候,你将看到spring是如何解决这个问题的。
-
“数据访问对象的接口定义不会因为最初的‘connection passing’方式而改变契约了,但是,现在却要强制使用每个数据访问对象使用TransactionResourceManager来获取数据资源接口, 另外,如果当前数据访问对象对应的数据方法不想参与跨越多个数据操作的事务的时候,甚至于不想(或不能)使用类似的事务管理支持,是否就意味着无法获得connection进行数据访问了那? ”
不知道你是否还记得我们在介绍spring的数据访问一章内容的时候,曾经提到的org.springframework.jdbc.datasource.DataSourceUtils工具类, 当时我们只是强调了DataSourceUtils提供的异常转译能力,实际上,DataSourceUtils最主要工作却在于对connection的管理,DataSourceUtils会从类似TransactionResourceManager的类(spring中对应org.springframework.transaction.support.TransactionSynchronizationManager)那里 获取Connection资源,如果当前线程之前没有绑定任何connection,他就通过数据访问对象的DataSource引用获取新的connection,否则就使用绑定的那个connection。 这就是为什么要强调,当我们要使用spring提供的事务支持的时候,必须通过DataSourceUtils来获取连接的原因,因为它提供了spring事务管理框架在数据访问层需要提供的基础设施中不可或缺的一部分,而JdbcTemplate等类内部已经使用DataSourceUtils来管理连接了,所以,我们不用操心这些细节。 从这里,你也应可以看出,spring的事务管理与它的数据访问框架是紧密结合的。
Note
对应Hibernate的SessionFactoryUtils,对应JDO的PersistenceManagerFactoryUtils以及对应其他数据访问技术的Utils类, 他们的作用与DataSourceUtils是相似的,除了提供异常转译功能,他们更多的用于数据访问资源的管理工作,以配合对应的PlatformTransactionManager实现类进行事务管理。
spring的事务抽象包括三个主要接口,即PlatformTransactionManager,TransactionDefinition以及TransactionStatus,他们之间的关系如下:
三接口以org.springframework.transaction.PlatformTransactionManager为中心,互为犄角,多少有点儿“ 晋西北铁三角”的味道。 org.springframework.transaction.PlatformTransactionManager负责界定事务边界,org.springframework.transaction.TransactionDefinition负责定义事务相关属性,包括隔离级别,传播行为等, org.springframework.transaction.PlatformTransactionManager将参照org.springframework.transaction.TransactionDefinition的属性定义来开启相关事务,事务开启之后到事务结束期间的事务状态由org.springframework.transaction.TransactionStatus负责, 我们也可以通过org.springframework.transaction.TransactionStatus对事务进行有限的控制。
org.springframework.transaction.TransactionDefinition主要定义了有哪些事务属性可以指定,这包括:
-
事务的隔离级别(Isolation)
-
事务的传播行为(Propagation Behavisor)
-
事务的超时时间(Timeout)
-
是否为只读事务(ReadOnly)
TransactionDefinition内定义了五个常量用于标志可供选择的隔离级别:
-
ISOLATION_DEFAULT. 如果指定隔离级别为ISOLATION_DEFAULT,则表示使用数据库默认的隔离级别,通常情况下是“read committed”;
-
ISOLATION_READ_UNCOMMITTED. 对应“Read Uncommitted”隔离级别,无法避免脏读,不可重复读和幻读;
-
ISOLATION_READ_COMMITTED. 对应“Read Committed”隔离级别,可以避免脏读,但无法避免不可重复读和幻读;
-
ISOLATION_REPEATABLE_READ. 对应“Repeatable read”隔离级别,可以避免脏读和不可重复读,但不能避免幻读;
-
ISOLATION_SERIALIZABLE. 对应“Serializable”隔离级别,可以避免所有的脏读,不可重复读以及幻读,但并发性效率最低;
FooService的业务方法的传播行为被我们指定为Required,表示如果当前存在事务的话,则加入当前事务,因为FoobarService在调用FooService的业务方法的时候已经启动了一个事务,所以,FooSerivce的业务方法会直接加入FoobarService启动的“ 事务1”中; BarService的业务方法的传播行为被指定为Required New,表示无论当前是否存在事务,都需要为其重新启动一个事务,所以,它使用的是自己启动的“ 事务2”。
TransactionDefinition针对事务的传播行为提供了以下几种选择,除了PROPAGATION_NESTED是spring特有的外,其他的传播行为的语义与CMT基本相同:
-
PROPAGATION_REQUIRED. 如果当前存在一个事务,则加入当前事务;如果不存在任何事务,则创建一个新的事务。总之,要至少保证在一个事务中运行。PROPAGATION_REQUIRED通常作为默认的事务传播行为。
-
PROPAGATION_SUPPORTS. 如果当前存在一个事务,则加入当前事务;如果当前不存在事务,则直接执行。 对于一些查询方法来说,PROPAGATION_SUPPORTS通常是比较合适的传播行为选择。 如果当前方法直接执行,那么不需要事务的支持;如果当前方法被其他方法调用,而其他方法启动了一个事务的时候,使用PROPAGATION_SUPPORTS可以保证当前方法能够加入当前事务并洞察当前事务对数据资源所做的更新。 比如说,A.service()会首先更新数据库,然后调用B.service()进行查询,那么,B.service()如果是PROPAGATION_SUPPORTS的传播行为, 就可以读取A.service()之前所做的最新更新结果,而如果使用稍后所提到的PROPAGATION_NOT_SUPPORTED,则B.service()将无法读取最新的更新结果,因为A.service()的事务在这个时候还没有提交(除非隔离级别是read uncommitted):
-
PROPAGATION_MANDATORY. PROPAGATION_MANDATORY强制要求当前存在一个事务,如果不存在,则抛出异常。 如果某个方法需要事务支持,但自身又不管理事务提交或者回滚的时候,比较适合使用PROPAGATION_MANDATORY。 你可以参照《JAVA TRANSACTION DESIGN STRATEGIES》一书中对REQUIRED和MANDATORY两种传播行为的比较来更深入的了解PROPAGATION_MANDATORY的可能应用场景。
-
PROPAGATION_REQUIRES_NEW. 不管当前是否存在事务,都会创建新的事务。如果当前存在事务的话,会将当前的事务挂起(suspend)。 如果某个业务对象所做的事情不想影响到外层事务的话,PROPAGATION_REQUIRES_NEW应该是合适的选择,比如,假设当前的业务方法需要向数据库中更新某些日志信息, 但即使这些日志信息更新失败,我们也不想因为该业务方法的事务回滚而影响到外层事务的成功提交,因为这种情况下,当前业务方法的事务成功与否对外层事务来说是无关紧要的。
-
PROPAGATION_NOT_SUPPORTED. 不支持当前事务,而是在没有事务的情况下执行。如果当前存在事务的话,当前事务原则上将被挂起(suspend),但要依赖于对应的PlatformTransactionManager实现类是否支持事务的挂起(suspend),更多情况请参照TransactionDefinition的javadoc文档。 PROPAGATION_NOT_SUPPORTED与PROPAGATION_SUPPORTS之间的区别,可以参照PROPAGATION_SUPPORTS部分的实例内容。
-
PROPAGATION_NEVER. 永远不需要当前存在事务,如果存在当前事务,则抛出异常。
-
PROPAGATION_NESTED. 如果存在当前事务,则在当前事务的一个嵌套事务中执行,否则与PROPAGATION_REQUIRED的行为类似,即创建新的事务,在新创建的事务中执行。 PROPAGATION_NESTED粗看起来好像与PROPAGATION_REQUIRES_NEW的行为类似,实际上二者是有差别的。 PROPAGATION_REQUIRES_NEW创建的新事务与外层事务属于同一个“档次”,即二者的地位是相同的,当新创建的事务运行的时候,外层事务将被暂时挂起(suspend); 而PROPAGATION_NESTED创建的嵌套事务则不然,它是寄生于当前外层事务的,它的地位比当前外层事务的地位要小一号,当内部嵌套事务运行的时候,外层事务也是出于active状态:
也就是说,PROPAGATION_REQUIRES_NEW新创建的事务虽然是在当前外层事务内执行,但新创建的事务是独立于当前外层事务而存在的,二者拥有各自独立的状态而互不干扰; 而PROPAGATION_NESTED创建的事务属于当前外层事务的内部子事务(sub-transaction),内部子事务的处理内容属于当前外层事务的一部分,而不能独立于外层事务而存在,并且与外层事务共有事务状态,我想这也就是为什么称其为内部嵌套事务的原因。PROPAGATION_NESTED可能的应用场景在于,你可以将一个大的事务划分为多个小的事务来处理,并且外层事务可以根据各个内部嵌套事务的执行结果来选择不同的执行流程。 比如,某个业务对象的业务方法A.service()可能调用其他业务方法B.service()向数据库中插入一批业务数据,但当插入数据的业务方法出现错误的时候(比如主键冲突),我们可以在当前事务中捕捉前一个方法抛出的异常,然后选择另一个更新数据的业务方法C.service()来执行, 这个时候,我们就可以把B.service()和C.serivce()方法的传播行为指定为PROPAGATION_NESTED[9],如果用伪代码演示的话,看起来如下:
/** * PROPAGATION_REQUIRED */ A.service() { try { // PROPAGATION_NESTED B.service(); } catch(Exception e) { // PROPAGATION_NESTED C.service(); } }
不过,并非所有的PlatformTransactionManager实现都支持PROPAGATION_NESTED类型的传播行为,现在只有org.springframework.jdbc.datasource.DataSourceTransactionManager在使用JDBC3.0数据库驱动的情况下才支持(当然,数据库和相应的驱动程序也需要提供支持),另外, 某些JtaTransactionManager也可能提供支持,但JTA规范并没有要求提供对嵌套事务的支持。
TransactionDefinition提供了TIMEOUT_DEFAULT常量定义,用来指定事务的超时时间,TIMEOUT_DEFAULT默认值为-1,这会采用当前事务系统默认的超时时间,你将可以通过TransactionDefinition的具体实现类提供自定义的事务超时时间。
TransactionDefinition提供的最后一个重要信息就是将要创建的是否是一个只读(ReadOnly)的事务,如果你需要创建一个只读的事务的话,可以通过TransactionDefinition的相关实现类进行设置。 只读的事务仅仅是给相应的ResourceManager提供一种优化的提示,但最终是否提供优化,则由最终的ResourceManager决定。对于一些查询来说,我们通常会希望它们采用只读事务。
TransactionDefinition仅仅是一个接口定义,要为PlatformTransactionManager创建事务提供信息,需要有相应的实现类提供支持。 TransactionDefinition的相关实现类虽然不多,但为了便于理解,我们依然将他们划分为“两派”:
将TransactionDefinition的相关实现类按照“ 编程式事务场景”和“ 声明式事务场景”划分为两个分支, 仅仅是出于每个类在相应场景中出现的频率方面考虑的,而不是说“ 声明式事务场景”的实现类不能在“ 编程式事务场景”使用。
org.springframework.transaction.support.DefaultTransactionDefinition是TransactionDefinition接口的默认实现类, 他提供了各事务属性的默认值,并且通过它的setter方法,你可以更改这些默认设置。这些默认值包括:
-
propagationBehavior = PROPAGATION_REQUIRED
-
isolationLevel = ISOLATION_DEFAULT
-
timeout = TIMEOUT_DEFAULT
-
readOnly = false
org.springframework.transaction.interceptor.TransactionAttribute是继承自TransactionDefinition的接口定义, 主要面向使用Spring AOP进行声明式事务管理的场合,它在TransactionDefinition定义的基础上添加了一个rollbackOn方法:
boolean rollbackOn(Throwable ex);这样,我们可以通过声明的方式指定业务方法在抛出哪些的异常的情况下可以回滚(rollback)事务。
TransactionAttribute的默认实现类是DefaultTransactionAttribute,他同时继承了DefaultTransactionDefinition, 在DefaultTransactionDefinition的基础上追加了rollbackOn的实现,DefaultTransactionAttribute的实现指定当异常类型为“unchecked exception”的情况下将回滚(rollback)事务。
DefaultTransactionAttribute之下有两个实现类,即RuleBasedTransactionAttribute以及DelegatingTransactionAttribute。 RuleBasedTransactionAttribute允许我们同时指定多个回滚规则,这些规则以包含org.springframework.transaction.interceptor.RollbackRuleAttribute或者org.springframework.transaction.interceptor.NoRollbackRuleAttribute的List形式提供, RuleBasedTransactionAttribute的rollbackOn将使用传入的异常类型与这些回滚规则进行匹配,然后再决定是否要回滚事务。
DelegatingTransactionAttribute是抽象类,它存在的目的就是被子类化,DelegatingTransactionAttribute会将所有方法调用委派给另一个具体的TransactionAttribute实现类, 比如DefaultTransactionAttribute或者RuleBasedTransactionAttribute,不过,除非不是简单的直接委派(什么附加逻辑都不添加),否则,实现一个DelegatingTransactionAttribute是没有任何意义的。
org.springframework.transaction.TransactionStatus接口定义表示整个事务处理过程中的事务状态, 我们将更多时候在编程式事务中使用该接口。
在事务处理过程中,我们可以使用TransactionStatus进行如下工作:
-
使用TransactionStatus提供的相应方法查询事务状态;
-
通过setRollbackOnly()方法标记当前事务以回滚(rollback);
-
如果相应的PlatformTransactionManager支持Savepoint,可以通过TransactionStatus在当前事务中创建内部嵌套事务;
TransactionStatus的实现层次比较简单,见下图:
org.springframework.transaction.SavepointManager是在JDBC3.0的基础上对Savepoint的支持提供的抽象, 通过继承SavepointManager,TransactionStatus获得可以管理Savepoint的能力,从而支持创建内部嵌套事务。
org.springframework.transaction.support.AbstractTransactionStatus为TransactionStatus的抽象类实现, 主要为其他实现子类提供一些“公共设施”,它下面主要有两个子类, DefaultTransactionStatus和SimpleTransactionStatus, 其中,DefaultTransactionStatus是spring事务框架内部使用的主要TransactionStatus实现类,spring事务框架内的各个TransactionManager的实现大都借助于DefaultTransactionStatus来记载事务状态信息。 SimpleTransactionStatus在spring框架内部的实现中没有使用到,目前来看,主要用于测试目的。
PlatformTransactionManager是spring事务抽象框架的核心组件,关于它的定义以及作用我们之前已经提过了,所以,这部分我们不妨更多的关注一下PlatformTransactionManager整个的层次体系以及针对不同数据访问技术的实现类。
PlatformTransactionManager整个的抽象体系基于Strategy模式,由PlatformTransactionManager对事务界定进行统一抽象,而具体的界定策略的实现则交由具体的实现类。 下面我们先来看一下有哪些实现类可供我们使用...
PlatformTransactionManager的实现类可以划分到面向局部事务和面向全局事务两个分支:
-
面向局部事务的PlatformTransactionManager实现类. spring为各种数据访问技术提供了现成的PlatformTransactionManager实现支持,以下列表给出了各种数据访问技术与它们对应的实现类的关系:
Table 1.1. 数据访问技术与PlatformTransactionManager实现类对应关系
数据访问技术 PlatformTransactionManager实现类 JDBC/iBatis DataSourceTransactionManager Hibernate HibernateTransactionManager JDO JdoTransactionManager JPA(Java Persistence API) JpaTransactionManager TopLink TopLinkTransactionManager JMS JmsTransactionManager JCA Local Transaction CciLocalTransactionManager
在这些实现类当中,CciLocalTransactionManager可能是比较少见的实现,CCI的意思是Common Client Interface, CciLocalTransactionManager主要是面向JCA的局部事务(Local Transaction),本书不打算对JCA的集成做过多的阐述,读者如果在实际项目中需要使用到JCA进行EIS(Enterprise Information System)系统集成,你可以从spring的参考文档获得使用spring提供的JCA集成支持的足够信息。有了这些实现类,我们在使用spring的事务抽象框架进行事务管理的时候,只需要根据当前使用的数据访问技术选择对应的PlatformTransactionManager实现类即可。
Tip
如果你的应用程序需要同时使用Hibernate以及JDBC(或者iBatis)进行数据访问,那么你可以使用HibernateTransactionManager对基于Hibernate和JDBC(或者iBatis)的事务进行统一管理, 只要Hibernate的SessionFactory和JDBC(或者iBatis)引用的是同一个DataSource就行。你能猜到为什么吗?
-
面向全局事务的PlatformTransactionManager实现类. org.springframework.transaction.jta.JtaTransactionManager是spring提供的支持分布式事务的PlatformTransactionManager实现。 直接使用JTA规范接口进行分布式事务管理有以下几个问题:
-
UserTransaction接口使用复杂不说(一长串的异常处理我们之前也见过了),它所暴露的事务管理能力有限,对于事务的挂起(suspend)以及恢复(resume)操作,只有JTA的TransactionManager才支持;
-
JTA规范并没有明确要求对TransactionManager的支持,这就造成当下各个JTA提供商虽然提供了TransactionManager的实现,但在应用服务器中暴露的位置各有差异,为了进一步支持REQUIRES_NEW和NOT_SUPPORTED之类需要事务挂起以及恢复操作的事务传播行为, 我们需要通过不同的方式来获取不同JTA提供商暴露的TransactionManager实现;
对于典型的基于JTA的分布式事务管理,我们直接使用JtaTransactionManager就可以了,但某些时候,需要更多使用到各JTA产品的TransactionManager特性的时候, 这个时候,我们就可以为JtaTransactionManager注入这些JTA产品的javax.transaction.TransactionManager的实现, 而至于说你是通过应用服务器获取该TransactionManager,还是直接使用本地定义的TransactionManager(比如JOTM或者Atomikos等独立JTA实现产品的TransactionManager), 则完全由你根据当时的场景来决定了。 能够为JtaTransactionManager提供具体的TransactionManager实现为我们扩展JtaTransactionManager提供了很好的一个切入点。
JtaTransactionManager有两个子类OC4JJtaTransactionManager和WebLogicJtaTransactionManager,分别面向基于Oracle OC4J和Weglogic的JTA分布式事务管理, 在这些情况下,使用具体的子类来代替通常的JtaTransactionManager。不过,大多数情况下,使用spring提供的FactoryBean机制来获取不同JTA提供商提供的TransactionManager实现,然后注入JtaTransactionManager使用,是比较好的做法, org.springframework.transaction.jta包下,spring提供了面向JOTM,Weblogic和Websphere的TransactionManager查找FactoryBean实现,如果需要,你也可以根据情况实现其他的TransactionManager实现的查找FactoryBean。
-
有了spring的事务抽象框架,事务管理策略的转换也变得很简单,通常也只是简单的配置文件变更而已。 如果我们最初只需要处理单一资源的事务管理,那么,局部场景中的面向不同数据访问技术的PlatformTransactionManager实现将是我们的最佳选择,即使后来需要引入分布式资源的事务管理, 对于我们来说,也仅仅是从局部事务场景中的某个PlatformTransactionManager实现转向JtaTransactionManager的变动而已,无论是编程式注入还是通过spring的IoC容器注入,对于应用程序来说都不会造成很大的变动。
PlatformTransactionManager的各个子类在实现的时候基本上遵循统一的结构和理念,所以,我们不妨选择以DataSourceTransactionManager这一实现类作为切入点, 以管中窥豹之势,一探spring的抽象事务框架中各个PlatformTransactionManager实现类的奥秘之所在。
不过,在开始之前,我们有必要先了解几个概念:
transaction object承载了当前事务的必要信息,PlatformTransactionManager实现类可以根据transaction object所提供的信息来决定如何处理当前事务。 transaction object的概念类似于JTA规范中的javax.transaction.Transaction定义。
TransactionSynchronization是可以注册到事务处理过程中的回调(callback)接口,它就像是事务处理的事件监听器, 当事务处理的某些规定时点发生的时候,会调用TransactionSynchronization上的一些方法来执行相应的回调逻辑,比如在事务完成后清理相应的系统资源等操作。
spring事务抽象框架所定义的TransactionSynchronization类似于JTA规范的javax.transaction.Synchronization,但比JTA的Synchronization提供了更多的回调方法, 允许我们对事务的处理添加更多的回调逻辑。
类似于JTA规范中的javax.transaction.TransactionSynchronizationRegistry, 我们通过TransactionSynchronizationManager来管理TransactionSynchronization,当前事务状态以及具体的事务资源。 在介绍spring事务框架实现原理的原型中,我们提到会将具体的事务资源,比如java.sql.Connection或者hibernate Session绑定到线程,而TransactionSynchronizationManager就是这些资源绑定的目的地,当然,从该类的名字也可以看出, 它更多关注与事务相关的Synchronization的管理。
OK,有了这些铺垫,我们开始进入正题...
spring的事务抽象框架是以PlatformTransactionManager作为顶层抽象接口,具体的实现交给不同的实现类,使用对象可以根据当前场景选择使用或者替换哪一个具体的实现类, 从这个层次看,整个框架的设计是以Strategy模式为基础的。 不过,从各个实现类的继承层次上来看,spring事务框架的实现则更多的依赖于模板方法模式:
org.springframework.transaction.support.AbstractPlatformTransactionManager作为DataSourceTransactionManager的父类, 以模板方法的形式封装了固定的事务处理逻辑,而只将与事务资源相关的操作以protected或者abstract方法的形式下放给DataSourceTransactionManager来实现。 作为模板方法父类,AbstractPlatformTransactionManager替各子类实现了以下固定的事务内部处理逻辑:
-
判定是否存在当前事务,然后根据判断结果执行不同的处理逻辑;
-
结合是否存在当前事务的情况,根据TransactionDefinition中指定的传播行为(Propagation)的不同语义执行后继逻辑;
-
根据情况挂起(suspend)或者恢复(resume)事务;
-
提交事务之前检查readOnly字段是否被设置,如果是的话,以事务的回滚代替事务的提交;
-
事务回滚的情况下清理并恢复事务状态;
-
如果事务的synchonization处于active状态,在事务处理的规定时点触发注册的synchonization回调接口;
-
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException
-
public final void rollback(TransactionStatus status) throws TransactionException
-
public final void commit(TransactionStatus status) throws TransactionException
-
protected final SuspendedResourcesHolder suspend(Object transaction) throws TransactionException
-
protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) throws TransactionException
我们先从第一个模板方法getTransaction(TransactionDefinition)开始。 getTransaction(TransactionDefinition)的主要目的是开启一个事务,但需要在此之前判断一下之前是否存在一个事务,如果存在,则根据TransactionDefinition中的传播行为决定是挂起当前事务还是抛出异常;同样的,不存在事务的情况下,也需要根据传播行为的具体语义来决定如何处理。 getTransaction(TransactionDefinition)方法的处理逻辑基本上按照下面的流程执行[10]:
Procedure 1.1. getTransaction(TransactionDefinition)执行流程
-
获取transaction object,以判断是否存在当前事务
Object transaction = doGetTransaction();
这行代码有两点需要申明:-
获取的transaction object类型会因具体实现类的不同而各异,DataSourceTransactionManager会返回DataSourceTransactionManager.DataSourceTransactionObject类型实例, HibernateTransactionManager会返回HibernateTransactionManager.HibernateTransactionObject类型的实例,等等。 AbstractPlatformTransactionManager不需要知道具体实现类返回的transaction object具体类型是什么,因为最终对transaction object的依赖都将通过方法参数进行传递, 只要具体的实现类在取得transaction object参数后知道如何转型就行,所以,这一步返回的transaction为Object类型;
-
doGetTransaction()为getTransaction(TransactionDefinition)模板方法暴露给子类来实现的abstract类型方法,DataSourceTransactionManager在实现doGetTransaction()方法逻辑的时候, 会从TransactionSynchronizationManager获取绑定的资源,然后添加到DataSourceTransactionObject之后返回。以此类推,其他AbstractPlatformTransactionManager子类都采用类似的逻辑实现了doGetTransaction()方法。
-
-
获取Log类的Debug信息,避免之后的代码重复
boolean debugEnabled = logger.isDebugEnabled(); if (debugEnabled) { logger.debug("Using transaction object [" + transaction + "]"); }
debugEnabled将以方法参数的形式在各方法调用间传递,以避免每次都调用logger.isDebugEnabled()获取debug日志状态。这一步与具体的事务处理流程关系不大。 -
检查TransactionDefinition参数合法性
if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); }
如果definition参数为空,则创建一个DefaultTransactionDefinition实例以提供默认的事务定义数据。 -
根据先前获得transaction object判断是否存在当前事务,根据判定结果采取不同的处理方式
if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(definition, transaction, debugEnabled); }
isExistingTransaction(transaction)默认情况下返回false,该方法需要具体子类根据情况进行覆写(Override), 对于DataSourceTransactionManager来说,它会根据传入的transaction所记载的信息进行判断:DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; return (txObject.getConnectionHolder() != null && txObject.getConnectionHolder().isTransactionActive());
对于HibernateTransactionManager来说,则会将transaction强制转型为HibernateTransactionObject,然后根据HibernateTransactionObject所记载的信息来判断之前是否存在一个事务。 其他具体实现类对isExistingTransaction(transaction)的处理亦是如此。不管isExistingTransaction(transaction)返回结果如何,实际上,下面的处理主体上都是以TransactionDefinition中的传播行为为中心进行的, 比如同样是PROPAGATION_REQUIRED,在存在当前事务与不存在当前事务两种情况下的处理是不同的,前者会使用之前的事务,后者则会创建新的事务,其他的传播行为的处理也是按照不同的场景分别处理。
-
如果isExistingTransaction(transaction)方法返回true,即存在当前事务的情况下
由handleExistingTransaction()方法统一处理存在当前事务情况下应该如何创建事务对应的TransactionStatus实例并返回。
-
如果definition定义的传播行为是PROPAGATION_NEVER,抛出异常并退出
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { throw new IllegalTransactionStateException( "Existing transaction found for transaction marked with propagation 'never'"); }
这是由TransactionDefinition.PROPAGATION_NEVER的语义决定的。 -
如果definition定义的传播行为是PROPAGATION_NOT_SUPPORTED,则挂起当前事务,然后返回
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { if (debugEnabled) { logger.debug("Suspending current transaction"); } Object suspendedResources = suspend(transaction); boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return newTransactionStatus( definition, null, false, newSynchronization, debugEnabled, suspendedResources); }
newTransactionStatus()方法返回一个DefaultTransactionStatus实例,因为我们挂起了当前事务,而PROPAGATION_NOT_SUPPORTED不需要事务,所以,返回的DefaultTransactionStatus不包含transaction object的信息(构造方法第二个参数)。 -
如果definition定义的传播行为是PROPAGATION_REQUIRES_NEW,则同样挂起当前事务,并开始一个新的事务并返回
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug("Suspending current transaction, creating new transaction with name [" + definition.getName() + "]"); } SuspendedResourcesHolder suspendedResources = suspend(transaction); try { doBegin(transaction, definition); } catch (TransactionException beginEx) { try { resume(transaction, suspendedResources); } catch (TransactionException resumeEx) { logger.error( "Inner transaction begin exception overridden by outer transaction resume exception", beginEx); throw resumeEx; } throw beginEx; } boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); }
AbstractPlatformTransactionManager首先将当前事务挂起,然后调用doBegin()方法开始新的事务,如果开始事务过程中出现异常,则恢复之前挂起的事务。 doBegin(transaction, definition)方法为abstract方法,需要具体子类来实现,在DataSourceTransactionManager中, doBegin()方法会首先检查传入的transaction以提取必要信息判断之前是否存在绑定的connection信息,如果没有,则从DataSource中获取新的connection,然后将其AutoCommit状态改为false,并绑定到TransactionSynchronizationManager。 当然,这期间也会牵扯事务定义的应用以及条件检查等逻辑。 当所有一起搞定之后,newTransactionStatus会创建一个包含definition,transaction object以及挂起的事务信息和其它状态信息的DefaultTransactionStatus实例并返回。 -
如果definition定义的传播行为是PROPAGATION_NESTED,根据情况创建嵌套事务,比如通过Savepoint或者JTA的TransactionManager
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { if (!isNestedTransactionAllowed()) { throw new NestedTransactionNotSupportedException( "Transaction manager does not allow nested transactions by default - " + "specify 'nestedTransactionAllowed' property with value 'true'"); } if (debugEnabled) { logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); } if (useSavepointForNestedTransaction()) { // Create savepoint within existing Spring-managed transaction, // through the SavepointManager API implemented by TransactionStatus. // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization. DefaultTransactionStatus status = newTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); return status; } else { // Nested transaction through nested begin and commit/rollback calls. // Usually only for JTA: Spring synchronization might get activated here // in case of a pre-existing JTA transaction. doBegin(transaction, definition); boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null); } }
在这种情况下,会首先通过isNestedTransactionAllowed()方法检查AbstractPlatformTransactionManager的nestedTransactionAllowed属性状态, 如果允许嵌套事务,那么还得分两种情况处理,对于DataSourceTransactionManager来说,因为它支持使用Savepoint创建嵌套事务,所以,会使用TransactionStatus创建相应的Savepoint并返回; 而像JtaTransactionManager则要依赖于具体JTA产品的TransactionManager提供嵌套事务支持。useSavepointForNestedTransaction()方法默认返回true,即默认使用Savepoint创建嵌套事务,如果具体子类不支持使用Savepoint创建嵌套事务,则需要覆写(Override)该方法,比如JtaTransactionManager。
-
如果需要检查事务状态匹配情况,则对当前存在事务与传入的defintion中定义的隔离级别与ReadOnly属性进行检查,如果数据不吻合,则抛出异常;
if (isValidateExistingTransaction()) { // validate isolation // validate read only ... }
AbstractPlatformTransactionManager的validateExistingTransaction属性默认值为false,如果你想进一步加强事务属性之间的一致性, 可以将validateExistingTransaction属性设置为true,那么这个时候,以上代码即会被触发执行。 -
剩下的就是其他情况下,直接构建TransactionStatus返回
比如对应PROPAGATION_SUPPORTS和PROPAGATION_REQUIRED的情况。
-
-
如果isExistingTransaction(transaction)方法返回false,即不存在当前事务的情况下
-
当definition中定义的传播行为是PROPAGATION_MANDATORY的时候,抛出异常
因为不存在当前事务,所以根据PROPAGATION_MANDATORY的语义,理当如此。
-
当definition中定义的传播行为是PROPAGATION_REQUIRED或者PROPAGATION_REQUIRES_NEW或者PROPAGATION_NESTED的时候,开启新的事务
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { doBegin(transaction, definition); } catch (TransactionException ex) { resume(null, suspendedResources); throw ex; } boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); }
之所以在doBegin之前先调用传入null的suspend()方法是因为考虑到如果有注册的Synchronization的话,需要暂时将这些与将要开启的新事务无关的Synchronization先放一边。 -
剩下的其他情况,则返回不包含任何transaction object的TransactionStatus并返回
这种情况下虽然是空的事务,但有可能需要处理在事务过程中相关的Synchronization。
-
-
事务处理的完成有两种情况,即回滚事务或者提交事务,AbstractPlatformTransactionManager提供的rollback(TransactionStatus)和commit(TransactionStatus)两个模板方法分别对应这两种情况下的处理。 因为事务提交过程中可能需要处理回滚逻辑,我们不妨以commit(TransactionStatus)的实现流程看一下AbstractPlatformTransactionManager是如何处理事务完成的:
Procedure 1.2. 事务提交执行流程
-
因为在事务处理过程中,我们可以通过TransactionStatus的setRollbackOnly()方法标记事务回滚,所以,commit(TransactionStatus)在具体提交事务之前会检查rollBackOnly状态, 如果该状态被设置,那么转而执行事务的回滚操作;
rollback(TransactionStatus)的逻辑主要包含三点:
-
回滚事务! 这当然是必须的啦,不过,这里的回滚事务又分三种情况:
-
如果是嵌套事务,则通过TransactionStatus释放Savepoint;
-
如果TransactionStatus表示当前事务是一个新的事务,则调用子类的doRollback(TransactionStatus)方法真正的回滚事务;
doRollback(TransactionStatus)是抽象方法,具体子类必须实现它,DataSourceTransactionManager在实现该方法的时候无疑是调用connection.rollback()啦! 至于说HibernateTransactionManager,会通过它Session上的Transaction的rollback()方法回滚事务,其他子类对doRollback(TransactionStatus)的实现逻辑依此类推。
-
如果当前存在事务,并且rollbackOnly状态被设置,则调用由子类实现的doSetRollbackOnly(TransactionStatus)方法,各子类实现通常会将当前的transaction object的状态设置为rollBackOnly。
-
-
触发Synchronization事件。
rollback的时候出发的事件比commit的时候要少,只有triggerBeforeCompletion(status)和triggerAfterCompletion()。
-
清理事务资源。这包括:
-
设置TransactionStatus中的completed为完成状态;
-
清理与当前事务相关的Synchronization;
-
调用doCleanupAfterCompletion()释放事务资源,并解除到TransactionSynchronizationManager的资源绑定。对于DataSourceTransactionManager来说,当然是关闭数据库连接,然后解除对DataSource对应资源的绑定。
-
如果之前有挂起的事务,恢复挂起的事务;
-
-
-
如果rollBackOnly状态没被设置,则执行正常的事务提交操作。
commit(TransactionStatus)方法余下的逻辑与rollback(TransactionStatus)方法基本相似,只是几个具体操作有所差别:
-
回滚事务现在是提交事务;
提交事务的时候,也会牵扯到几种情况:
-
决定是否提前检测全局的rollBackOnly标志,如果最外层事务已经被标记为rollBackOnly,并且failEarlyOnGlobalRollbackOnly为true,则抛出异常:
boolean globalRollbackOnly = false; if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { globalRollbackOnly = status.isGlobalRollbackOnly(); } ... if (globalRollbackOnly) { throw new UnexpectedRollbackException( "Transaction silently rolled back because it has been marked as rollback-only"); }
-
如果提交事务之前发现TransactionStatus持有Savepoint,则释放之,这实际上是在处理嵌套事务的提交;
-
如果TransactionStatus表示要提交的事务是一个新的事务,则调用子类的doCommit(TransactionStatus)方法实现提交事务。
doCommit(TransactionStatus)也是AbstractPlatformTransactionManager暴露给子类实现的抽象方法,子类必须实现该方法。 对于DataSourceTransactionManager来说,因为事务的提交有Connection决定,所以会直接调用connection.commit()提交事务。 其他的子类也会使用自身的局部事务API在该方法中实现事务的提交。
-
-
也需要出发Synchronization相关事件,不过,触发的事件比rollback(TransactionStatus)中的要多,包括triggerBeforeCommit(), triggerBeforeCompletion(),triggerAfterCommit()和triggerAfterCompletion()。
-
如果AbstractPlatformTransactionManager的rollbackOnCommitFailure状态被设置为true,则表示如果在事务提交过程中出现异常,需要回滚事务, 所以,当commit(TransactionStatus)方法捕获相应异常并且检测到该字段被设置的时候,需要回滚事务。rollbackOnCommitFailure的默认值是false,表示即使提交过程中出现异常,也不回滚事务。
-
既然commit(TransactionStatus)与rollback(TransactionStatus)一样,都是意味着事务的完成,那么也需要在最后进行事务资源清理的工作,具体内容可以参照rollback(TransactionStatus)部分。
-
对于suspend和resume两个方法来说,逻辑更好理解了,前者会把TransactionSynchronizationManager上当前事务对应的Synchroniazation信息以及资源获取到SuspendedResourcesHolder中,然后解除这些绑定; 后者则会将SuspendedResourcesHolder中保持的信息重新绑定到TransactionSynchronizationManager。
实际上,如果将AbstractPlatformTransactionManager中处理Synchroniaztion回调以及事务传播行为的逻辑剥离一下的话,你就会发现,整个的逻辑流程就是开始为您展示的实现原型所表达的。
下图展示了AbstractPlatformTransactionManager需要子类实现或者覆写(Override)的方法:
对于各个子类来说,无非就是根据自身需要管理的资源和事务管理API提供这些方法的实现而已。
从Java平台尤其是J2EE平台上事务管理的传统意义上来看,事务管理有两种方式,即编程式事务管理以及声明式事务管理,对于这两种事务管理方式的支持, spring事务框架可以说是“青出于蓝而胜于蓝”。
通过spring进行编程式事务管理有两种方式,要么直接使用PlatformTransactionManager,要么使用更方便的TransactionTemplate,二者各有优缺点,但总体上来说,使用TransactionTemplate进行编程式事务管理是推荐的方式。
PlatformTransactionManager接口定义了事务界定的基本操作,我们可以直接使用PlatformTransactionManager进行编程式事务管理:
DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setTimeout(20); ... TransactionStatus txStatus = transactionManager.getTransaction(definition); try { // business logic implementation } catch(ApplicationException e) { transactionManager.rollback(txStatus); throw e; } catch(RuntimeException e) { transactionManager.rollback(txStatus); throw e; } catch(Error e) { transactionManager.rollback(txStatus); throw e; } transactionManager.commit(txStatus);只要为transactionManager提供合适的PlatformTransactionManager实现,然后结合TransactionDefinition开启事务,结合TransactionStatus来回滚或者提交事务就可以完成当前对象的整个事务管理。
直接使用PlatformTransactionManager,你可以完全的控制整个的事务处理过程,但是,缺点也是很明显的, 从抽象事务操作以屏蔽不同事务管理API差异的角度看,PlatformTransactionManager可能已经足够了, 但是,从应用程序开发的角度,却依然过于“底层”,单单是期间的这些异常处理就够我们忙活的了。 如果在每一个需要事务管理的地方,全都采用直接使用PlatformTransactionManager进行事务管理,那重复代码的数量将是惊人的。
鉴于使用PlatformTransactionManager进行事务管理的流程比较固定,各个事务管理期间只有部分逻辑存在差异,我们可以考虑像spring的数据访问层那样,使用模板方法模式 + Callback的方式对直接使用PlatformTransactionManager进行事务管理的代码进行封装, 这就有更方便的编程式事务管理方式,即使用TransactionTemplate的编程式事务管理。
org.springframework.transaction.support.TransactionTemplate对与PlatformTransactionManager相关的事务界定操作以及相关的异常处理进行了模板化封装, 开发人员更多的关注于通过相应的callback接口提供具体的事务界定内容即可。spring针对TransactionTemplate提供了两个callback接口,TransactionCallback和TransactionCallbackWithoutResult,二者的唯一区别就是是否需要返回执行结果。
使用TransactionTemplate进行事务管理的代码看起来要比直接使用PlatformTransactionManager要简洁并且容易管理的多:
TransactionTemplate txTemplate = ...; Object result = txTemplate.execute(new TransactionCallback(){ public Object doInTransaction(TransactionStatus txStatus) { Object result = null; // 各种事务操作 ... return result; }}); 或者 txTemplate.execute(new TransactionCallbackWithoutResult(){ @Override protected void doInTransactionWithoutResult(TransactionStatus txStatus) { // 事务操作1 // 事务操作2 // ... }});TransactionTemplate会捕捉TransactionCallback或者TransactionCallbackWithoutResult事务操作中抛出的“ unchecked exception”并回滚事务,然后将“ unchecked exception”抛给上层处理, 所以,现在我们只需要处理特定于应用程序的异常即可,而不用像直接使用PlatformTransactionManager那样对所有可能的异常都进行处理。
如果事务处理期间没有任何问题,TransactionTemplate最终会为我们提交事务,唯一需要我们干预的就只剩下某些情况下的事务回滚了。 如果在TransactionCallback或者TransactionCallbackWithoutResult的事务操作过程中需要让当前事务回滚而不是最终提交,一般来说,我们有两种方式:
-
抛出“unchecked exception”,TransactionTemplate会为我们处理事务的回滚。 如果事务操作中可能抛出“checked exception”,这种情况下就得在callback内部捕获,然后转译为“unchecked exception”后抛出。
txTemplate.execute(new TransactionCallbackWithoutResult(){ @Override protected void doInTransactionWithoutResult(TransactionStatus txStatus) { try { ... } catch(CheckedException e) { // throw specific exception here, avoid generic RuntimeException throw new RuntimeException(e); } }});
-
使用callback接口暴露的TransactionStatus将事务标记为rollBackOnly,TransactionTemplate在最终提交事务的时候如果检测到rollBackOnly标志状态被设置,将把提交事务改为回滚事务:
txTemplate.execute(new TransactionCallbackWithoutResult(){ @Override protected void doInTransactionWithoutResult(TransactionStatus txStatus) { boolean needRollback = false; ... if(needRollback) txStatus.setRollbackOnly(); }});
对于将事务操作中可能抛出的“checked exception”,如果既想回滚事务又不想让它以“unchecked exception”的形式向上层传播的话, 我们当然不能通过将其转译成“unchecked exception”的方式来处理啦,不过我们现在却可以通过TransactionStatus设置rollBackOnly:txTemplate.execute(new TransactionCallbackWithoutResult(){ @Override protected void doInTransactionWithoutResult(TransactionStatus txStatus) { try { ... } catch(CheckedException e) { logger.warn("Tranaction is Rolled back!",e); txStatus.setRollbackOnly(); } }});
这种情况下需要注意一个问题,千万不要只“txStatus.setRollbackOnly()”而忘记记录日志,虽然大家可能都知道“swallow exception”是不对的, 但这个地方确实容易忽略日志的记录,从而造成事务回滚了,而我们却不知道的情况。 当发现数据库中本来应该删除的数据却依然存在,并且日志中也没有任何异常信息的时候,你就得挠脑袋很长时间来发现到底哪里出了问题了,程序没跑?才怪!
TransactionStatus不但可以在事务处理期间通过setRollbackOnly()方法来干预事务的状态,如果需要,作为SavepointManager,它也可以帮助我们使用Savepoint机制创建嵌套事务。
我们以银行账户间转账为例说明如何使用TransactionStatus创建基于Savepoint的嵌套事务,不过,现在不是从一个账户转到另一个账户,而是从一个账户转到两个账户,一个是主账户,一个备用帐户,如果向主帐户转账失败,则将金额转入备用帐户,总之,金额从第一个账户取出之后,必须存入两个账户的其中一个,以保证整个事务的完整性。 在这样的前提下,我们的事务管理代码基本上如下所示:
txTemplate.execute(new TransactionCallbackWithoutResult(){ @Override protected void doInTransactionWithoutResult(TransactionStatus txStatus) { BigDecimal transferAmount = new BigDecimal("20000"); try { withdraw("WITHDRAW_ACOUNT_ID",transferAmount); Object savePointBeforeDeposit = txStatus.createSavepoint(); try { deposit("MAIN_ACOUNT_ID",transferAmount); } catch(DepositException ex) { logger.warn("rollback to savepoint for main acount transfer failure",ex); txStatus.rollbackToSavepoint(savePointBeforeDeposit); deposit("SECONDARY_ACOUNT_ID", transferAmount); } finally { txStatus.releaseSavepoint(savePointBeforeDeposit); } } catch(TransferException e) { logger.warn("failed to complete transfer operation!",e); txStatus.setRollbackOnly(); } }});当然,如果在转账期间的异常是“ unchecked exception”,最外层的捕捉TransferException是没有太多必要的。
在这里使用savepoint创建嵌套事务的好处在于,即使deposit中牵扯多笔数据的更新,通过“txStatus.rollbackToSavepoint(savePointBeforeDeposit)”也可以将这些数据恢复到没有存入金额之前的状态,而不会破坏当前事务的完整性。 如果在这里通过传播行为是PROPAGATION_REQUIRES_NEW的TransactionDefinition创建一个新的事务的话,虽然deposit过程出现问题也可以回滚数据,但取款与存款的操作就不在同一个事务中了(取款在当前事务,存款在另一个新的事务),这无疑违反了事务的ACID属性。
Note
使用TransactionStatus创建基于Savepoint的嵌套事务需要底层的PlatformTransactionManager实现类的支持,当前只有在JDBC3.0驱动下的DataSourceTransactionManager可用。
通过使用TransactionStatus创建基于Savepoint的嵌套事务并非创建嵌套事务的唯一方式,也并非最方便的方式,实际上,我们更倾向于结合PROPAGATION_NESTED传播行为的声明式事务管理方式。
直接使用编程式事务管理一个比较头疼的问题就是事务管理代码与业务逻辑代码相互混杂,而声明式事务管理则可以避免这种不同系统关注点之间的纠缠,使得事务管理代码不用再去“污染”具体业务逻辑的实现。
声明式事务实际上并没有想象中的那么神秘,当我们将“事务管理”这一横切关注点从原来硬编码事务管理逻辑的系统中剥离出来之后, 你就会发现,声明式事务已经在“幸福终点站”那里等着我们了。
我们先不管spring是如何提供声明式事务管理的,如果要我们从原来硬编码事务管理的系统中将这些事务管理相关的代码从业务对象中剥离出来的话,我们会怎么做? 最土的一个办法,为每一个Service都提供一个TransactionFacade,事务管理逻辑现在都集中到了TransactionFacade中,Service实现类可以对事务管理一无所知,只要保证针对所有Service的调用必须走TransactionFacade即可。 所以,整个情形看起来就是这个样子:
这种方法虽然可以实现事务管理代码与业务逻辑代码之间的分离,但是如果不做一些变通的话,在实际的开发中也不会给你带来更多的好处,难道你想每一个Serivce对象都给它实现一个对应的TransactionFacade对象?
对,经过SpringAOP的洗礼,我想你已经想到了,动态代理不就是为这个设计的吗?!呵呵,不过,原理归原理,要真的实现这种功能,直接使用SpringAOP才是正道啊。 事务管理本身就是一种横切关注点,与其他的横切关注点本质上没有任何区别,所以,我们完全可以为其提供相应的Advice实现,然后织入(weave)到系统中需要该横切逻辑的Joinpoint处,这样不就达到了将事务管理逻辑从业务逻辑实现中剥离出来的目的了吗? 现在,我们要做的实际上就是提供一个Interceptor,在业务方法执行开始之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务,有了spring的编程式事务管理API的支持,实现这样的一个Interceptor对于我们来说应该就很容易了:
public class PrototypeTransactionInterceptor implements MethodInterceptor { private PlatformTransactionManager transactionManager; public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); TransactionDefinition definition = getTransactionDefinitionByMethod(method); TransactionStatus txStatus = transactionManager.getTransaction(definition); Object result = null; try { result = invocation.proceed(); } catch(Throwable t) { if(needRollbackOn(t)) { transactionManager.rollback(txStatus); } else { transactionManager.commit(txStatus); } throw t; } transactionManager.commit(txStatus); return result; } private boolean needRollbackOn(Throwable t) { // TODO ... return false; } private TransactionDefinition getTransactionDefinitionByMethod(Method method) { // TODO ... return null; } public PlatformTransactionManager getTransactionManager() { return transactionManager; } public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } }在实现这样的一个Interceptor的过程中,我们会发现有些逻辑不好确定:
-
针对每一个方法业务方法的拦截,需要知道该方法是否需要事务支持,如果需要,针对该事务的TransactionDefinition相关信息又从哪里获得?
-
调用方法过程中如果抛出异常的话,如何对这些异常进行处理,哪些异常抛出的情况下需要回滚事务,哪些异常抛出的情况下又不需要?
spring提供了用于声明式事务管理的一切设施(使用org.springframework.transaction.interceptor.TransactionInterceptor实现替代我们的PrototypeTransactionInterceptor), 对于我们来说,所要做的仅仅是决定要使用xml元数据驱动的还是使用Annotation元数据驱动的声明式事务管理啦!
Tip
啥?我还没有公布第二个问题是如何解决的?还记得在TransactionDefinition部分提到的用于声明式事务的TransactionAttribute定义吗? 其实仅需要将“在什么异常类型抛出的情况下需要回滚事务”的信息补充到默认的TransactionDefintion定义中,用于事务处理的Interceptor根据获得的TransactionDefinition提供的该信息就能够决定出现异常的情况下应该如何结束事务。 而TransactionAttribute就是追加了异常回滚信息的TransactionDefinition定义,通过TransactionAttribute实现类代替默认的TransactionDefintion实现类创建事务不就结了?
spring允许你在ioc容器的配置文件中直接指定事务相关的元数据,从1.x版本发展到2.x版本,不管配置形式如何变化,所要达到的目的以及底层的机制却是不变的,配置形式的演化只是为了能够更加简洁方便。
从spring1.x到2.x,大体上来说,我们可以使用以下四种配置方式在ioc容器的配置文件中指定事务需要的元数据:
-
使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor
-
使用“一站式”的TransactionProxyFactoryBean
-
使用BeanNameAutoProxyCreator
-
使用Spring2.x的声明事务配置方式
总是FooService,BarService的,可能大家都看烦了,所以,我们这回打算换个口味。假设我们要搭建一个QuoteService, 先暂且不管它是应用于一般的证券系统还是外汇系统,总之,通过它我们能够查询报价信息并且必要的话,也可以更新底层数据内容。 现在的QuoteService不是一个远程服务,它的目的也很简单,基本上就是实现一个Quote信息的基本管理功能(在Spring Remoting一章我们将介绍如何通过spring将QuoteService以远程服务的方式暴露出来)。
我们首先定义的是对应QuoteService的服务接口,面向接口编程为我们管理问题域提供了很好的抽象方式:
public interface IQuoteService { Quote getQuate(); Quote getQuateByDateTime(DateTime dateTime); void saveQuote(Quote quote); void updateQuote(Quote quote); void deleteQuote(Quote quote); }QuoteService实现了IQuoteService接口:
public class QuoteService implements IQuoteService { private JdbcTemplate jdbcTemplate; public Quote getQuate() { return (Quote)getJdbcTemplate().queryForObject("", new RowMapper(){ public Object mapRow(ResultSet rs, int row) throws SQLException { Quote quote = new Quote(); // ... return quote; }}); } public Quote getQuateByDateTime(DateTime dateTime) { throw new NotImplementedException(); } public void saveQuote(Quote quote) { throw new NotImplementedException(); } public void updateQuote(Quote quote) { throw new NotImplementedException(); } public void deleteQuote(Quote quote) { throw new NotImplementedException(); } public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }只是为了说明针对QuoteService的事务管理,所以,暂且直接JdbcTemplate进行数据访问,系统如果需要进一步的抽象可以将数据访问逻辑提取DAO一层以屏蔽数据访问技术造成的差异。 我们仅给出一个方法的实现,其余的先抛出NotImplementedException,当系统运行的时候,这可以告诉我们还有工作没有完成。
在有了QuoteService这个简单的模型之后,我们开始进入正题。
xml元数据驱动的声明式事务本质上来说,是为TransactionInterceptor提供需要的TransactionDefiniton(确切的说,是TransactionAttribute)信息。 而至于说将TransactionInterceptor管理的事务加诸到相应的业务对象上的过程,则就纯粹是一个AOP的配置过程了。既然如此,最基本的方式当然就是直接通过ProxyFactoryBean(或者ProxyFactory)进行事务管理这一横切关注点到系统的织入工作啦! 所以,使用ProxyFactoryBean和TransactionInterceptor为我们的QuoteService添加声明式事务,看起来就是这个样子:
<beans> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost/databaseName"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_20</prop> <prop key="saveQuote">PROPAGATION_REQUIRED</prop> <prop key="updateQuote">PROPAGATION_REQUIRED</prop> <prop key="deleteQuote">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="quoteServiceTarget" class="...QuoteService"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="quoteService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="quoteServiceTarget"/> <property name="proxyInterfaces" value="...IQuoteService"/> <property name="interceptorNames"> <list> <value>transactionInterceptor</value> </list> </property> </bean> <bean id="client" class="...QuoteSerivceClient"> <property name="quoteService" ref="quoteService"/> </bean> </beans>QuoteSerivceClient是一般意义上使用QuoteService的应用类,当我们通过它调用QuoteService的业务方法的时候,除了getQuote()方法之外,其他方法(比如getQuoteService().saveQuote(quote))因为抛出了“ unchecked”的NotImplementedException,从而导致事务自动回滚,这说明我们的声明式事务生效了,不是吗?
1094 [main] DEBUG org.springframework.transaction.support.TransactionSynchronizationManager - Initializing transaction synchronization 1094 [main] DEBUG org.springframework.transaction.interceptor.TransactionInterceptor - Getting transaction for [...IQuoteService.saveQuote] 1094 [main] DEBUG org.springframework.transaction.interceptor.TransactionInterceptor - Completing transaction for [...IQuoteService.saveQuote] after exception: org.apache.commons.lang.NotImplementedException: Code is not implemented 1094 [main] DEBUG org.springframework.transaction.interceptor.RuleBasedTransactionAttribute - Applying rules to determine whether transaction should rollback on org.apache.commons.lang.NotImplementedException: Code is not implemented 1094 [main] DEBUG org.springframework.transaction.interceptor.RuleBasedTransactionAttribute - Winning rollback rule is: null 1094 [main] DEBUG org.springframework.transaction.interceptor.RuleBasedTransactionAttribute - No relevant rollback rule found: applying superclass default 1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Triggering beforeCompletion synchronization 1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback 1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@b76fa] 1094 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Triggering afterCompletion synchronization 1094 [main] DEBUG org.springframework.transaction.support.TransactionSynchronizationManager - Clearing transaction synchronization现在让我们回头探究一下在使用ProxyFactoryBean和TransactionInterceptor进行声明式事务的过程中到底有哪些奥秘以及需要我们注意的地方。
声明式事务要起作用,需要几个方面协同工作,这包括事务要管理的具体数据资源类型,采用的数据访问技术,特定的事务管理器实现,以及TransactionInterceptor提供的业务方法拦截功能。 具体到我们的QuoteService来说,我们要对数据库提供的数据资源进行事务操作,所以,配置中需要dataSource的定义,这是事务操作的基础:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost/databaseName"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean>要对数据资源进行访问,QuoteService采用的是JDBC的方式,即直接使用spring提供的JdbcTemplate,那么,我们需要提供JdbcTemplate以及特定于Jdbc的事务管理器(DataSourceTransactionManager):
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>数据访问技术与事务管理器的类型是一一对应的,否则就有“ 驴唇不对马嘴”之嫌,让美国总统来发号施令中国的事务,我们当然可以不理他。 所以,如果你的“ QuoteService”要用Hibernate进行数据访问,那么,请提供HibernateTransactionManager作为事务管理器; 如果你的“ QuoteService”要用JDO进行数据访问,请将DataSourceTransactionManager替换为JdoTransactionManager,依此类推。
TransactionInterceptor是整个声明式事务的主体,要让他发挥事务管理的职能,请为他提供一个事务管理器;要让他知道该为哪个方法加诸什么事务,是否要加诸事务,请为他提供必要的映射信息:
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_20</prop> <prop key="saveQuote">PROPAGATION_REQUIRED</prop> <prop key="updateQuote">PROPAGATION_REQUIRED</prop> <prop key="deleteQuote">PROPAGATION_REQUIRED</prop> </props> </property> </bean>TransactionInterceptor所需要的业务对象上每个业务方法的事务管理信息通过org.springframework.transaction.interceptor.TransactionAttributeSource接口来获取:
public interface TransactionAttributeSource { TransactionAttribute getTransactionAttribute(Method method, Class targetClass); }TransactionAttributeSource的具体实现类通常以不同的形式存储了从不同位置获取的事务管理元数据信息, 比如NameMatchTransactionAttributeSource将以方法名作为映射信息的Key,对相应的元数据进行存储;MethodMapTransactionAttributeSource则直接以对应每个方法的Method实例作为Key来存储元数据对应的映射信息; 而AnnotationTransactionAttributeSource则是直接从源码中的Annotation中获取对应每一个业务方法的事务管理信息。
我们通常通过两个属性为TransactionInterceptor设置需要的TransactionAttributeSource:
-
transactionAttributes属性. 我们刚才就是使用transactionAttributes属性为TransactionInterceptor设置的映射信息,transactionAttributes是Properties类型, TransactionInterceptor内部将通过transactionAttributes提供的信息构造一个NameMatchTransactionAttributeSource类型的TransactionAttributeSource使用。
-
transactionAttributeSource属性. transactionAttributeSource属性就是直接可用的TransactionAttributeSource类型,但是我们在ioc容器的配置文件中指定的是String形式,所以, 容器将通过org.springframework.transaction.interceptor.TransactionAttributeSourceEditor对String形式的值进行一个转换,再设置给TransactionInterceptor, 转换后的具体TransactionAttributeSource实现类为MethodMapTransactionAttributeSource。同样的元数据,通过transactionAttributeSource设置如下:
<property name="transactionAttributeSource"> <value> org.spring21.package.IQuoteService.getQuate*=PROPAGATION_SUPPORTS,readOnly,timeout_20 org.spring21.package.IQuoteService.saveQuote=PROPAGATION_REQUIRED org.spring21.package.IQuoteService.updateQuote=PROPAGATION_REQUIRED org.spring21.package.IQuoteService.deleteQuote=PROPAGATION_REQUIRED </value> </property>
唯一需要注意的就是现在需要指定全限定的类名以及方法名。Note
也可以通过transactionAttributeSources属性指定元数据信息,它是transactionAttributeSource的复数形式
<bean id="quoteServiceTarget" class="...QuoteService"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="quoteService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="quoteServiceTarget"/> <property name="proxyInterfaces" value="...IQuoteService"/> <property name="interceptorNames"> <list> <value>transactionInterceptor</value> </list> </property> </bean> <bean id="client" class="...QuoteSerivceClient"> <property name="quoteService" ref="quoteService"/> </bean>只不过,一定不要把“ quoteServiceTarget”当作“ quoteService”而注入给需要的对象,否则,事务管理不生效那就怨不得别人了。
元数据中事务属性指定规则
以String形式给出的事务属性是有一定规则的,这个规则由org.springframework.transaction.interceptor.TransactionAttributeEditor类的逻辑来界定, TransactionAttributeEditor负责将String形式给出的事务属性转换为具体的TransactionAttribute实例(RuleBasedTransactionAttribute)。
String形式的事务属性规则如下:
PROPAGATION_NAME,[ISOLATION_NAME],[readOnly],[timeout_NNNN],[+Exception1],[-Exception2]除了PROPAGATION_NAME是必须的之外,其他规则可以根据需要决定是否需要指定,各个区段之间以逗号分隔:
-
PROPAGATION_NAME. 对应org.springframework.transaction.TransactionDefinition中定义的与传播行为相关的常量名, 即PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_SUPPORTS,PROPAGATION_MANDATORY,PROPAGATION_NESTED,PROPAGATION_NOT_SUPPORTED和PROPAGATION_NEVER。
-
ISOLATION_NAME. 对应org.springframework.transaction.TransactionDefinition中定义的与Isolation相关的常量名, 即 ISOLATION_DEFAULT ,ISOLATION_READ_UNCOMMITTED,ISOLATION_READ_COMMITTED,ISOLATION_REPEATABLE_READ和ISOLATION_SERIALIZABLE。
-
readOnly. 如果你需要标明当前事务为只读事务,则追加该值即可。
-
timeout_NNNN. 指定事务的超时时间,需要以“timeout_”为前缀,后面加数字的形式,如timeout_20限定超时时间为20秒。
-
[+Exception1],[-Exception2]. 自定义异常回滚规则,前缀加号(+)后跟具体的异常类型表示即使业务方法抛出该异常也同样提交事务;前缀减号(-)后跟具体的异常类型表示业务方法抛出该异常的时候回滚事务。 比如,当业务方法抛出QuoteException的时候,我们想要事务回滚,则可以这样指定:
serviceMethod=PROPAGATION_REQUIRED,-QuoteException
因为“unchecked exception”默认情况下会自动回滚,所以,通过自定义异常回滚规则主要是指定“checked exception”类型的应用异常。
使用ProxyFactoryBean和TransactionInterceptor实现声明式事务可以从最低层次上理解spring提供的声明式事务是如何运作的,不过,为了减少配置量,进一步提高开发效率, 我们会探索更加便捷的方式,TransactionProxyFactoryBean则更近的向这一点迈进。
TransactionProxyFactoryBean是专门面向事务管理的“ProxyFactoryBean”,它直接将TransactionInterceptor纳入自身进行管理, 使用TransactionProxyFactoryBean代替ProxyFactoryBean进行声明式事务管理,不需要单独声明TransactionInterceptor的bean定义,有关事务的元数据,事务管理器等信息全都通过TransactionProxyFactoryBean的bean定义指定就可以。
这样,同样针对QuoteService的声明式事务管理,使用TransactionProxyFactoryBean后的样子如下:
<beans> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost/dbName"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="quoteServiceTarget" class="...QuoteService"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="quoteService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="target" ref="quoteServiceTarget"/> <property name="proxyInterfaces" value="...IQuoteService"/> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_10</prop> <prop key="saveQuote">PROPAGATION_REQUIRED</prop> <prop key="updateQuote">PROPAGATION_REQUIRED</prop> <prop key="deleteQuote">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="client" class="...QuoteSerivceClient"> <property name="quoteService" ref="quoteService"/> </bean> </beans>现在,TransactionProxyFactoryBean集ProxyFactoryBean,TransactionInterceptor功能于一身,一心一意的为声明式事务管理做贡献了。
不过,你也看到了,针对TransactionProxyFactoryBean的bean定义看起来不是那么苗条,如果每一个需要声明式事务的业务对象都来这么一下子, 那配置量可着实不轻松,所以,通常情况下,我们会使用bean定义模板的方式来简化使用TransactionProxyFactoryBean进行声明式事务的配置:
<bean id="txProxyFactoryBean" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract="true"> <property name="proxyInterfaces" value="...IQuoteService"/> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributes"> <props> <prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_10</prop> <prop key="saveQuote">PROPAGATION_REQUIRED</prop> <prop key="updateQuote">PROPAGATION_REQUIRED</prop> <prop key="deleteQuote">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="quoteService" parent="txProxyFactoryBean"> <property name="target" ref="quoteServiceTarget"/> </bean> <bean id="quoteService2" parent="txProxyFactoryBean"> <property name="target" ref="otherQuoteServiceTarget"/> </bean> ...将共有的一些属性提取到“ txProxyFactoryBean”的bean定义模板中,就可以减少每次配置单独业务对象对应的bean定义的工作量。
相对于直接使用ProxyFactoryBean和TransactionInterceptor来说,使用TransactionProxyFactoryBean可以将声明式事务相关的关注点集中起来, 一定程度上减少了配置的工作量。不过话又说回来了,当应用程序中仅有少量的业务对象需要配置声明式事务的话,配置的工作量还算说的过去,一旦需要声明式事务的业务对象数量增加, 采用这种近乎手工作坊式的配置方式就会拖开发的后腿了。这个时候,我们自然会想到AOP中的自动代理机制,而下面正是针对如何使用自动代理对声明式事务进行管理的内容...
使用BeanNameAutoProxyCreator进行声明式事务管理进一步的简化了配置的工作,当所有的声明式事务相关装备一次到位之后,要为新的业务对象添加声明式事务支持, 唯一要做的就是在你为该业务对象添加bean定义的时候,同时将它的beanName添加到BeanNameAutoProxyCreator管理的“beanNames”列表中。
使用BeanNameAutoProxyCreator为业务对象提供声明式事务支持,通常配置如下:
<beans> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost/databaseName"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager" ref="transactionManager"/> <property name="transactionAttributeSource"> <value> org.spring21.package.IQuoteService.getQuate*=PROPAGATION_SUPPORTS,readOnly,timeout_20 org.spring21.package.IQuoteService.saveQuote=PROPAGATION_REQUIRED org.spring21.package.IQuoteService.updateQuote=PROPAGATION_REQUIRED org.spring21.package.IQuoteService.deleteQuote=PROPAGATION_REQUIRED </value> </property> </bean> <bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="interceptorNames"> <list> <value>...IQuoteService</value> </list> </property> <property name="beanNames"> <list> <idref bean="quoteService"/> ... </list> </property> </bean> <bean id="quoteService" class="...QuoteService"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="client" class="...QuoteSerivceClient"> <property name="quoteService" ref="quoteService"/> </bean> </beans>现在,我们只需要正常的向IoC容器的配置文件中追加相应的业务对象bean定义即可,BeanNameAutoProxyCreator将根据TransactionInterceptor提供的事务管理功能自动为添加到它的“ beanNames”列表的所有业务对象自动添加事务支持(当然,本质上是为其生成动态代理对象)。
无论应用中业务对象数量多少,使用BeanNameAutoProxyCreator都可以很便捷的处理这些业务对象的声明式事务需求。 不过,可能在实际的开发过程中,你依然感觉使用BeanNameAutoProxyCreator有其不够便捷之处,好消息就是,如果你的应用程序可以或者已经升级到spring2.x,那么,使用基于XSD的配置方式吧!
spring2.x后提供的基于Xml schema的配置方式专门为事务管理提供了单独的一个命名空间用于简化配置,结合新的aop命名空间, 现在的声明式事务管理看起来要清晰许多:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:lang="http://www.springframework.org/schema/lang" 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-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd"> <aop:config> <aop:pointcut id="txServices" expression="execution(* cn.spring21.unveilspring.IQuoteService.*(..))"/> <aop:advisor pointcut-ref="txServices" advice-ref="txAdvice"/> </aop:config> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="getQuate*" propagation="SUPPORTS" read-only="true" timeout="20"/> <tx:method name="saveQuote"/> <tx:method name="updateQuote"/> <tx:method name="deleteQuote"/> </tx:attributes> </tx:advice> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost/dbName"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="quoteService" class="...QuoteService"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="client" class="...QuoteSerivceClient"> <property name="quoteService" ref="quoteService"/> </bean> </beans><tx:advice>是专门为声明事务Advice设置的配置元素,底层当然还是我们的TransactionInterceptor,仅仅是披一件新衣裳而已。 <tx:advice>的“ transaction-manager”指定了它要使用的事务管理器是哪一个,如果容器中事务管理器的beanName恰好就是“ transactionManager”,那么可以不明确指定。 <tx:advice>内部由<tx:attributes>提供声明式事务所需要的元数据映射信息,每一条映射信息对应一个<tx:method/>元素声明。 <tx:method/>只有“ name”属性是必须指定的,其他的属性代表事务定义的其他内容,比如propagation用于指定传播行为,isolation用于指定隔离度(Isolation),timeout用于指定事务的超时时间等等, 如果不明确指定的话,将采用DefaultTransactionDefinition的设置内容。下表是<tx:method/>可用的属性的详细列表 [11]:
Table 1.2. <tx:method/>属性对照表
name | 事务元数据将要加诸于上的业务方法名称,可以使用*通配符。 |
propagation | 事务的传播行为,不明确指定的话,默认值为REQUIRED,你会发现,该名称去掉了TransactionDefintion中常量名称的前缀,在指定的时候,需要注意。 |
isolation | 用于指定事务的隔离度,不明确指定,默认值采用DEFAULT,也是TransactionDefintion中常量名称去掉前缀。 |
timeout | 事务的超时时间,默认值为-1。 |
read-only | 指定事务是否为只读事务,默认值为false; |
rollback-for | 用于指定能够触发事务回滚的异常类型,比如rollback-for=“cn.spring21.unveilspring.QuoteException”, 如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。 |
no-rollback-for | 与rollback-for正好相反,即使抛出no-rollback-for指定的异常类型也“不”回滚事务。 |
通过<tx:advice>指定的事务信息需要通过SpringAOP的支持织入到具体的业务对象,所以,剩下的工作实际上是AOP的配置了:
<aop:config> <aop:pointcut id="txServices" expression="execution(* cn.spring21.unveilspring.IQuoteService.*(..))"/> <aop:advisor pointcut-ref="txServices" advice-ref="txAdvice"/> </aop:config>在SpringAOP一章我们已经说过,<aop:config>底层也是依赖于自动代理(Autoproxy)机制,所以,我一直强调,新的基于XSD的配置方式仅仅是换了一身简洁明快的外衣, 而我想让大家看到的是外衣里面的东西。
Note
更多使用<tx:advice>的内容可以参照spring2.x之后的参考文档,其中有更多详细内容,比如配置多个<tx:advice>以区分不同的事务需求等内容。
随着Java5(Tiger)的发布,Annotation越来越受到开发人员的关注和喜爱,如果你的应用程序构建于Java5或者更高版本的虚拟机之上的话,那么恭喜你,现在你也可以使用Spring提供的基于Annotation的声明式事务管理啦[12]。
Annotation元数据驱动的声明式事务管理的基本原理是,将对应业务方法的事务元数据直接通过Annotation标注到业务方法或者业务方法所在的对象之上, 然后在业务方法执行期间,通过反射(Reflection)读取标注在该业务方法之上的Annotation所包含的元数据,最终将根据读取的信息为业务方法构建事务管理的支持。
spring定义了org.springframework.transaction.annotation.Transactional用于标注业务方法所对应的事务元数据信息,通过Transactional,我们可以指定与<tx:method/>几乎相同的信息,当然,现在不用指定方法名称了, 因为Transactional直接标注到了业务方法或者业务方法所在的对象定义之上。通过查看Transactional的定义,我们可以获取所有可以指定的事务定义内容:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }要为我们的QuoteService添加基于Annotation的声明式事务管理,需要为其添加Transactional以标注必要的事务管理信息:
@Transactional public class QuoteService implements IQuoteService { private JdbcTemplate jdbcTemplate; @Transactional(propagation=Propagation.SUPPORTS,readOnly=true,timeout=20) public Quote getQuate() { return (Quote)getJdbcTemplate().queryForObject("SELECT * FROM fx_quote where quote_id=2", new RowMapper(){ public Object mapRow(ResultSet rs, int row) throws SQLException { Quote quote = new Quote(); // ... return quote; }}); } @Transactional(propagation=Propagation.SUPPORTS,readOnly=true,timeout=20) public Quote getQuateByDateTime(DateTime dateTime) { throw new NotImplementedException(); } public void saveQuote(Quote quote) { throw new NotImplementedException(); } public void updateQuote(Quote quote) { throw new NotImplementedException(); } public void deleteQuote(Quote quote) { throw new NotImplementedException(); } public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }如果将@Transactional标注为对象级别的话,对象中的方法将“ 继承”该对象级别上的@Transactional的事务管理元数据信息, 如果某个方法有特殊的事务管理需求,可以在方法级别添加更加详细的@Transactional设定,比如我们的getQuote*()方法。通过将相同的事务管理行为提取到对象级别的@Transactional, 可以有效的减少标注的数量。如果不为@Transactional指定自定义的一些设定,它也会像<tx:method/>那样采用DefaultTransactionDefinition一样的事务定义内容。
仅仅通过@Transactional标注业务对象以及对象中的业务方法并不会为业务方法带来任何事务管理的支持,@Transactional只是一个标志而已,需要我们在执行业务方法的时候通过反射读取这些信息并根据这些信息构建事务才能使这些声明的事务行为生效。 这就好像下面的代码所演示的那样:
... public Quote getQuate() { try { Method method = quoteService.getClass().getDeclaredMethod("getQuate", null); boolean isTxAnnotationPresent = method.isAnnotationPresent(Transactional.class); if(!isTxAnnotationPresent) { return (Quote)quoteService.getQuate(); } Transactional txInfo = method.getAnnotation(Transactional.class); TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); if(!txInfo.propagation().equals(Propagation.REQUIRED)) transactionTemplate.setPropagationBehavior(txInfo.propagation().value()); if(txInfo.readOnly()) transactionTemplate.setReadOnly(true); // ... return (Quote)transactionTemplate.execute(new TransactionCallback(){ public Object doInTransaction(TransactionStatus txStatus) { return quoteService.getQuate(); }}); } catch (SecurityException e) { e.printStackTrace(); // don't do this return null; } catch (NoSuchMethodException e) { e.printStackTrace(); // don't do this return null; } } ...不过,我们不用自己去写这些底层的逻辑了,通过在容器的配置文件中指定如下一行配置,这些搜寻Annotation,读取内容,构建事务等工作全都由spring的IoC容器帮我们搞定:
<tx:annotation-driven transaction-manager="transactionManager"/>所以,使用Annotation元数据驱动的声明式事务管理,基本上就需要做两件事:
-
使用@Transactional标注相应的业务对象以及相关业务方法. 这一步通常由业务对象的开发者统一负责了,@Transactional的使用使得元数据以及业务逻辑的实现全部集中到了一处,有了IDE的支持, 管理起来更是得心应手。
-
在容器的配置文件中设定事务基础设施. 我们需要添加<tx:annotation-driven transaction-manager="transactionManager"/>以便有人使用我们所标注的@Transactional, 并且,需要给出相应的事务管理器,要进行事务管理,没有他可不行。
<tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost/dbName"/> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="quoteService" class="...QuoteService"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="client" class="...QuoteSerivceClient"> <property name="quoteService" ref="quoteService"/> </bean>至此,基于Annotation的声明式事务就算大功告成了。
Tip
spring推荐将@Transactional标注于具体的业务实现类或者实现类的业务方法上, 之所以如此,是因为spring aop可以采用两种方式来生成代理对象(dynamic proxy 或者cglib)。 如果将@Transactional标注于业务接口的定义上,那么,当使用Dynamic proxy机制构建代理对象的时候,读取接口定义上的@Transactional信息是没有问题的; 可是当使用CGLIB构建代理对象的时候,则无法读取接口上定义的@Transactional数据。
本章之初,在事务管理器实现原型的讲解中,我们为您展示了ThreadLocal在Spring的事务管理框架中所起的核心作用,即通过ThreadLocal的使用来避免“connection passing”方式最初的尴尬局面。不过,我们依然认为这只是反映了ThreadLocal使用中的一个侧面,为了让大家更加深入的理解ThreadLocal并在之后的开发活动中灵活使用之,特追加这部分扩展内容。
ThreadLocal是Java语言提供的用于支持“线程局部变量(thread-local variable)”的标准实现类,我们可以在称为“Thread-Specific Storage Pattern”[13]的多线程编程模式中一窥ThreadLocal的典型应用场景。
国内的Java技术社区对ThreadLocal的理解可以说百家争鸣,异常热闹,有的将ThreadLocal和synchronized放在一起讨论,有的说他们之间没有任何关系,还有的说ThreadLocal不是用来解决多线程环境下的对象共享问题的, 这些观点通常会令许多开发人员对ThreadLocal更加迷惑。不能说这些观点是不对的,但笔者认为这些都没有接触到真正的点子上! 那么,ThreadLocal到底是为何而生那?它又是否与之前的观点有什么千丝万缕的联系那? 下面让笔者带您一起来探索这些问题的答案。
单单从程序层面来看,我们所编写的代码实际上是在管理系统中各个对象的相关状态,如果不能够对各个对象状态的访问进行合理的管理,对象的状态将被破坏,进而导致系统的不正常运行。特别是多线程环境下,多个线程可能同时对系统中的单一或者多个对象状态进行访问,如果不能保证在此期间的线程安全,将会把整个系统带向崩溃的边缘。
为了保证整个应用程序的线程安全,我们可以采用多种方式,不过,在此之前,我们不妨先来看看打架与我们管理线程安全问题有何相似之处。现在你就是系统中的一个对象,假设某一天你出门在外遇到三名歹徒(当然,仅仅是假设,不用害怕,呵呵),你毫无退路,只能一搏,那么,你会采用什么样的策略来保证你的人身安全那?毕竟,我们最终的目的都是保证你这个对象的状态始终处于“正确”的状态,对吧?!
策略一,你是个练家子,而且身边并无他人相助,所以,你只能同时对付三名歹徒,为了不受伤害,你是辗转腾挪,左躲右闪,尽量保证每一时刻都只是对付一名歹徒,以便最终能够全身而退。如果把三名歹徒对你的攻击顺序比作三个线程的话,你实际上是在用同步(Synchronization)的方式来管理这三个线程对你的访问;
使用同步(Synchronization)方式来管理多个线程对对象状态的访问以保证应用程序的线程安全是最常用的方式,不过,你也看到了,你需要很辛苦的来对待,稍一不慎,就有可能受伤啊。所以,这个时候我们就在想,我要是孙悟空有多好。
“但见那孙猴子揪一撮猴毛一吹,片刻化作多个分身”,“小的们,给我上”,现在,我们再也不用苦熬了,让一个分身对付一个歹徒(小妖),让他们在各自的线程内各自折腾去吧!不用一对三,当然也就不可能破坏到我的完好状态咯。
这就是策略二,通过避免对象的共享,同样达到线程安全的目的。你想啊,都在各自的线程内跟各自的分身折腾去了,自然也就不会需要同步对单一共享资源的访问啦。ThreadLocal的出现,实际上就是帮助我们以策略二的方式来管理程序的线程安全。只要当前环境允许,能不共享的尽量不共享,反而更容易管理应用程序的线程安全。
综上来说那,Synchronization和ThreadLocal在横向上可能没有任何的关系,但从纵向上看,他们实际上都服务于同一个目的,那就是帮助我们实现应用程序的线程安全。 另外,说ThreadLocal不是用来解决多线程环境下对象共享问题的,也就更好解释了, ThreadLocal的目的是通过避免对象的共享来保证应用程序实现中的线程安全,共享对象是ThreadLocal尽量避免的,如果要管理的对象非要共享,ThreadLocal自然不会理会这码子事儿啦。
既然我们已经了解了ThreadLocal因何而生,现在该是我们探索ThreadLoca又是如何运作的时候了,它到底是如何来完成它的职责的那?
我们虽然是通过ThreadLocal来设置特定于各个线程的数据资源,但ThreadLocal自身不会保存这些特定的数据资源,既然是特定于线程的数据资源,自然是由每一个线程自己来管理啦。 每一个Thread类都有一个ThreadLocal.ThreadLocalMap类型的名为threadLocals的实例变量,它就是保持那些通过ThreadLocal设置给这个线程的数据资源的地方。 当我们通过ThreadLocal的set(data)方法来设置数据的时候,ThreadLocal会首先获取当前线程的引用,然后通过该引用获取当前线程持有的threadLocals,最后,以当前ThreadLocal作为Key,将要设置的数据设置到当前线程:
Thread thread = Thread.currentThread(); ThreadLocalMap threadlocalmap = thread.threadLocals; ... threadlocalmap.set(this, obj);而至于余下的get()之类的方法,基本上也是同样的道理,都是首先取得当前线程,然后根据每个方法的语义对当前线程所持有的threadLocals中的数据进行操作。
实际上,ThreadLocal就好像是一个窗口,通过这个窗口,我们可以将特定于线程的数据资源绑定到当前线程, 我们也可以通过这个窗口获取绑定的数据资源,当然,我们也可以解除之前绑定到当前线程的数据资源。在整个线程的生命周期内,你都可以通过ThreadLocal这个窗口与当前线程打交道。
为了更好的理解Thread与ThreadLocal之间的关系,我们不妨设想一下城市的公交系统:
城市中的各条公交线路就好像我们系统中的那一个个线程,在各条公交线路上,会有相应的公交车辆, 这些公交车辆就好像Thread的threadLocals,用来运送特定于该条线路的乘客(数据资源)。 为了乘客可以乘车或者下车,各条公交线路沿线都会设置多个乘车点(Bus stop),而这些乘车点实际上就是ThreadLocal。 虽然同一个乘车点可能会有多条公交线路共用,但同一时间,乘客只会搭乘他要乘坐并且当前经过的公交车。这与ThreadLocal和Thread的关系是类似的, 同一个ThreadLocal虽然可以为多个线程指定数据资源,但只会将数据资源指定到当前的线程。
至此,您是否再也不会对ThreadLocal感觉神秘了那?
ThreadLocal的概念和提供的功能其实很简单,但如果能够充分发挥ThreadLocal的能力, 将会为我们的开发工作带来意想不到的效果。
基本上,我们可以从两个方面来看待并灵活应用ThreadLocal:
-
横向上看,我们是更注重于ThreadLocal横跨多个线程的能力, 这当然是ThreadLocal最初的目的之所在, 为了以更加简单的方式来管理应用程序的线程安全, ThreadLocal干脆将没有必要共享的对象不共享,直接为每一个线程分配一份各自特定的数据资源。
-
纵向上看,我们则着眼于ThreadLocal于单一线程内可以发挥的能力, 通过ThreadLocal设置的特定于各个线程的数据资源可以随着所在线程的执行流程“随波逐流”。
在充分发挥ThreadLocal两方面能力的基础上,我们可以发掘出以下几种ThreadLocal的应用场景:
-
管理应用程序实现中的线程安全. 对于某些Stateful的或者非线程安全的对象,我们可以在多线程程序中为每一个线程都分配相应的副本(copy), 而不是让多个线程共享该类型的某个单一对象,从而避免了需要协调多个线程对这些对象进行访问的“危险”工作。
在使用JDBC进行数据访问的过程中,Connection对象就属于那种Stateful并且非线程安全的类, 所以,为了保证多个线程使用Connection进行数据访问过程中的安全,我们通过ThreadLocal为每一个线程分配了一个他们各自持有的Connection, 从而避免了对单一Connection资源的争用。毕竟,在JDBC中是一个Connection对应一个事务, 如果所有的线程都共用一个Connection的话,那整个事务管理就有点儿失控的感觉了。
在本章开始部分,当以简化的形式向你展示spring的事务框架是如何对Connection进行管理的时候,我们只是强调了使用ThreadLocal来避免“Connection Passing”的尴尬, 而实际上,通过ThreadLocal来保证Connection在多线程环境下的正确使用,应该也是spring的事务框架使用ThreadLocal进行Connection管理的原因之一。
-
实现当前程序执行流程内的数据传递. 这种场景下,我们更多关注的是在单一的线程环境中使用ThreadLocal,当剥离“线程安全”等因素的考虑之后, “connecton passing”实际上就可以看作这个场景下的一例。
除此之外,我们也可以通过ThreadLocal来跟踪保存线程内的日志序列,在程序执行的任何必要执行点将系统跟踪信息添加到ThreadLocal,然后在合适的时点取出分析; 我们还可以通过ThreadLocal来保存某种全局变量,在线程内执行流程的某个时点设置该全局变量,然后在合适的位置获取其值以做某些判断工作等等。 只要你愿意,任何合适的数据都可以通过ThreadLocal在单一线程内传递数据这一功能进行传递。
采用ThreadLocal进行当前执行流程内的数据传递可以避免耦合性很强的方法参数形式的数据传递方式,但这有些像是让数据随着“暗流”漂泊的意思, 一旦处理不当就会出现“触礁”之类的事故,比如资源没有适当的清理导致系统行为差异之类,所以,通常应该通过一组框架类来规范并屏蔽对ThreadLocal的直接操作,尽量避免应用代码的直接接触。
-
某些情况下的性能优化. 有些情况下,系统中一些没有必要共享的对象被设置成了共享,为了保证应用程序的线程安全以及对象状态的正确, 我们往往就得通过同步等方式对多线程的访问进行管理和控制,这个时候,各个线程在走到这个共享对象的时候就得排队, 一个一个的对该共享对象进行访问,显然,这将严重影响系统的处理性能,去银行取款的时候,如果就一个窗口在营业,而你又急着取钱,可以想象一下你现在是一种什么样的心情。 所以,能够避免共享的时候,就尽量不要共享,多开几个营业窗口,要比单一营业窗口的处理速度快的多。
某些情况下,通过ThreadLocal这种“以空间换时间”的方式来管理对象访问,可以收到更好的响应效果。
-
“per-thread Singleton”. 当某项资源的初始化代价有些大,并且在整个执行流程中还会多次访问它的时候, 为了避免在访问的时候需要每次都去初始化该项资源,我们可以第一次将该资源初始化完成之后,直接通过ThreadLocal将其绑定到当前线程, 之后,所有对该资源的访问都从当前线程获取即可。
这实际上与“实现当前程序执行流程内的数据传递”的应用场景很相似,不过,该场景更侧重于资源管理, 所以,单独罗列与此也不为过啦。
在spring的数据访问一章我们介绍了如何实现一个简单的AbstractRountingDataSource原型实现来管理多个数据源之间的切换功能, 不过,当时的原型实现可能让你觉得过于简单而不甚真实,所以,这里我们引入ThreadLocal来协助管理多数据源切换的条件,以期抛砖引玉,使得大家在日常的开发工作中灵活运用ThreadLocal带给我们的便利。
在多数据源的切换过程中,切换的条件可能随着应用程序的需求而各异, 而且,通常不会像我们的AbstractRoutingDataSource原型实现那样只需要内部条件就可以实现数据源切换的判断, 更多时候我们会需要外部条件的介入,这个时候就会有一个问题,如何为AbstractRoutingDataSource的实现子类传入这些外部条件相关数据?!ThreadLocal这个时候就可以派上用场。
我们的思路是,通过ThreadLocal保存每一个数据源所对应的标志(该标志我们以枚举类的形式给出), AbstractRoutingDataSource在通过determineCurrentLookupKey()获取对应数据源的键值的时候,从ThreadLocal获取当前线程所持有的数据源对应标志然后返回。 而至于说什么情况下使用哪一个具体的数据源的问题,则是由应用程序的需求来决定,只要在必要的地方将所要使用的具体数据源的对应标志通过ThreadLocal绑定到当前线程即可。
Procedure 1.3. 基于ThreadLocal管理多数据源切换条件的AbstractRoutingDataSource实现流程
-
假设我们有MAIN,INFO和DBLINK三个数据源可用,第一步要做的事情就是先给出一个枚举类, 其中定义了对应这三个数据源的标志:
public enum DataSources { MAIN,INFO,DBLINK; }
-
我们定义所使用的ThreadLocal,没有“车站”,我们可没法上车啊:
public class DataSourceTypeManager { private static final ThreadLocal<DataSources> dsTypes = new ThreadLocal<DataSources>(){ @Override protected DataSources initialValue() { return DataSources.MAIN; } }; public static DataSources get() { return dsTypes.get(); } public static void set(DataSources dataSourceType) { dsTypes.set(dataSourceType); } public static void reset() { dsTypes.set(DataSources.MAIN); } }
-
有了标志枚举类和相应的ThreadLocal定义之后,我们就可以实现我们的AbstractRoutingDataSource了:
public class ThreadLocalVariableRountingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceTypeManager.get(); } }
-
现在我们需要将ThreadLocalVariableRountingDataSource以及相关的依赖注册到IoC容器当中:
<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value=".."/> <property name="driverClassName" value=".."/> <property name="username" value=".."/> <property name="password" value="."/> </bean> <bean id="infoDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value=".."/> <property name="driverClassName" value=".."/> <property name="username" value=".."/> <property name="password" value="."/> </bean> <bean id="dblinkDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value=".."/> <property name="driverClassName" value=".."/> <property name="username" value=".."/> <property name="password" value="."/> </bean> <bean id="dataSource" class="..ThreadLocalVariableRountingDataSource"> <property name="defaultTargetDataSource" ref=""/> <property name="targetDataSources"> <map key-type="..DataSources"> <entry key="MAIN" value-ref="mainDataSource"/> <entry key="INFO" value-ref="infoDataSource"/> <entry key="DBLINK" value-ref="dblinkDataSource"/> </map> </property> </bean>
注意,我们在配置ThreadLocalVariableRountingDataSource所使用的多个目标数据源的时候,使用了<map>的key-type属性指明了键值的类型, 否则,你得通过其他的方式来确定枚举类的各值作为key与目标数据源之间的对应关系。 -
万事俱备之后,你可以使用如下代码使得数据源的切换生效:
DataSourceTypeManager.set(DataSources.INFO); // 或者 DataSourceTypeManager.set(DataSources.DBLINK); ...
此后通过ThreadLocalVariableRountingDataSource所进行的数据访问则会使用你所设定的具体数据源。至于说以上代码你要放在什么地方,可以因应用程序而异,比如你可以在程序或者某个线程启动之后进行设置; 你也可以在某个AOP的Interceptor中根据条件判断来进行设定以实现数据源的切换,等等。
至此,打完收功。 怎么样?你现在是否对ThreadLocal的应用跃跃欲试了那?
Strategy模式的本意是封装一系列可以互相替换的算法逻辑,使得具体算法的演化独立于使用它们的客户端代码。 为了理解为什么要这么做,我们不妨来看一个具体的场景:
在一个信贷系统中,通常会提供多种还款方式,比如等额本金还款方式,等额本息还款方式,一次还本付息方式等等, 那么,针对每一位顾客所选择的还款方式,我们就需要按照这些还款方式的具体逻辑为顾客计算每次所需要归还的本金以及利息的额度, 如果要我们来实现这个根据还款方式计算额度的逻辑,我们会怎么做那?
public RepaymentDetails calculateRepayment(BigDecimal totalAmount,String customerId) { RepaymentDetails details = new RepaymentDetails(); Object type = getRepaymentTypeByCustomerId(customerId); if(isEqualInterestRepaymentType(type)) { BigDecimal interest = getEqualInterestOfCentrelBank(); YearMonthDay repaymentInterval = getRepaymentIntervalByCustomerId(customerId); // carry out caculation according to totalAmount and other data } if(isEqualPrincipalRepaymentType(type)) { BigDecimal interest = getStandardInterestOfCentrelBank(); YearMonthDay repaymentInterval = getRepaymentIntervalByCustomerId(customerId); // carry out caculation according to totalAmount and other data } if(isOnceForAll(type)) { BigDecimal interest = getStandardInterestOfCentrelBank(); // carry out caculation according to totalAmount and other data } ... return details; }当然,你可以对这些代码做进一步的改进,但是,如果总体结构上不做任何变更的话,这种实现方式暴露的问题会依然存在:
-
客户端代码与算法逻辑代码相互混杂, 导致客户端代码的过于复杂并且后期难以维护;
-
混杂的算法逻辑代码与客户端代码耦合性太强,算法的变更或者添加新的算法都会直接导致客户端代码的调整,使得客户端代码和算法逻辑代码无法独立演化;
-
几乎同一逻辑单元内实现的各种算法无可避免的需要多重的条件语句来区分针对不同算法所使用的数据或者对应算法的特定逻辑实现;
使用Strategy模式来重构这段代码的话,我们首先通过RepaymentStrategy定义来抽象还款逻辑算法, 然后,针对不同的还款方式,给出RepaymentStrategy定义的不同实现。对于使用还款逻辑的客户端代码来说, 它只需要获取相应的RepaymentStrategy引用,并调用接口暴露的计算接口即可:
客户端代码只需要跟策略接口打交道,而算法的变更以及添加对于使用策略接口进行计算操作的客户端代码来说几乎没有任何影响。
Strategy模式虽然定义上强调的是对算法的封装,但我们不应该只着眼“算法”一词,实际上, 只要能够有效的剥离客户端代码与特定关注点之间的依赖关系,Strategy模式就应该进入考虑之列,在这一点上, spring框架的事务抽象就是一个很好的范例,通过将使用不同事务管理API进行事务管理的界定行为进行统一的抽象, 客户端代码可以透明的方式使用PlatformTransactionManager这一策略接口进行事务界定,即使具体的事务策略需要变更, 对于客户端代码来说也不会造成过大的冲击。
spring框架中使用Strategy模式的地方很多,除了本章的事务抽象框架,还包括以下几处:
-
IoC容器根据bean定义的内容实例化相应bean对象的时候,会根据情况决定使用反射还是使用cglib来实例化相应的对象。 InstantiationStrategy是容器使用的实例化策略的抽象接口,spring框架默认提供了CglibSubclassingInstantiationStrategy和SimpleInstantiationStrategy两个具体实现类。
-
spring的validation框架中,org.springframework.validation.Validator定义也是一个策略接口, 具体的实现类将根据具体场景提供不同的验证逻辑,而这些具体验证逻辑的差异性,对于使用Validator进行数据验证的客户端代码来说,则是透明的。
Strategy模式的重点在于通过统一的抽象向客户端屏蔽其所依赖的具体行为,但该模式并没有关注客户端代码应该如何来使用这些行为。 一般来讲,客户端代码使用Strategy模式的方式可以简单划分为两种:
-
客户端整个生命周期内只依赖于单一的策略. Spring提供的事务抽象可以归属这一类情况。使用PlatformTransactionManager进行事务界定的客户端代码在其整个生命周期内只依赖于一个PlatformTransactionManager的实现类, 或者DataSourceTransactionManager,或者HibernateTransactionManager等等,这样的情况比较容易处理,直接为客户端代码注入所需要的策略实现类即可。
-
客户端整个生命周期内可能动态的依赖多个策略. 比如我们的还款场景中,客户端可能需要根据每一个顾客所选择的还款方式来决定使用哪一个策略实现类为其计算对应的还款明细, 对于这种情况,你会发现,Strategy模式通常宣称的可以避免多重条件语句的问题其实仅仅是将其转移给了客户端代码而已:
RepaymentStrategy strategy = fallbackStrategy(); ... public RepaymentDetails calculateRepayment(BigDecimal totalAmount,String customerId) { Object type = getRepaymentTypeByCustomerId(customerId); if(isEqualInterestRepaymentType(type)) { strategy = EqualInterestStrategy(); } if(isEqualPrincipalRepaymentType(type)) { strategy = EqualPrincipalStrategy(); } if(isOnceForAll(type)) { strategy = OnceForAllStrategy(); } ... return strategy.performCalculation(); }
不过,如果你想真正的避免多重条件语句的话,也不是没有办法,最简单的方法就是提前准备一个具体策略类型与其对应条件之间的关系映射。 对于还款的场景来说,我们可以这么做:-
在客户端代码中声明一个对关系映射的依赖:
public class StrategyContext { private Map<Object,RepaymentStrategy> strategyMapping; ... // setters and getters }
-
通过ioc容器注入所有客户端可能动态依赖的策略实现类实例:
<bean id="strategyContext" class="...StrategyContext"> <property name="strategyMapping"> <ref local="strategyMapping"/> </property> </bean> <util:map id="strategyMapping"> <entry key="EQAUL_INTEREST"> <bean class="...EqualInterestStrategy"></bean> </entry> <entry key="EQAUL_PRINCIPAL"> <bean class="...EqualPrincipalStrategy"></bean> </entry> <entry key="ONCE_FOR_ALL"> <bean class="...OnceForAllStrategy"></bean> </entry> </util:map>
-
在计算还款明细的方法中,使用还款策略的代码直接从关系映射中获取具体的策略即可:
RepaymentStrategy strategy = fallbackStrategy(); ... public RepaymentDetails calculateRepayment(BigDecimal totalAmount,String customerId) { Object type = getRepaymentTypeByCustomerId(customerId); RepaymentStrategy strategy = strategyMapping.get(type); // check constraint if(strategy == null) stargety = fallbackStrategy(); return strategy.performCalculation(); }
Tip
除了使用ioc容器注入映射关系,你还可以将对应应用程序的映射关系放到数据库或者其他外部配置文件,甚至Annotation中。 通过ioc容器一次注入多个策略实例可能需要占用多一些的系统资源,对于资源紧要的应用来说,可以考虑通过反射等方式按需构建具体策略实例,这个就留给读者来完成吧!
-
在系统中合理的使用Strategy模式可以使得系统向着“高内聚,低耦合”的理想方向迈进,在改善应用程序代码结构的同时,进一步的提高产品质量。 实际上,Strategy模式更是多态(Polymorphism)的完美体现,当你的OO内功修炼到“炉火纯青”之地的时候,你也就发现,所谓的Strategy模式的概念,在你的脑海中或许已经淡然了。
无论是spring的参考文档还是大多数介绍spring的书籍在提到使用Spring的JtaTransactionManager进行分布式事务管理的时候, 都强调需要使用从应用服务器的JNDI服务获取的dataSource,而不是本地配置的普通dataSource:
那么原因是什么那?
我们知道事务管理是要加诸于具体的事务资源之上的,所以,通常的PlatformTransactionManager的实现都会有相对应的事务资源的引用,比如,DataSourceTransactionManager会需要指定DataSource,HibernateTransactionManager会需要指定SessionFactory等等:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${jdbc.driverClassName}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>从常识上来讲,像DataSourceTransactionManager这样才是正常的情况, 而我们也看到JtaTransactionManager确实没有明确指定依赖于哪一个资源,却依然能够将需要加入分布式事务的资源纳入其管理范围,那么,它是怎么做到的那? 如果揭开这个谜团,是否就能搞清楚当使用JtaTransactionManager的时候,要求我们使用从应用服务器的JNDI服务查找到的DataSource的真正原因那?
在介绍JtaTransactionManager的时候,我们说到JtaTransactionManager只是对各种具体的JTA实现产品提供的分布式事务管理功能进行了封装, 最终的工作JtaTransactionManager都会委派给具体的JTA实现来做,所以,追根溯源,我们不得不追到JTA规范以及JTA实现中去。
略去JTA以及X/Open规范千言不提,我们还是长话短说,直接进入正题吧![14]
首先,具体的事务资源(RDBMS,MessageQueue,etc)要加入JTA管理的分布式事务,JTA规范要求其实现javax.transaction.xa.XAResource接口, 所以,希望加入JTA管理的分布式事务的资源管理器(RM)通常会提供相应的适配器(Adaptor)用于提供基于XAResource的分布式事务交互能力,比如关系数据库提供的支持XA的JDBC驱动程序就属于这样的适配器(Adaptor)。 这样,所有资源管理与事务交互的工作基本上就由RM的Adaptor来统一管理啦。
当想要参与JTA分布式事务的事务资源拥有了XAResource支持之后,JTA的javax.transaction.TransactionManager(我们称其为JTA TransactionManager,区别于spring的JtaTransactionManager)与RM之间就可以进行通信:
Adaptor通常都有应答能力,这样,当JTA TransactionManager使用两阶段提交协议管理分布式事务的过程中,可以同每一个RM之间进行交互。
不过,JTA TransactionManager在事务管理过程中要与哪一些RM打交道却不是由它自己说了算的,想要参与JTA分布式事务的RM何时何地甚至怎样加入JTA TransactionManager管理的分布式事务, 也不是每一个RM自己说了算,JTA TransactionManager与各个RM之间的联系要由ApplicationServer(一般意义上的TP Monitor)来进行协调! ApplicationServer为基于JTA的分布式事务提供运行时环境,并负责协调JTA TransactionManager与各RM之间的交互:
Procedure 1.4. ApplicationServer内部的运作
-
ApplicationServer一开始当然要先通过JNDI绑定它的JTA实现中的UserTransaction或者TransactionManager具体实现类, 这样,客户端应用程序就可以通过JNDI获取他们。现在客户端应用程序想要开始一个分布式事务,进而UserTransaction或者TransactionManager的相应方法被调用;
-
ApplicationServer内部会要求TransactionManager为当前事务分配一个唯一的标志(以Xid表示),然后开始事务,并将当前事务绑定到当前线程;
-
客户端跟ApplicationServer要求相应的数据资源进行数据访问,ApplicationServer会跟RM的Adaptor要一个事务资源对象,我们暂且称之为TransactionalResource,该资源对象包含两部分,一部分是JTA TransactionManager需要与之交互的XAResource,另一部分是要暴露给客户端应用程序使用的Connection资源, 取得TransactionalResource之后,ApplicationServer要做两件事情:
-
ApplicationServer从TransactionalResource中取得XAResource给TransactionManager,TransactionManager开始通过获得的这个XAResource与RM进行交互, 实际上,现在TransactionManager只是调用XAResource的start(xid)方法通知RM开始记录;
-
ApplicationServer然后再把跟XAResource属于同一个TransactionalResource的Connection传给客户端应用程序使用, 然后客户端应用程序就可以使用ApplicationServer传给的Connection进行数据访问操作了。
-
-
客户端应用程序数据访问操作完成,关闭之前ApplicationServer传给的Connection,ApplicationServer在感知到Connection被关闭之后,会通知TransactionManager, TransactionManager则调用与这个Connection属于同一个TransactionalResource的XAResource的end(xid)方法结束事务记录;
-
如果在当前分布式事务期间还有使用其他RM进行的数据操作,ApplicationServer以几乎同样的方式从RM的Adaptor那里获取TransactionalResource类似的对象, 然后协调TransactionManager重复余下的工作;
-
-
当客户端通过UserTransaction或者TransactionManager的相应方法要求结束事务的时候,ApplicationServer就会通知TransactionManager使用两阶段提交协议提交当前事务:
-
TransactionManager调用XAResource的prepare(xid)方法通知各个RM准备提交事务;
-
如果各个XAResource回答全部OK,TransactionManager调用XAResource的commit(xid)方法通知各个RM最终提交事务;
-
可见,各个RM确实参与到了JTA TransactionManager所管理的分布式事务中,只不过,参与的过程由ApplicationServer对客户端应用程序屏蔽了。 之所以要求客户端应用程序通过应用服务器的JNDI获取DataSource等资源,是因为只有使用ApplicationServer暴露的与XAResource绑定到同一TransactionalResource的Connection, 才可以保证客户端应用程序所做的所有数据访问操作能够加入ApplicationServer所协调的分布式事务中:
ApplicationServer为客户端应用程序暴露与当前分布式事务相关的Connection的方式,就是实现一个DataSource,然后把该DataSource绑定到JNDI,这样,客户端应用程序就可以通过从JNDI取得的DataSource中获取与事务相关的Connection了。 如果你使用本地定义的DataSource,因为它与当前分布式事务不发生任何关系,所以,也就根本不可能参与到分布式事务中去。
不过,如果你使用的JTA实现不是相应的ApplicationServer提供的,比如,可以独立使用的Atomikos或者JOTM等JTA实现,要求你从应用服务器的JNDI服务取得相应的DataSource也是不成立的, 这个时候,你直接使用各个JTA产品提供的DataSource封装类进行数据访问即可,与ApplicationServer屏蔽掉RM与TransactionManager之间的关系一样,这些产品也有与ApplicationServer完成同样工作的角色为我们关联具体的RM与当前产品的TransactionManager。 下面是在spring中使用Atomikos的典型配置方式:
<bean id="datasource1" class="com.atomikos.jdbc.SimpleDataSourceBean" init-method="init" destroy-method="close"> <property name="uniqueResourceName" value="XADBMS_ONE"/> <property name="xaDataSourceClassName" value="COM.FirstSQL.Dbcp.DbcpXADataSource"/> <property name="xaDataSourceProperties" value="user=username;portNumber=8000"/ <property name="exclusiveConnectionMode" value="true"/> </bean> <bean id="datasource2" class="com.atomikos.jdbc.SimpleDataSourceBean" init-method="init" destroy-method="close"> <property name="uniqueResourceName" value="XADBMS_TWO"/> <property name="xaDataSourceClassName" value="COM.FirstSQL.Dbcp.DbcpXADataSource"/> <property name="xaDataSourceProperties" value="user=username;portNumber=8000"/ <property name="exclusiveConnectionMode" value="true"/> </bean> <bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close"> <property name="forceShutdown" value="true"/> </bean> <bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp"> <property name="transactionTimeout" value="200"/> </bean> <bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager" ref="atomikosTransactionManager"/> <property name="userTransaction" ref="atomikosUserTransaction"/> </bean> <bean id="dao" class= "..."> <property name="dataSource" ref="datasource"/> </bean>我们需要告知Spring的JtaTransactionManager使用Atomikos的UserTransaction和TransactionManager实现,而至于Atomikos的UserTransaction和TransactionManager到底如何与RM(database, messsage queue,etc)进行交互,那就是Atomikos的事情了。
Note
不要被Atomikos的SimpleDataSourceBean的名字给迷惑了,另外,只要你的数据库提供了有XA支持的数据库驱动,你就可以通过SimpleDataSourceBean来配置需要的支持XA的DataSource,但各个参数的设置需要参考相应驱动程序的文档, 比如xaDataSourceClassName以及xaDataSourceProperties属性。
多个Atomikos的SimpleDataSourceBean存在的情况下,他们对应的uniqueResourceName必须是不同的!!!
最后的内容是专门为spring的JtaTransactionManager的,2.5版本的spring发布后,在XSD的配置中,可使用tx命名空间下专门为JtaTransactionManager提供的配置元素来简化其配置:
<tx:jta-transaction-manager/>当然,如果需要,也可以通过内嵌的<property>指定使用自定义的UserTransaction或者TransactionManager。
[1] 这里的“正确”有所泛指,比如,系统状态需要始终完整,各项数据的关系始终保持一致等等,当系统各项状态以合理的形式存在的时候,那我们说系统处于一个正确的状态,这样的表达方式我想应该可以接受吧?!
[2] 也可称为“隔离度”,大家只需要知道我们指的都是Isolation这个概念就可以。
[3] 又称“幻影读”。
[4] 更确切的说是JTA(Java Transaction API)以及JTS规范(Java Transaction Service Specification)
[5] 事务界定规定了一个事务什么时候开始以及什么时候结束
[6] 事务的上下文传播指的是,在整个事务处理工程中,如何在各事务操作之间传播事务信息的行为。
[7] 这里的connection不是特指java.sql.Connection类型,而是泛指应用程序与事务资源之间的通信通道,对于JDBC来说,恰好对应java.sql.Connection,如果是Hibernate,那就应该是Session,诸如此类
[8] 实际上,你可以通过配置来决定让Hibernate使用基于Jdbc Connection的局部事务管理,还是让Hibernate使用分布式的事务管理。
[9] 当然,如果只是检测单条数据插入的主键冲突,然后改为更新数据的话,更多时候,我们会直接在一个数据访问方法中搞定。
[10] 因为UML的Sequence图对于条件判断等逻辑无法以恰当的方式表示,而Activity图整个表示下来过大,不便显示,所以,最后决定以文字描述的方式进行。
[11] 实际上,只要有一个支持XSD的XML编辑器,对于我们来说根本就不用记住每一个属性的具体特征,通常这种编辑器都会有提示功能, 只要提示的每个属性你知道它具体代表什么意思,剩下的就是个选择的问题。
[12] 当然,即使你不使用Java5,也不意味着你不能使用源码中的元数据驱动的声明式事务,比如你可以使用commons attributes(http://commons.apache.org/attributes/),但是, Java5的Annotation可以为我们带来更多的东西,比如编译器检查,IDE良好的重构支持等等。 如果当前应用无法使用Java5,而又要使用源代码中元数据驱动的声明式事务,你可以参照spring参考文档中提供的使用commons attributes的相关信息。
[13] 在《Java多线程设计模式》一书中,对“Thread-Specific Storage Pattern”有详细的介绍和讲解。
[14] 实际上JTA规范本身不到100页,想要更深入的了解JTA的内容的话,看一下也无妨啊!