漫谈WebSphere应用服务器之事务
1.事务及事务管理器的基本概念和历史
说起事务,大部分人都马上能说出事务的四个属性:ACID(原子性、一致性、隔离性和持久性)。事务这个概念最先来自数据库领域。我们可以看一下wikipedia上如何定义数据库事务的:
A database transaction comprises a unit of work performed within a database management system (or similar system) against a database, and treated in a coherent and reliable way independent of other transactions.
从该定义其实已经可以体现出事务的四个特性了。而提供事务能力的事务处理系统早在60年代就出现了,典型代表有到现在还在企业中稳定运行的IBM的CICS、BEA的Texedo等。而后事务处理最为一个新兴的学科开始发展起来,并在伟大的图灵奖得主、一个传奇式的人物 - Jim Gray下得到发扬光大。他的事务处理-概念和技术这本著作也成为这个领域的不朽经典。
除此之外,影响深远的当属X/Open提出的DTP模型了。
在90年代,随着分布式计算的兴起,企业的计算模式也发生了质的变化。一个业务的生命周期往往需要跨越多个系统和多种资源(如数据库,消息系统等)。因此如何保证这种分布式计算模式下面的事务性成了当时研究的热点。其中X/Open的DTP模型规范了事务处理系统中各组件的职能及交互的契约,它也成为目前这个领域的事实标准。
如上图所示,该模型提供了一个构建分布式事务处理系统的方法。其核心包括三个组件和两组接口。
三个组件:
1. 应用程序(AP):应用程序是事务的发起者;它决定了一个事务的生命周期(比如什么时候开始一个新的事务、什么时候该提交或者回滚事务)。
2. 事务管理器(TM):如其名字所表述的,它提供了对事务的管理和恢复。
3. 资源管理器(RM)(Resource Manager),负责对资源的访问。
两组接口:
1. TX接口。该接口定义了应用程序和事务管理器之间交互的方法。比如应用程序可以调用事务管理器的tx_open/tx_close方法用来建立/断开事务管理器。然后使用tx_begin/tx_rollback/tx_commit来通知事务管理器开始/ 回滚/提交事务。
当事务管理器接收到开始/回滚/提交事务的请求后,事务管理器必须告知加入到当前事务的资源管理器来执行对应的操作,这是通过另一组XA接口来实现的。
2. XA接口定义了事务管理器和资源管理器之间交互的方法。重要的的方法有:
xa_open/xa_close。事务管理器通过调用这两个方法来通知资源管理器进行初始化和结束。
xa_start/xa_end。事务管理器通过这两个方法来通知资源管理器开始和结束事务。这两个方法也就确定了一个事务的边界。
xa_prepare/xa_commit/xa_rollback。事务管理器通过两阶段提交协议来通知资源管理器进行事务的提交或者回滚。
在这个模型下,一个典型的事务处理可以表示如下:
-------------------------------------------------------------------------------
//1. 应用程序开始事务。
tx_begin.
//应用程序进行业务操作,在需要保证事务的部分开始事务。
tx_begin
//应用程序调用资源管理器进行业务操作
//事务管理器通知资源管理器进行初始化
xa_open
//事务管理器通知资源管理器事务开始
xa_start
//资源管理器记录事务性操作。。。
//最后应用提交事务。
tx_commit
//事务管理器通知资源管理器开始两阶段提交,最后结束事务
xa_prepare
xa_commit
xa_close
tx_close
--------------------------------------------------------------------------------------------------------
CORBA的兴起也给X/Open的DTP模型找到了一个绝好的实践场地。
在OMG的OTS规范中给出了如下的事务架构:
可以看到该模型其实和DTP模型是完全一致的,只不过是用CORBA来重新描述了而已。而且在OTS中也明确说明了是可以和X/Open的DTP互操作的。
2. J2EE事务
网络就是计算机这个口号虽然没有造就一个互联网软件帝国,却让Java成为了企业级应用的首选。随着EJB编程模型在J2EE中的引入,JTA和JTS也随之发布。如下图所示,J2EE世界中的事务模型完全是延续DTP的。它也定义了应用、资源管理器和事务管理器。
JTA协议简单来说就是用Java来定义了X/Open的DTP模型中的两组接口(TX和XA)。具体而言,JTA把应用程序分为了两类,一类是真正的客户应用程序,另一类是应用服务器。针对这两类事务客户端它分别定义了javax.transaction.UserTransaction和javax.transaction.TransactionManager接口,这两组接口直接对应了TX接口。通过UserTransaction接口,客户应用程序就可以显示的控制事务的开始和结束。但在J2EE的世界中更推崇让容器来管理事务,这样应用无需硬编码,只要通过声明式的方式就可以让自己的EJB运行在特定的事务上下文中。在J2EE规范中定义了好多种类型的事务属性,根据这些属性容器在调用EJB之前和之后就会执行相应的事务控制操作。比如如果事务属性设置为RequireNew:
-----------------------------------------------------------------------------------------------------------
//在开始调用Bean方法之前,容器代码先拿到一个事务管理器对象。如何拿到
//这个对象在规范中没有说明,因此不同的容器提供商有不同的方法。
TransactionManager txManager = TransactionManagerFactory.getTransactionManager();
//由于是需要新的事务,因此需要把当前已有的事务挂起(如果有事务的话)。
Transaction current = txManager.suspend();
//接下来就可以发起一个新的事务了。
txManager.begin();
//然后容器把请求发给EJB实例,业务逻辑开始执行。在执行过程中任何
//事务相关的操作都会被自动的加入到当前事务中。最后执行完成推出。
//容器重新接管控制权,如果没有异常,则提交当前事务。
txManager.commit();
//最后恢复被挂起的事务。
txManager.resume(current);
----------------------------------------------------------------------------------------------------------
而XA接口则是由javax.transaction.xa.XAResource来提供的。该接口定义了资源管理器和事务管理器之间的契约。在上一章节中我们已经 简要的介绍过XA接口的方法。这儿需要注意的是在J2EE的环境下,open和close并不会被调用到,因此资源的初始化和关闭时由资源管理器自己完成的。
而JTS相对而言就比较低层得多,它规范了一个J2EE下的事务管理器在上层需要提供对JTA的支持,而下层则需要实现OTS的Java映射。
说到这儿,J2EE事务基本上可以告一段落。但其实还有一个问题是在上述的代码中,业务逻辑进行事务性操作的时候,比如往数据库里面加入一条记录,或者往JMS Queue中写入一条消息。这些资源是如何自动的加入到当前的事务中呢?这部分内容是不可能在J2EE的规范中找到的。目前绝大部分容器的实现都是在XA数据源上做的文章。比如我们在WebSphere应用服务器上定义了一个jdbc/Order的数据源,然后在一个Session Bean中用下面的示例代码来添加一个订单:
---------------------------------------------------------------------------------------------
InitialContext ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup("jdbc/Order");
Connection con = ds.getConnection();
Statement st = con.createStatement();
String sql ="insert into order .....";
st.execute(sql);
//最后cleanup
st.close();
con.close();
---------------------------------------------------------------------------------------------
在上述例子中,如果这个Session Bean的事务标记为Required、RequiredNew、Support,则这儿的数据库操作会自动加入到当前的事务中。如何自动加入的秘密就在于这儿返回的数据源。一个简单的实现是当ds.getConnection方法被调用时,数据源的实现里面首先调用底层的资源管理器拿到一个Connection,然后调用Connection的getXAResource拿到XAResource,接着拿到当前的transactionManager应用,然后通过transactionManager.getTranaction().enlistResource把该资源加到当前的事务中。最后返回一个Connection的封装。通过这种方式资源就被自动的加入到当前事务中,然后在业务逻辑执行完成后,当容器重新拿到控制权后,容器就会根据配置来调用事务管理器来完成事务的提交或者回滚。
3. WebSphere事务
WAS作为一个J2EE服务器,自然也就实现了JTS跟JTA。这儿再温习一下整个事务的一个完整的运作过程.。这儿我们使用如下的一个场景:应用在完成一个业务时需要同时访问数据库和消息系统,而且需要保证对这两种资源的更新的事务性,这儿应用使用了userTransaction来自己控制事务。示例代码如下:
-----------------------------------------------------------------------------------------
InitialContext ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup("jdbc/order");
ConnectionFactory connectionFactory =
(ConnectionFactory)ic.lookup("jms/orderCF");
UserTransaction ut = (UserTransaction) ic.lookup("java:comp/UserTransaction");
try{
ut.begin();
javax.jms.Connection jmsConnection = connectionFactory.createConnection();
Session session = jmsConnection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Queue sendQueue = (Queue) ic.lookup("jms/orderQueue");
MessageProducer producer = session.createProducer(sendQueue);
jmsConnection.start();
TextMessage requestMessage = session.createTextMessage("data");
producer.send(requestMessage);
session.close();
jmsConnection.close();
java.sql.Connection jdbcConnection = ds.getConnection();
java.sql.Statement st = jdbcConnection.createStatement();
String sql = "insert into ...";
st.execute(sql);
st.close();
jdbcConnection.close();
ut.commit();
}catch(Exception e) {
try {
ut.rollback();
} catch (IllegalStateException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (SecurityException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (SystemException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
-----------------------------------------------------------------------------------------
我们可以用下面的两幅图来分析整个事务的处理过程。
首先看一下这两个资源是如何注册到事务管理器中的。
1. 应用通过userTransaction.begin()开始事务。JTS把创建事务并把它关联到当前的线程上。
2. 用户执行JDBC操作:拿到数据源、连接、创建Statement,最后调用statement.execute来执行sql语句来在数据库中创建一条新条目。
3. 因为这儿使用的statement是经过容器包装的一个Wrapper,在其实现中首先向真正的XAConnection拿到XAResource。
4 然后调用transactionManager.getTransaction().enlistResource()方法来把该XAResource加入到当前事务中。
5 事务管理器调用该XAResource的start方法来通知资源管理器开始记录事务性操作。
6 最后调用真正的Statement的execute()方法来执行数据库操作。
7到11步和上述类似。通过类似的操作把JMS资源加到当前的事务中。
当业务步骤完成后,调用close来关闭连接,然后调用commit来提交事务。这些操作都会触发对应的XA的动作,如下所示:
12. 当connection的close方法被调用到后
13. 容器提供的Connection wrapper会先调用transactionManager.getTransaction().delistResource()方法来停止记录。
14. JTS调用XAResouce的end方法来结束事务。
15. 真正的connection的close方法被调用,资源管理器释放资源。
16 到19步完成JMS的close操作。
20. 然后userTransaction的commit被调用,此时开始两阶段提交。
21 首先JTS向JDBC的XAResource发起prepare请求,如果返回Prepared,则
22 JTS向JMS的XAResource发起prepare请求,如果还是返回Prepared,则
23. 调用JDBC的XAResource的commit方法落实所有修改
24. 调用JMS的XAResource的commit方法落实所有修改
除了经典的XA的支持,还值得提及的是WAS的一些特别的扩展。
首先要介绍的是本地事务
这儿先回顾一下为什么在EJB这种分布式计算模型下面为什么还有本地事务的需求。让我们先看一下在J2EE规范中明确提到了未指定的事务上下文这个概念。
--------------------------------------------------------------------------------------------------------------
The term “an unspecified transaction context” is used in the EJB specification to refer to the cases in which the EJB architecture does not fully define the transaction semantics of an enterprise bean method execution:
The execution of a method of an enterprise bean with container-managed transaction demarcation for which the value of the transaction attribute is NotSupported, Never, or Supports (onyl applies to the case when the client calls without a transaction context)
The execution of the ejbCreate<METHOD>, ejbRemove, ejbPassivate, and ejbActivate methods of a session bean with container-managed transaction demarcation.
The execution of the ejbCreate<METHOD> and ejbRemove methods of a message-driven
bean with container-managed transaction demarcation.
-----------------------------------------------------------------------------------------------------------
在上述提到的场景中,由于缺乏事务的语义,因此没有办法规定在这些场景下的事务属性。因此规范索性把这部分定义留给了具体的容器提供商。
在这些情况下,WAS总是会启动一个本地事务(LTC)来对事务进行控制。LTC针对资源管理器的本地事务提供了三个层次的支持。首先是事务的边界,缺省是Bean方法;然后是对于没有完成的本地事务的处理策略,缺省是回滚。最后是对资源的回收,保证在本地事务推出时候里面所使用到得资源得以释放。
LTC本身没有编程接口,只能通过WAS对EJB部署描述符的扩展来配置。
分布式事务的一些扩展
分布式事务上WAS也提供了两种扩展。
第一种是Last participant。这种扩展用在一个事务中如果有多个资源管理器支持XA(2PC,两阶段),但恰好有一个只能支持RMLT (resource manager local transactions)(1PC,一阶段,没有Prepare,只有commit)的场景下。在这种情况下, 可以通过配置last participant来支持这种场景。具体实现上是在开始两阶段的prepare时,事务管理器首先询问其他的XA资源是否准备好,如果回答都是OK,接下来就调用1PC资源的提交,如果提交成功,则调用其他的2PC的commit,否则rollback。
上面的场景要求只能有一个1PC的资源,如果把这个限制放松,则没有办法用这个方式来实现对不同类型的资源的统一协调。ActivitySession就是为了满足这种涉及了多个1PC的资源的应用场景。可以说它扩展了传统的XA的概念,把它延伸了道了1PC资源上。和分布式事务类似,它的事务边界的划分也可以通过声明式或者是编程来实现。但不同点在于它没有Prepare阶段,也没有Recovery。
其他编程接口
在J2EE规范中,UserTransaction只能在Bean或者Servlet里面使用。那么在普通的POJO里面,如果访问事务,如果发起新的事务呢?WebSphere提供了下面的一些办法:
ExtendedJTATransaction.
通过该接口可以知道当前有没有全局事务,如果有则可以注册一个SynchronizationCallback对象到当前的事务或者容器中所有的事务中。
可以在EJB、Servlet、JSP或者普通的POJO中通过下面的代码访问到该对象:
ExtendedJTATransactionexJTA = (ExtendedJTATransaction)ctx.lookup("UOWManager
java:comp/websphere/ExtendedJTATransaction");
//判断是否有活动的事务
if (exJTA.getGlobalId() != null) {
//给当前的事务注册一个callback
SynchronizationCallback sync = new SynchronizationCallback();
exJTA.registerSynchronizationCallbackForCurrentTran
(sync);}
ExtensionHelper.
如果需要在POJO中发起新的事务,则可以用ExtensionHelper。示例代码如下:
InitialContext ctx = new InitialContext();
ExtensionHelper eh = (ExtensionHelper) ctx.lookcup(ExtensionHelper.JNDI_NAME);
TransactionControl tc = eh.getTransactionControl();
TxHandle txh = tc.preinvoke();
try {
// Do some work ...
// Commit the transaction
tc.postinvoke(txh);
} catch (Throwable fatalException) {
// Rollback the transaction
tc.handleException(txh);
}
在WAS6.1以后提供了一个更简洁的接口来实现对UOW的管理。上面在POJO中启动一个事务的代码可以重写如下:
try
{
UOWManager uowManager = UOWManagerFactory.getUOWManager();
uowManager.runUnderUOW(UOWSynchronizationRegistry.UOW_TYPE_GLOBAL_TRANSACTION, false, new UOWAction()
{
public void run() throws Exception
{
// Perform. transactional work here.
}
});
}
catch (UOWActionException uowae)
{
// Transactional work resulted in a checked exception being thrown.
// The UOW was not affected.
}
catch (RuntimeException re)
{
// Transactional work resulted in an unchecked exception being thrown.
// The UOW was rolled back
}
catch (UOWException uowe)
{
// The completion of the UOW failed unexpectedly.
}
4. Web服务事务
现在SOA火的一塌糊涂。而作为SOA基石之一的Web服务业早就深入人心。那么在处处都是服务的时候,如何保证服务之间交互的事务性也是必须要解决的问题。和CORBA的OTS、J2EEE的JTA/JTS一样,Web服务也搞了一套自己的事务解决方案。目前,Web服务的事务是由WS-Coordination和WS-Transaction两个规范构成。WS-Coordination中提出了一套WS-Coordination Framework来定义了在Web服务的场景下如何确定事务边界、如何开始和提交事务(这其实也就是定义了JTA,但JTA中的XA部分则是放在WS-Trsansaction中定义的)。同时考虑到Web服务的特性,在很多场景下传统的分布式事务(XA)是不能满足长时间运行的事务的需求的,因此Web服务事务在WS-Transaction规范中提供了两类提交协议:WS-AT和WS-BA。前者是经典的分布式事务,后者则是为了长事务而提出的,主要是利用了补偿的概念。
这两个协议具体是如何运作的细节可以参考Web Service Transaction链接。该文章通过一个例子非常详细的描述了一个Web服务事务的运作过程,详细列出了其中交互的Web服务事务的SOAP消息,非常值得学习。