Java 理论和实践: 理解 JTS ― 幕后魔术
在这个系列的 第 1 部分,我们讨论了事务并研究了它们的基本属性 ― 原子性(atomicity)、一致性(consistency)、孤立性(isolation)和持久性(durability)。事务是企业应用程序的基本构件;没有它们,几乎不可能构建有容错能力的企业应用程序。幸运的是,Java 事务服务(Java Transaction Service,JTS)和 J2EE 容器自动为您做了大量的事务管理工作,这样您就不必将事务意识直接集成到组件代码中。结果简直是一种魔术 ― 通过遵守几条简单的规则,J2EE 应用程序就可以自动获得事务性语义,只需极少或根本不需要额外的组件代码。本文旨在通过展示事务管理如何发生,以及发生在何处来揭开这个魔术的神秘面纱。
什么是 JTS?
JTS 是一个 组件事务监视器(component transaction monitor)。这是什么意思?在第 1 部分,我们介绍了 事务处理监视器(TPM)这个概念,TPM 是一个程序,它代表应用程序协调分布式事务的执行。TPM 与数据库出现的时间长短差不多;在 60 年代后期,IBM 首先开发了 CICS,至今人们仍在使用。经典的(或者说 程序化)TPM 管理被程序化定义为针对事务性资源(比如数据库)的操作序列的事务。随着分布式对象协议,如 CORBA、DCOM 和 RMI 的出现,人们希望看到事务更面向对象的前景。将事务性语义告知面向对象的组件要求对 TPM 模型进行扩展 ― 在这个模型中事务是按照事务性对象的调用方法定义的。JTS 只是一个组件事务监视器(有时也称为 对象事务监视器(object transaction monitor)),或称为 CTM。
JTS 和 J2EE 的事务支持设计受 CORBA 对象事务服务(CORBA Object Transaction Service,OTS)的影响很大。实际上,JTS 实现 OTS 并充当 Java 事务 API(Java Transaction API)― 一种用来定义事务边界的低级 API ― 和 OTS 之间的接口。使用 OTS 代替创建一个新对象事务协议遵循了现有标准,并使 J2EE 和 CORBA 能够互相兼容。
乍一看,从程序化事务监视器到 CTM 的转变好像只是术语名称改变了一下。然而,差别不止这一点。当 CTM 中的事务提交或回滚时,与事务相关的对象所做的全部更改都一起被提交或取消。但 CTM 怎么知道对象在事务期间做了什么事?象 EJB 组件之类的事务性组件并没有 commit()
或 rollback()
方法,它们也没向事务监视器注册自己做了什么事。那么 J2EE 组件执行的操作如何变成事务的一部分呢?
![]() ![]() |
![]()
|
透明的资源征用
当应用程序状态被组件操纵时,它仍然存储在事务性资源管理器(例如,数据库和消息队列服务器)中,这些事务性资源管理器可以注册为分布式事务中的资源管理器。在第 1 部分中,我们讨论了如何在单个事务中征用多个资源管理器,事务管理器如何协调这些资源管理器。资源管理器知道如何把应用程序状态中的变化与特定的事务关联起来。
但这只是把问题的焦点从组件转移到了资源管理器 ― 容器如何断定什么资源与该事务有关,可以供它征用?请考虑下面的代码,在典型的 EJB 会话 bean 中您可能会发现这样的代码:
清单 1. bean 管理的事务的透明资源征用
|
注意,这个示例中没有征用当前事务中 JDBC 连接的代码 ― 容器会为我们完成这个任务。我们来看一下它是如何发生的。
资源管理器的三种类型
当一个 EJB 组件想访问数据库、消息队列服务器或者其它一些事务性资源时,它需要到资源管理器的连接(通常是使用 JNDI)。而且,J2EE 规范只认可三种类型的事务性资源 ― JDBC 数据库、JMS 消息队列服务器和“其它通过 JCA 访问的事务性服务”。后面一种服务(比如 ERP 系统)必须通过 JCA(J2EE Connector Architecture,J2EE 连接器体系结构)访问。对于这些类型资源中的每一种,容器或提供者都会帮我们把资源征调到事务中。
在清单 1 中, con1
和 con2
好象是普通的 JDBC 连接,比如那些从 DriverManager.getConnection()
返回的连接。我们从一个 JDBC DataSource
得到这些连接,JDBC DataSource
可以通过查找 JNDI 中的数据源名称得到。EJB 组件中被用来查找数据源( java:comp/env/OrdersDB
)的名称是特定于组件的;组件的部署描述符的 resource-ref
部分将其映射为容器管理的一些应用程序级 DataSource
的 JNDI 名称。
隐藏的 JDBC 驱动器
每个 J2EE 容器都可以创建有事务意识的池态 DataSource
对象,但 J2EE 规范并不向您展示如何创建,因为这不在 J2EE 规范内。浏览 J2EE 文档时,您找不到任何关于如何创建 JDBC 数据源的内容。相反,您不得不为您的容器查阅该文档。创建一个数据源可能需要向属性或配置文件添加一个数据源定义,或者也可以通过 GUI 管理工具完成,这取决于您的容器。
每个容器(或连接池管理器,如 PoolMan)都提供它自己的创建 DataSource
机制,JTA 魔术就隐藏在这个机制中。连接池管理器从指定的 JDBC 驱动器得到一个 Connection
,但在将它返回到应用程序之前,将它与一个也实现 Connection
的虚包包在一起,将自己置入应用程序和底层连接之间。当创建连接或者执行 JDBC 操作时,包装器询问事务管理器当前线程是不是正在事务的上下文中执行,如果事务中有 Connection
的话,就自动征用它。
其它类型的事务性资源,JMS 消息队列和 JCA 连接器,依靠相似的机制将资源征用隐藏起来,使用户看不到。如果要使 JMS 队列在部署时对 J2EE 应用程序可用,您就要再次使用特定于提供者的机制来创建受管 JMS 对象(队列连接工厂和目标),然后在 JNDI 名称空间内发布这些对象。提供者创建的受管对象包含与 JDBC 包装器(由容器提供的连接池管理器添加)相似的自动征用代码。
![]() ![]() |
![]()
|
透明的事务控制
两种类型的 J2EE 事务 ― 容器管理的和 bean 管理的 ― 在如何启动和结束事务上是不同的。事务启动和结束的地方被称为 事务划分(transaction demarcation)。清单 1 中的示例代码演示了 bean 管理的事务(有时也称为 编程(programmatic)事务)。Bean 管理的事务是由组件使用 UserTransaction
类显式启动和结束的。通过 ejbContext
使 UserTransaction
对 EJB 组件可用,通过 JNDI 使其对其它 J2EE 组件可用。
容器根据组件的部署描述符中的事务属性代表应用程序透明地启动和结束容器管理的事务(或称为 宣告式事务(declarative transaction))。通过将 transaction-type
属性设置为 Container
或 Bean
您可以指出 EJB 组件是使用 bean 管理的事务性支持还是容器管理的事务性支持。
使用容器管理的事务,您可以在 EJB 类或方法级别上指定事务性属性;您可以为 EJB 类指定缺省的事务性属性,如果不同的方法会有不同的事务性语义,您还可以为每个方法指定属性。这些事务性属性在装配描述符(assembly descriptor)的 container-transaction
部分被指定。清单 2 显示了一个装配描述符示例。 trans-attribute
的受支持的值有:
-
Supports
-
Required
-
RequiresNew
-
Mandatory
-
NotSupported
-
Never
trans-attribute
决定方法是否支持在事务内部执行、当在事务内部调用方法时容器会执行什么操作以及在事务外部调用方法时容器会执行什么操作。最常用的容器管理的事务属性是 Required
。如果设置了 Required
,过程中的事务将在该事务中征用您的 bean,但如果没有正在运行的事务,容器将为您启动一个。在这个系列的第 3 部分,当您可能想使用每个事务属性时,我们将研究各个事务属性之间的区别。
清单 2. EJB 装配描述符样本
|
功能强大,但很危险
与清单 1 中的示例不同,由于有宣告式事务划分,这段组件代码中没有事务管理代码。这不仅使结果组件代码更加易读(因为它不与事务管理代码混在一起),而且它还有另一个更重要的优点 ― 不必修改,甚至不必访问组件的源代码,就可以在应用程序装配时改变组件的事务性语义。
尽管能够指定与代码分开的事务划分是一种非常强大的功能,但在装配时做出不好的决定会使应用程序变得不稳定,或者严重影响它的性能。对容器管理的事务进行正确分界的责任由组件开发者和应用程序装配人员共同担当。组件开发者需要提供足够的文档说明组件是做什么的,这样应用程序部署者就能够明智地决定如何构建应用程序的事务。应用程序装配人员需要理解应用程序中的组件是怎样相互作用的,这样就可以用一种既强制应用程序保持一致又不削弱性能的方法对事务进行分界。在这个系列的第 3 部分中我们将讨论这些问题。
![]() ![]() |
![]()
|
透明的事务传播
在任何类型的事务中,资源征用都是透明的;容器自动将事务过程中使用的任意事务性资源征调到当前事务中。这个过程不仅扩展到事务性方法使用的资源(比如在清单 1 中获得的数据库连接),还扩展到它调用的方法(甚至远程方法)使用的资源。我们来看一下这是如何发生的。
容器用线程与事务相关联
我们假设对象 A
的 methodA()
启动一个事务,然后调用对象 B
的 methodB()
(对象 B 将得到一个 JDBC 连接并更新数据库)。 B
获得的连接将被自动征调到 A
创建的事务中。容器怎么知道要做这件事?
当事务启动时,事务上下文与执行线程关联在一起。当 A
创建事务时, A
在其中执行的线程与该事务关联在一起。由于本地方法调用与主调程序(caller)在同一个线程内执行,所以 A
调用的每个方法也都在该事务的上下文中。
橱中骸骨
如果对象 B
其实是在另一个线程,甚至另一个 JVM 中执行的 EJB 组件的存根,情况会怎样?令人吃惊的是,远程对象 B
访问的资源仍将在当前事务中被征用。EJB 对象存根(在主调程序的上下文中执行的那部分)、EJB 协议(IIOP 上的 RMI)和远端的骨架对象协力要使其透明地发生。存根确定调用者是不是正在执行一个事务。如果是,事务标识,或者说 Xid,被作为 IIOP 调用的一部分与方法参数一起传播到远程对象。(IIOP 是 CORBA 远程-调用协议,它为传播执行上下文(比如事务上下文和安全性上下文)的各种元素而备;关于 RMI over IIOP 的更多信息,请参阅 参考资料。)如果调用是事务的一部分,那么远程系统上的骨架对象自动设置远程线程的事务上下文,这样,当调用实际的远程方法时,它已经是事务的一部分了。(存根和骨架对象还负责开始和提交容器管理的事务。)
事务可以由任何 J2EE 组件来启动 ― 一个 EJB 组件、一个 servlet 或者一个 JSP 页面(如果容器支持的话,还可以是一个应用程序客户机)。这意味着,应用程序可以在请求到达时在 servlet 或者 JSP 页面中启动事务、在 servlet 或者 JSP 页面中执行一些处理、作为页面逻辑的一部分访问多个服务器上的实体 bean 和会话 bean 并使所有这些工作透明地成为一个事务的一部分。图 1 演示了事务上下文怎样遵守从 servlet 到 EJB,再到 EJB 的执行路径。
图 1.单个事务中的多个组件
![]() ![]() |
![]()
|
最优化
让容器来管理事务允许容器为我们做出某些最优化决定。在图 1 中,我们看到一个 servlet 和多个 EJB 组件在单个事务的上下文中访问一个数据库。每个组件都获得到数据库的 Connection
;很可能每个组件都在访问同一个数据库。即使多个连接是从不同的组件到同一个资源,JTS 也可以检测出多个资源是否和事务有关,并最优化该事务的执行。您可以从第 1 部分回忆起来,单个事务要包含多个资源管理器需要使用两阶段提交协议,这比单个资源管理器使用的单阶段提交代价要高。JTS 能够确定事务中是不是只征用了一个资源管理器。如果它检测出所有与事务相关的资源都一样,它可以跳过两阶段提交并让资源管理器自己来处理事务。
![]() ![]() |
![]()
|
结束语
这个虑及透明事务控制、资源征用和透明传播的魔术不是 JTS 的一部分,而是 J2EE 容器如何在幕后代表 J2EE 应用程序使用 JTA 和 JTS 服务的一部分。在幕后有许多实体合力使这个魔术透明地发生;EJB 存根和骨架、容器厂商提供的 JDBC 驱动器包装器、数据库厂商提供的 JDBC 驱动器、JMS 提供器和 JCA 连接器。所有这些实体都与事务管理器进行交互,于是应用程序代码就不必与之交互了。
在第 3 部分,我们将看一下关于管理 J2EE 上下文中事务的一些实际问题 ― 事务划分和孤立 ― 以及它们对应用程序一致性、稳定性和性能的影响。