JTA

概念

Java Transaction API (JTA) 指定事务管理器和分布式事务系统中涉及的各方之间的标准Java接口:资源管理器、应用服务器和事务应用程序。

JTA规范是Sun Microsystems与交易处理和数据库系统领域的领先行业合作伙伴合作开发的。

参考 JSR-907

作用

JTA提供:

  • 划分事务边界

  • X/Open XA API允许资源参与到事务中。

XA

在X/Open XA的体系结构中,事务管理器或事务处理监控器 (TP monitor)协调 资源跨越多个资源,如数据库和消息队列的事务。每

一个资源都有自己的管理器。资源管理器通常拥有自己的用于操纵资源的API,例如关系型数据库使用的JDBC。

此外,资源适配器允许事务管理器协调该资源管理器和其他资源管理器之间的分布式事务。

最后,与事务管理器通讯的应用程序开始,提交,或回滚事务。应用程序同样需要使用资源自己的API与不同的资源通讯,修改资源。

组成

Java事务API由三个部分组成:

  1. UserTransaction - 高层的应用事务划分接口,供客户程序使用

  2. TransactionManager - 高层的事务管理器接口,供应用服务器使用

  3. XAResource,X/Open XA协议的标准Java映射,供事务性资源管理器使用。

  • 标准的分布式事务

一个分布式事务(Distributed Transaction)包括一个事务管理器(transaction manager)和一个或多个资源管理器(resource manager)。

一个资源管理器(resource manager)是任意类型的持久化数据存储。事务管理器(transaction manager)承担着所有事务参与单元者的相互通讯的责任。

JTA的优缺点

JTA的优点很明显,就是提供了分布式事务的解决方案,严格的ACID。

但是,标准的JTA方式的事务管理在日常开发中并不常用,因为他有很多缺点:

  • 实现复杂

  • 通常情况下,JTA UserTransaction需要从JNDI获取。这意味着,如果我们使用JTA,就需要同时使用JTA和JNDI。

  • JTA本身就是个笨重的API

  • 通常JTA只能在应用服务器环境下使用,因此使用JTA会限制代码的复用性。

UserTransaction 接口

javax.transaction.UserTransaction 接口给应用程序提供了编程控制事务边界的能力。该接口可以供Java客户端程序或EJB使用。

UserTransaction的begin方法开始一个全局事务,并将该事务与调用线程关联。事务到线程的管理是由事务管理器完成的,对应用程序透明的。

对嵌套事务的支持不是必须的。如果调用线程的上下文已经与事务关联,并且事务管理器的实现并不支持嵌套的事务,UserTransaction的begin方法调用时将抛出NotSupportedException。

底层的事务管理器的实现负责提供不同应用程序间事务上下文的传播,事务管理器位于客户端和服务器计算机上。 传播的事务上下文的格式由客户端和服务器计算机协商确定。例如,如果事务管理器是JTS规范的实现,将使用CORBA OTS 1.1规范中描述的事务上下文传播格式。

事务上下文的传播对于应用程序来说是透明的。

实例

public void transferAccount() { 
    UserTransaction userTx = null; 
    Connection connA = null; 
    Statement stmtA = null; 
    Connection connB = null; 
    Statement stmtB = null; 

    try{ 
          // 获得 Transaction 管理对象
        userTx = (UserTransaction)getContext().lookup("\
              java:comp/UserTransaction"); 
        // 从数据库 A 中取得数据库连接
        connA = getDataSourceA().getConnection(); 
        
        // 从数据库 B 中取得数据库连接
        connB = getDataSourceB().getConnection(); 
  
        // 启动事务
        userTx.begin();
        
        // 将 A 账户中的金额减少 500 
        stmtA = connA.createStatement(); 
        stmtA.execute("
       update t_account set amount = amount - 500 where account_id = 'A'");
        
        // 将 B 账户中的金额增加 500 
        stmtB = connB.createStatement(); 
        stmtB.execute("\
        update t_account set amount = amount + 500 where account_id = 'B'");
        
        // 提交事务
        userTx.commit();
        // 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)
    } catch(SQLException sqle){ 
        try{ 
            // 发生异常,回滚在本事务中的操纵
            userTx.rollback();
            // 事务回滚:转账的两步操作完全撤销 
            //( 数据库 A 和数据库 B 中的数据更新被同时撤销)
            stmt.close(); 
            conn.close(); 
            ... 
        }catch(Exception ignore){ 
            
        } 
        sqle.printStackTrace(); 
        
    } catch(Exception ne){ 
        e.printStackTrace(); 
    } 
}

JDBC 事务

Java事务的类型有三种:JDBC 事务、JTA(Java Transaction API)事务、容器事务。

常见的容器事务如Spring事务,容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API实现。

JDBC 事务

JDBC的一切行为包括事务是基于一个Connection的,在JDBC中是通过Connection对象进行事务管理。

在JDBC中,常用的和事务相关的方法是: setAutoCommit、commit、rollback等。

  • 伪代码
try{ 
    Connection connection = DriverManager.getConnection("XXX");
    // 将自动提交设置为 false,
    // 若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交
    connection.setAutoCommit(false);

    //execute sql

    // 提交事务
    connection.commit();
} catch(Exception e) {
    // 发生异常,回滚在本事务中的操做
    connection.rollback();
}

优缺点

JDBC为使用Java进行数据库的事务操作提供了最基本的支持。

通过JDBC事务,我们可以将多个SQL语句放到同一个事务中,保证其ACID特性。

JDBC事务的主要优点就是API比较简单,可以实现最基本的事务操作,性能也相对较好。

但是,JDBC事务有一个局限:一个 JDBC 事务不能跨越多个数据库!

所以,如果涉及到多数据库的操作或者分布式场景,JDBC事务就无能为力了。

JBoss JTA

事务性标准为应用程序程序员提供了非常低级别的接口。

Sun Microsystems指定了更高级别的接口,以帮助开发分布式事务应用程序。

这些接口仍然是低级的,足以要求程序员关注事务应用程序的状态管理和并发性。

它们对于需要XA资源集成功能的应用程序最有用,而不是其他api所允许的更一般的资源。

ps: 感觉 JTA 只是一套标准,JBoss JTA 就是这套规范的一种实现。

组成

Java事务API (JTA)由三个元素组成:

  • 高级应用程序事务界定接口

  • 用于应用服务器的高级事务管理器接口

  • 用于事务资源管理器的X/Open XA协议的标准Java映射

所有JTA类和接口都在javax中声明。事务包和相应的JBossJTA实现在 com.arjuna.ats.jta 中定义。

重要

JBoss事务服务创建的每个Xid都需要在其中编码唯一的节点标识符。

JBoss事务服务仅恢复与指定节点标识符匹配的事务和状态。

节点标识符应该通过 com.arjuna.ats.arjuna.xa.nodeIdentifier 提供给JBoss事务服务。

您必须确保这个值在JBoss事务服务实例中是唯一的。

如果不提供值,JBoss事务服务将生成一个值,并通过日志基础结构报告值。

节点标识符应该是字母数字。

spring 整合

通过使用Atomikos或Bitronix嵌入式事务管理器,Spring Boot支持跨多个XA资源的分布式JTA事务。

在部署到合适的Java EE应用服务器时,还支持JTA事务。

Atomikos

Atomikos 分布式数据管理工具。

Bitronix

Bitronix 事务管理器(BTM)是JTA 1.1 API的一个简单但完整的实现。

它是一个完全工作的XA事务管理器,提供JTA API所需的所有服务,同时尽量使代码简单,以便更容易地理解XA语义。

实现原理

JTA 是如何实现事务的呢?

JTA 架构

它包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager ) 两部分。

我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器则承担着所有事务参与单元的协调与控制。

根据所面向对象的不同,我们可以将 JTA 的事务管理器和资源管理器理解为两个方面:面向开发人员的使用接口(事务管理器)和面向服务提供商的实现接口(资源管理器)。

其中开发接口的主要部分即为上述示例中引用的 UserTransaction 对象,开发人员通过此接口在信息系统中实现分布式事务;

而实现接口则用来规范提供商(如数据库连接提供商)所提供的事务服务,它约定了事务的资源管理功能,使得 JTA 可以在异构事务资源之间执行协同沟通。

以数据库为例,IBM 公司提供了实现分布式事务的数据库驱动程序,Oracle 也提供了实现分布式事务的数据库驱动程序, 在同时使用 DB2 和 Oracle 两种数据库连接时, JTA 即可以根据约定的接口协调者两种事务资源从而实现分布式事务。

正是基于统一规范的不同实现使得 JTA 可以协调与控制不同数据库或者 JMS 厂商的事务资源,其架构如下图所示:

开发人员接口---Transaction Manager---提供商接口
   |                                 |
   |                                 |
开发人员使用                    厂商1  厂商2  厂商3     

开发人员使用开发人员接口,实现应用程序对全局事务的支持;各提供商(数据库,JMS 等)依据提供商接口的规范提供事务资源管理功能;

事务管理器( TransactionManager )将应用对分布式事务的使用映射到实际的事务资源并在事务资源间进行协调与控制。

下面,本文将对包括 UserTransaction、Transaction 和 TransactionManager 在内的三个主要接口以及其定义的方法进行介绍。

分布式事务和事务管理器

如前所述,分布式事务是访问和更新两个或多个网络资源上的数据的事务。

这些资源可以由单个服务器(例如Oracle,SQL Server和Sybase)上的几个不同的RDBMS组成;

或者它们可以包括驻留在多个不同服务器上的单个类型的数据库的若干实例。

在任何情况下,分布式事务涉及各种资源管理器之间的协调。 这种协调是事务管理器的功能。

事务管理器负责做出提交或回滚任何分布式事务的最终决定。

提交决定应导致成功的事务; 回滚使数据库中的数据保持不变。

JTA在事务管理器和分布式事务中的其他组件之间指定标准Java接口:应用程序,应用程序服务器和资源管理器。

这种关系如下图所示:

20180903-jta-struct.gif

事务管理器周围的编号框对应于JTA的三个接口部分:

  1. javax.transaction.UserTransaction 接口为应用程序提供了以编程方式控制事务边界的能力。 javax.transaction.UserTransaction方法启动一个全局事务,并将事务与调用线程相关联。

  2. javax.transaction.TransactionManager 接口允许应用服务器代表被管理的应用来控制事务边界。

  3. javax.transaction.xa.XAResource 接口是基于 X/Open CAE规范(分布式事务处理:XA规范)的工业标准XA接口的Java映射。

需要注意的是,我们需要使用支持XAResource接口的JDBC驱动程序。

除了正常的JDBC交互还需要支持JTA的XAResource部分。

DataDirect Connect for JDBC驱动程序提供此支持。

应用程序代码的开发人员不应该关心分布式事务管理的细节。

这是分布式事务基础结构的工作,包括应用程序服务器,事务管理器和JDBC驱动程序。

应用程序代码的唯一注意事项是,当连接在分布式事务的范围内时,它不应调用会影响事务边界的方法。

具体来说,应用程序不应调用Connection方法commit,rollback和setAutoCommit(true),因为它们会干扰基础架构对分布式事务的管理。

分布式事务处理过程

事务管理器是分布式事务基础设施的主要组件; 但是,JDBC驱动程序和应用程序服务器组件应具有以下特性:

  • 驱动程序应该实现JDBC 2.0 API(包括可选软件包接口XADataSource和XAConnection)或更高版本和 JTA 接口 XAResource.

  • 应用服务器应提供一个为了与分布式事务基础结构进行交互并用于提高性能的具有连接池功能的实现了接口 DataSource 的类。

分布式事务处理的第一步是应用程序向事务管理器发送对事务的请求。

虽然最终事务的提交或回滚决定将事务视为单个逻辑单元,但这其中可能包含许多事务分支。

事务分支与分布式事务中涉及的每个资源管理器的请求相关联。

因此,对三个不同RDBMS的请求需要三个事务分支。

每个事务分支必须由本地资源管理器提交或回滚。

事务管理器控制事务的边界,并且负责关于总事务是否应该提交或回滚的最终决定。

该决策分为两个阶段,称为两阶段提交协议。

在第一阶段,事务管理器轮询分布式事务中涉及的所有资源管理器(RDBMS),以查看每个资源管理器是否准备好提交。 如果资源管理器无法提交,它会做出响应并回滚其事务的特定部分,以便不会更改数据。

在第二阶段中,事务管理器确定是否有资源管理器否定响应,并且如果是,则回滚整个事务。 如果没有否定响应,则管理器提交整个事务,并将结果返回给应用程序。

事务管理器代码的开发人员必须熟悉JTA的所有三个接口:UserTransaction,TransactionManager和XAResource,这些接口在Sun Java Transaction API(JTA)规范。

JDBC API教程和参考,第三版也是一个有用的参考。

JDBC驱动程序开发人员只需要关心XAResource接口。

此接口是允许资源管理器参与事务的工业标准X / Open XA协议的Java映射。

与XAResource接口连接的驱动程序的组件负责在事务管理器和资源管理器之间“转换”。

UserTransaction

开发人员通常只使用此接口实现 JTA 事务管理,其定义了如下的方法:

  • begin()- 开始一个分布式事务,(在后台 TransactionManager 会创建一个 Transaction 事务对象并把此对象通过 ThreadLocale 关联到当前线程上 )

  • commit()- 提交事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务提交)

  • rollback()- 回滚事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务回滚)

  • getStatus()- 返回关联到当前线程的分布式事务的状态 (Status 对象里边定义了所有的事务状态,感兴趣的读者可以参考 API 文档 )

  • setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚

Transaction

Transaction 代表了一个物理意义上的事务,在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程。

UserTransaction 接口中的 commit()、rollback(),getStatus() 等方法都将最终委托给 Transaction 类的对应方法执行。

Transaction 接口定义了如下的方法:

  • commit()- 协调不同的事务资源共同完成事务的提交

  • rollback()- 协调不同的事务资源共同完成事务的回滚

  • setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚

  • getStatus()- 返回关联到当前线程的分布式事务的状态

  • enListResource(XAResource xaRes, int flag)- 将事务资源加入到当前的事务中(在上述示例中,在对数据库 A 操作时 其所代表的事务资源将被关联到当前事务中,同样,在对 数- 据库 B 操作时其所代表的事务资源也将被关联到当前事务中)

  • delistResourc(XAResource xaRes, int flag)- 将事务资源从当前事务中删除

  • registerSynchronization(Synchronization sync)- 回调接口,Hibernate 等 ORM 工具都有自己的事务控制机制来保证事务, 但同时它们还需要一种回调机制以便在事务完成 时得到通知从而触发一些处理工作,如清除缓存等。这就涉及到了 Transaction 的回调接口 registerSynchronization。工具可以通过此接口将回调程序注入到事务中,当事务成功提交后,回调程序将被激活。

TransactionManager

TransactionManager 本身并不承担实际的事务处理功能,它更多的是充当用户接口和实现接口之间的桥梁。

下面列出了 TransactionManager 中定义的方法,可以看到此接口中的大部分事务方法与 UserTransaction 和 Transaction 相同。

在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程上;

同样 UserTransaction.commit() 会调用 TransactionManager.commit(), 方法将从当前线程下取出事务对象 Transaction 并把此对象所代表的事务提交, 即调用 Transaction.commit()

  • begin()- 开始事务

  • commit()- 提交事务

  • rollback()- 回滚事务

  • getStatus()- 返回当前事务状态

  • setRollbackOnly()

  • getTransaction()- 返回关联到当前线程的事务

  • setTransactionTimeout(int seconds)- 设置事务超时时间

  • resume(Transaction tobj)- 继续当前线程关联的事务

  • suspend()- 挂起当前线程关联的事务

JDBC 驱动和 XAResource

为了简化XAResource的解释,这些示例说明了当没有应用程序服务器和事务管理器时,应用程序如何使用JTA。

事实上,这些示例中的应用程序也充当应用程序服务器和事务管理器。

大多数企业使用事务管理器和应用程序服务器,因为它们比起应用程序可以更高效地管理分布式事务。

然而,通过遵循这些示例,应用程序开发人员可以测试JDBC驱动程序中JTA支持的鲁棒性。

某些示例可能不适用于特定数据库,因为是这些数据库固有问题导致的。

在使用JTA之前,你首先必须实现一个Xid类来识别事务(这通常由事务管理器完成)。

Xid包含三个元素:formatID,gtrid(全局事务ID)和bqual(分支限定符ID)。

formatID通常为零,这意味着您正在使用OSI CCR(开放系统互连承诺,并发和恢复标准)进行命名。 如果使用其他格式,formatID应大于零值,-1表示Xid为空。

gtrid和bqual可以各自包含高达64字节的二进制代码,以分别标识全局事务和分支事务。

唯一的要求是,gtrid和bqual一起必须是全局唯一的。

同样,这可以通过使用在OSI CCR中指定的命名规则来实现。

Xid 的实现

import javax.transaction.xa.*;

public class MyXid implements Xid {
    protected int formatId;
    protected byte gtrid[];
    protected byte bqual[];

    public MyXid() {
    }

    public MyXid(int formatId, byte gtrid[], byte bqual[]) {
        this.formatId = formatId;
        this.gtrid = gtrid;
        this.bqual = bqual;
    }

    public int getFormatId() {
        return formatId;
    }

    public byte[] getBranchQualifier() {
        return bqual;
    }

    public byte[] getGlobalTransactionId() {
        return gtrid;
    }
}

其次,您需要为您使用的数据库创建一个数据源:

public DataSource getDataSource() throws SQLException {
    SQLServerDataSource xaDS = new com.ddtek.jdbc.sqlserver.SQLServerDriver.SQLServerDataSource();
    xaDS.setDataSourceName("SQLServer");
    xaDS.setServerName("server");
    xaDS.setPortNumber(1433);
    xaDS.setSelectMethod("cursor");
    return xaDS;
}

1—此示例使用两阶段提交协议提交一个事务分支:

XADataSource xaDS;
XAConnection xaCon;
XAResource xaRes;
Xid xid;
Connection con;
Statement stmt;
int ret;
xaDS=getDataSource();
xaCon=xaDS.getXAConnection("jdbc_user","jdbc_password");
xaRes=xaCon.getXAResource();
con=xaCon.getConnection();
stmt=con.createStatement();
xid=new MyXid(100,new byte[]{0x01},new byte[]{0x02});
try{
    xaRes.start(xid,XAResource.TMNOFLAGS);
    stmt.executeUpdate("insert into test_table values (100)");
    xaRes.end(xid,XAResource.TMSUCCESS);

    ret=xaRes.prepare(xid);
    if(ret==XAResource.XA_OK){
        xaRes.commit(xid,false);
    }
} catch(XAException e){
    e.printStackTrace();
} finally{
    stmt.close();
    con.close();
    xaCon.close();
}

事务的回滚

xaRes.start(xid, XAResource.TMNOFLAGS);
stmt.executeUpdate("insert into test_table values (100)");
xaRes.end(xid, XAResource.TMSUCCESS);
ret = xaRes.prepare(xid);
if (ret == XAResource.XA_OK) {
    xaRes.rollback(xid);
}

分布式事务分支如何挂起

此示例显示分布式事务分支如何挂起,让同一连接执行本地事务,并稍后恢复分支。

分布式事务的两阶段提交操作不会影响本地事务。

xid = new MyXid(100, new byte[]{0x01}, new byte[]{0x02});
xaRes.start(xid, XAResource.TMNOFLAGS);
stmt.executeUpdate("insert into test_table values (100)");
xaRes.end(xid, XAResource.TMSUSPEND);
// This update is done outside of transaction scope, so it
// is not affected by the XA rollback.
stmt.executeUpdate("insert into test_table2 values (111)");
xaRes.start(xid, XAResource.TMRESUME);
stmt.executeUpdate("insert into test_table values (200)");
xaRes.end(xid, XAResource.TMSUCCESS);
ret = xaRes.prepare(xid);
if (ret == XAResource.XA_OK) {
    xaRes.rollback(xid);
}

不同事务之间共享一个XA资源

此示例说明如何在不同事务之间共享一个XA资源。

创建了两个事务分支,但它们不属于同一个分布式事务。

JTA允许XA资源在第一个分支上执行两阶段提交,即使资源仍然与第二个分支相关联。

xid1 = new MyXid(100, new byte[]{0x01}, new byte[]{0x02});
xid2 = new MyXid(100, new byte[]{0x11}, new byte[]{0x22});
xaRes.start(xid1, XAResource.TMNOFLAGS);
stmt.executeUpdate("insert into test_table1 values (100)");
xaRes.end(xid1, XAResource.TMSUCCESS);
xaRes.start(xid2, XAResource.TMNOFLAGS);
// Should allow XA resource to do two-phase commit on
// transaction 1 while associated to transaction 2
ret = xaRes.prepare(xid1);
if (ret == XAResource.XA_OK) {
    xaRes.commit(xid1, false);
}
stmt.executeUpdate("insert into test_table2 values (200)");
xaRes.end(xid2, XAResource.TMSUCCESS);
ret = xaRes.prepare(xid2);
if (ret == XAResource.XA_OK) {
    xaRes.rollback(xid2);
}

同一资源管理器

此示例说明如果不同连接上的事务分支连接到同一资源管理器,则它们可以作为单个分支连接。

此功能提高了分布式事务的效率,因为它减少了两阶段提交进程的数量。

将创建到同一数据库服务器的两个XA连接。

每个连接都创建自己的XA资源,常规JDBC连接和语句。

在第二个XA资源启动事务分支之前,它会检查它是否使用与第一个XA资源使用相同的资源管理器。

如果是这种情况,如在此示例中,它加入在第一个XA连接上创建的第一个分支,而不是创建一个新的分支。

稍后,可以使用XA资源准备和提交事务分支。

xaDS = getDataSource();
xaCon1 = xaDS.getXAConnection("jdbc_user", "jdbc_password");
xaRes1 = xaCon1.getXAResource();
con1 = xaCon1.getConnection();
stmt1 = con1.createStatement();
xid1 = new MyXid(100, new byte[]{0x01}, new byte[]{0x02});
xaRes1.start(xid1, XAResource.TMNOFLAGS);
stmt1.executeUpdate("insert into test_table1 values (100)");
xaRes1.end(xid, XAResource.TMSUCCESS);
xaCon2 = xaDS.getXAConnection("jdbc_user", "jdbc_password");
xaRes2 = xaCon2.getXAResource();
con2 = xaCon2.getConnection();
stmt2 = con2.createStatement();
if (xaRes2.isSameRM(xaRes1)) {
    xaRes2.start(xid1, XAResource.TMJOIN);
    stmt2.executeUpdate("insert into test_table2 values (100)");
    xaRes2.end(xid1, XAResource.TMSUCCESS);
} else {
    xid2 = new MyXid(100, new byte[]{0x01}, new byte[]{0x03});
    xaRes2.start(xid2, XAResource.TMNOFLAGS);
    stmt2.executeUpdate("insert into test_table2 values (100)");
    xaRes2.end(xid2, XAResource.TMSUCCESS);
    ret = xaRes2.prepare(xid2);
    if (ret == XAResource.XA_OK) {
         xaRes2.commit(xid2, false);
    }
}
ret = xaRes1.prepare(xid1);
if (ret == XAResource.XA_OK) {
    xaRes1.commit(xid1, false);
}

故障恢复

此示例显示如何在故障恢复期间恢复已准备或启发式完成的事务分支。

它首先尝试回滚每个分支; 如果失败,它会尝试告诉资源管理器丢弃有关事务的知识。

MyXid[] xids;
xids = xaRes.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN);
for (int i=0; xids!=null && i<xids.length; i++) {
    try {
        xaRes.rollback(xids[i]);
    }
    catch (XAException ex) {
        try {
            xaRes.forget(xids[i]);
        }
        catch (XAException ex1) {
            System.out.println("rollback/forget failed: " +
            ex1.errorCode);
        }
    }
}

参考资料

  • jta

http://www.oracle.com/technetwork/java/javaee/jta/index.html

https://zh.wikipedia.org/wiki/Java%E4%BA%8B%E5%8A%A1API

  • 实现

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-jta.html

https://access.redhat.com/documentation/en-us/jboss_enterprise_application_platform/5/html/transactions_development_guide/chap-transactions_jta_programmers_guide-an_introduction_to_the_jta

  • JTA 实现原理

https://my.oschina.net/fileoptions/blog/898579

https://www.progress.com/tutorials/jdbc/understanding-jta

https://leokongwq.github.io/2017/01/03/transaction-understanding-jta.html