Distributed Transactions and the Transaction Manager As we stated previously, a distributed transaction is a transaction that
accesses and updates data on two or more networked resources. These
resources could consist of several different RDBMSs housed on a single
sever, for example, Oracle, SQL Server, and Sybase; or they could include
several instances of a single type of database residing on a number of
different servers. In any case, a distributed transaction involves
coordination among the various resource managers. This coordination is the
function of the transaction manager.
The transaction manager is responsible for making the final decision
either to commit or rollback any distributed transaction. A commit decision
should lead to a successful transaction; rollback leaves the data in the
database unaltered. JTA specifies standard Java interfaces between the
transaction manager and the other components in a distributed transaction:
the application, the application server, and the resource managers. This
relationship is illustrated in the following diagram:
The numbered boxes around the transaction manager correspond to the three
interface portions of JTA:
1UserTransactionThe javax.transaction.UserTransaction
interface provides the application the ability to control transaction
boundaries programmatically. The javax.transaction.UserTransaction method
starts a global transaction and associates the transaction with the calling
thread.
2Transaction ManagerThe javax.transaction.TransactionManager
interface allows the application server to control transaction boundaries on
behalf of the application being managed.
3XAResourceThe javax.transaction.xa.XAResource interface is a
Java mapping of the industry standard XA interface based on the X/Open CAE
Specification (Distributed Transaction Processing: The XA
Specification).
Notice that a critical link is support of the XAResource interface by the
JDBC driver. The JDBC driver must support both normal JDBC interactions,
through the application and/or the application server, as well as the
XAResource portion of JTA.
Developers of code at the application level should not be concerned about
the details of distributed transaction management. This is the job of the
distributed transaction infrastructurethe application server, the
transaction manager, and the JDBC driver. The only caveat for application
code is that is should not invoke a method that would affect the boundaries
of a transaction while the connection is in the scope of a distributed
transaction. Specifically, an application should not call the Connection
methods commit, rollback, and setAutoCommit(true) because they would
interfere with the infrastructure's management of the distributed
transaction.
The Distributed Transaction Process
The transaction manager is the primary component of the distributed
transaction infrastructure; however, the JDBC driver and application server
components should have the following characteristics:
- The driver should implement the JDBC 2.0 API, including the Optional
Package interfaces XADataSource and XAConnection, and the JTA interface
XAResource.
- The application server should provide a DataSource class that is
implemented to interact with the distributed transaction infrastructure and
a connection pooling module (for improved performance).
The first step of the distributed transaction process is for the
application to send a request for the transaction to the transaction
manager. Although the final commit/rollback decision treats the transaction
as a single logical unit, there can be many transaction branches
involved. A transaction branch is associated with a request to each
resource manager involved in the distributed transaction. Requests to three
different RDBMSs, therefore, require three transaction branches. Each
transaction branch must be committed or rolled back by the local resource
manager. The transaction manager controls the boundaries of the transaction
and is responsible for the final decision as to whether or not the total
transaction should commit or rollback. This decision is made in two phases,
called the Two-Phase Commit Protocol.
In the first phase, the transaction manager polls all of the resource
managers (RDBMSs) involved in the distributed transaction to see if each one
is ready to commit. If a resource manager cannot commit, it responds
negatively and rolls back its particular part of the transaction so that
data is not altered.
In the second phase, the transaction manager determines if any of the
resource managers have responded negatively, and, if so, rolls back the
whole transaction. If there are no negative responses, the translation
manager commits the whole transaction, and returns the results to the
application.
Developers of transaction manager code must be conversant with all three
interfaces of JTA: UserTransaction, TransactionManager, and XAResource,
which are described in the Sun JTA
specification. The JDBC API Tutorial and Reference, Second Edition is
also a useful reference. JDBC driver developers need only be concerned with
the XAResource interface. This interface is a Java mapping of the industry
standard X/Open XA protocol that allows a resource manager to participate in
a transaction. The component of the driver connected with the XAResource
interface is responsible for "translating" between the transaction manager
and the resource manager. The following section provides examples of
XAResource calls.
The JDBC Driver and XAResource
To simplify the explanation of XAResource, these examples illustrate how an
application would use JTA when there is no application server and
transaction manager involved. Basically, the application in these examples
is also acting as application server and transaction manager. Most
enterprises use transaction managers and application servers because they
manage distributed transactions much more efficiently than an application
can. By following these examples, however, an application developer can test
the robustness of JTA support in a JDBC driver. Some examples may not work
for a particular database because of inherent problems associated with that
database.
Before using JTA, you must first implement an Xid class for identifying
transactions (this would normally be done by the transaction manager). The
Xid contains three elements: formatID, gtrid (global transaction ID), and
bqual (branch qualifier ID).
The formatID is usually zero, meaning that you are using the OSI CCR
(Open Systems Interconnection Commitment, Concurrency, and Recovery
standard) for naming. If you are using another format, the formatID should
be greater than zero. A value of -1 means that the Xid is null.
The gtrid and bqual can each contain up to 64 bytes of binary code to
identify the global transaction and the branch transaction, respectively.
The only requirement is that the gtrid and bqual taken together must be
globally unique. Again, this can be achieved by using the naming rules
specified in the OSI CCR.
The following example illustrates implementation of an 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;
}
}
Second, you need to create a datasource for the database that you are
using:
public DataSource getDataSource()
throws SQLException
{
SQLServerDataSource xaDS = new
com.merant.datadirect.jdbcx.sqlserver.SQLServerDataSource();
xaDS.setDataSourceName("SQLServer");
xaDS.setServerName("server");
xaDS.setPortNumber(1433);
xaDS.setSelectMethod("cursor");
return xaDS;
}
Example 1This example uses the two-phase commit protocol to
commit one transaction branch:
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();
}
Because the initialization code is the same or very similar for all the
examples, only significantly different code is represented from this point
forward.
Example 2This example, similar to Example 1, illustrates a
rollback:
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);
}
Example 3This example shows how a distributed transaction
branch suspends, lets the same connection do a local transaction, and them
resumes the branch later. The two-phase commit actions of distributed
transaction do not affect the local transaction.
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);
}
Example 4This example illustrates how one XA resource can be
shared among different transactions. Two transaction branches are created,
but they do not belong to the same distributed transaction. JTA allows the
XA resource to do a two-phase commit on the first branch even though the
resource is still associated with the second branch.
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(xid2, 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);
}
Example 5This example illustrates how transaction branches
on different connections can be joined as a single branch if they are
connected to the same resource manager. This feature improves distributed
transaction efficiency because it reduces the number of two-phase commit
processes. Two XA connections to the same database server are created. Each
connection creates its own XA resource, regular JDBC connection, and
statement. Before the second XA resource starts a transaction branch, it
checks to see if it uses the same resource manager as the first XA resource
uses. If this is case, as in this example, it joins the first branch created
on the first XA connection instead of creating a new branch. Later, the
transaction branch can be prepared and committed using either XA
resource.
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 = xaCon1.getXAResource();
con2 = xaCon1.getConnection();
stmt2 = con1.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);
}
Example 6This example shows how to recover prepared or
heuristically completed transaction branches during failure recovery. It
first tries to rollback each branch; if it fails, it tries to tell resource
manager to discard knowledge about the transaction.
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);
}
}
}
References
Cheung & Matena, Java Transaction API (JTA), 1999, Sun Microsystems,
Inc.
White, Fisher, Cattell, Hamilton, Hapner, and Sun Microsystems, Inc.,
JDBC API Tutorial and Reference, Second Edition, 1999,
Addison-Wesley.
X/Open CAE Specification, Distributed Transaction Processing: The XA
Specification, 1991, The X/Open Company.
| Copyright © 2002 DataDirect, Inc. Reprinted with permission.
|
|