了解 XA 事务
Microsoft JDBC Driver for SQL Server 提供对 Java Platform, Enterprise Edition/JDBC 2.0 可选分布式事务的支持。 从 SQLServerXADataSource 类获取的 JDBC 连接可以参与标准分布式事务处理环境,例如 Java Platform, Enterprise Edition (Java EE) 应用程序服务器。
在本文中,XA 代表扩展体系结构。
警告
适用于 SQL 的 Microsoft JDBC Driver 4.2(和更高版本)包含现有功能的新的超时选项,以供自动回滚尚未准备好的事务。 有关更多详细信息,请参阅本主题后面将介绍的为自动回滚尚未准备好的事务配置服务器端超时设置。
备注
用于此分布式事务实现的类如下:
类 | 实现 | 说明 |
---|---|---|
com.microsoft.sqlserver.jdbc.SQLServerXADataSource | javax.sql.XADataSource | 分布式连接的类工厂。 |
com.microsoft.sqlserver.jdbc.SQLServerXAResource | javax.transaction.xa.XAResource | 事务管理器的资源适配器。 |
备注
XA 分布式事务连接默认使用“提交读”隔离级别。
XA 事务使用准则和限制
以下额外的准则适用于紧密耦合的事务:
将 XA 事务与 Microsoft 分布式事务处理协调器 (MS DTC) 一起使用时,你可能会注意到 MS DTC 的当前版本不支持紧密结合的 XA 分支行为。 例如,MS DTC 在 XA 分支事务 ID (XID) 与 MS DTC 事务 ID 之间具有一对一的映射,由松散耦合的 XA 分支执行的工作彼此之间是隔离的。
MS DTC 还支持紧密耦合的 XA 分支,其中全局事务 ID (GTRID) 相同的多个 XA 分支映射到一个 MS DTC 事务 ID。 通过该项支持,多个紧密耦合的 XA 分支可在资源管理器中查看彼此的更改,例如 SQL Server。
借助 SSTRANSTIGHTLYCPLD标志,应用程序可以使用紧密耦合的 XA 事务,这些事务有不同的 XA 分支事务 ID (BQUAL),但有相同的全局事务 ID (GTRID) 和格式 ID (FormatID)。 为了使用该功能,必须对 XAResource.start 方法的 flags 参数设置 SSTRANSTIGHTLYCPLD:
xaRes.start(xid, SQLServerXAResource.SSTRANSTIGHTLYCPLD);
配置说明
如果要同时使用 XA 数据源和 Microsoft 分布式事务处理协调器 (MS DTC) 来处理分布式事务,则需要执行以下步骤。 大致步骤如下所示:
- 确保 MS DTC 服务正在运行并自动启动。
- 配置服务器端组件。
- 配置服务器端超时(可选)。
- 向用户授予访问权限。
运行 MS DTC 服务
在 Service Manager 中,MS DTC 服务应标记为“自动”,以确保其在启动 SQL Server 服务时运行。 若要为 XA 事务启用 MS DTC,必须执行以下步骤:
在 Windows Vista 和更高版本上:
选择“开始”按钮,在“开始搜索”框中键入 dcomcnfg,然后按 ENTER 打开“组件服务”。 也可以在“开始搜索”框中键入
%windir%\system32\comexp.msc
打开“组件服务”。依次展开“组件服务”、“计算机”、“我的电脑”和“分布式事务处理协调器”。
右键单击“本地 DTC”,再选择“属性” 。
选择“本地 DTC 属性”对话框上的“安全”选项卡。
选中“启用 XA 事务”复选框,然后选择“确定”。 此操作将导致 MS DTC 服务重启。
再次选择“确定”以关闭“属性”对话框,然后关闭“组件服务”。
停止 SQL Server,然后重新启动,确保它与 MS DTC 更改同步。 (此步骤在 SQL Server 2019、SQL Server 2017 CU 16 及更高版本中是可选的。)
配置 JDBC 分布式事务组件
配置服务器端组件的步骤因目标服务器版本而异。 若要检查服务器版本,请对服务器执行查询 SELECT @@VERSION
并查看输出。 对于 SQL Server 2017 累积更新 (CU) 16 及更高版本,请按照 SQL Server 2017 CU16 及更高版本的说明进行操作。 对于较低的 SQL Server 版本,请按照 SQL Server 2017 CU15 及更低版本的说明进行操作。
SQL Server 2017 CU16 及更高版本
若要使用 JDBC 驱动程序启用必需组件来执行 XA 分布式事务,请执行以下存储过程。
EXEC sp_sqljdbc_xa_install
若要禁用组件,请执行以下存储过程。
EXEC sp_sqljdbc_xa_uninstall
SQL Server 2017 CU15 及更低版本
注意
这仅适用于 SQL Server 2017 CU15 及更低版本。 sqljdbc_xa.dll 提供的函数已包含在 SQL Server 2017 CU16 及更高版本中。
JDBC 分布式事务组件包含在 JDBC 驱动程序安装的 xa 目录中。 这些组件包括 xa_install.sql 和 sqljdbc_xa.dll 文件。 如果在不同的客户端上安装了不同版本的 JDBC 驱动程序,则建议在服务器上使用最新的 sqljdbc_xa.dll。
可通过以下步骤配置 JDBC 驱动程序分布式事务组件:
将新 sqljdbc_xa.dll 从 JDBC 驱动程序安装目录复制到每台参与分布式事务的 SQL Server 计算机的 Binn 目录中。
注意
如果将 XA 事务用于 32 位 SQL Server(仅适用于 SQL Server 2014 或更低版本),则使用 x86 文件夹中的 sqljdbc_xa.dll 文件,即使 SQL Server 安装在 x64 处理器上也不例外。 如果在 x64 处理器上将 XA 事务用于 64 位 SQL Server,则使用 x64 文件夹中的 sqljdbc_xa.dll 文件。
对每个参与分布式事务的 SQL Server 实例执行数据库脚本 xa_install.sql。 此脚本将安装 sqljdbc_xa.dll 调用的扩展存储过程。 这些扩展存储过程为 Microsoft JDBC Driver for SQL Server 实现分布式事务和 XA 支持。 需要以 SQL Server 实例管理员的身份来运行此脚本。
若要为特定用户授予使用 JDBC 驱动程序参与分布式事务的权限,请将该用户添加至 SqlJDBCXAUser 角色。
在每个 SQL Server 实例上,一次只能配置一个版本的 sqljdbc_xa.dll 程序集。 应用程序可能需要使用不同版本的 JDBC 驱动程序,才能使用 XA 连接来连接到同一个 SQL Server 实例。 在这种情况下,必须在 SQL Server 实例上安装最新的 JDBC 驱动程序附带的 sqljdbc_xa.dll。
有三种方法可验证 SQL Server 实例上当前安装的 sqljdbc_xa.dll 的版本:
打开参与分布式事务处理的 SQL Server 计算机的 LOG 目录。 选择并打开 SQL Server 的“ERRORLOG”文件。 在“ERRORLOG”文件中搜索“使用‘SQLJDBC_XA.dll’版本 ...”这一短语。
打开参与分布式事务处理的 SQL Server 计算机的 Binn 目录。 选择 sqljdbc_xa.dll 程序集。
- 在 Windows Vista 或更高版本上:右键单击“sqljdbc_xa.dll”,然后选择“属性”。 然后选择“详细信息”选项卡。“文件版本”字段显示 SQL Server 实例上当前安装的 sqljdbc_xa.dll 的版本。
按照下一节的代码示例所示设置日志记录功能。 在输出日志文件中搜索“服务器 XA DLL 版本:...”这一短语。
升级 sqljdbc_xa.dll
注意
这仅适用于 SQL Server 2017 CU15 及更低版本。 sqljdbc_xa.dll 提供的函数已包含在 SQL Server 2017 CU16 及更高版本中。
您在安装新版本的 JDBC 驱动程序时,也应该使用新版本中的 sqljdbc_xa.dll 来升级服务器上的 sqljdbc_xa.dll。
重要
你应在维护时段内或在未处理任何 MS DTC 事务时升级 sqljdbc_xa.dll。
使用 Transact-SQL 命令卸载 sqljdbc_xa.dll。
DBCC sqljdbc_xa (FREE)
将新 sqljdbc_xa.dll 从 JDBC 驱动程序安装目录复制到每台参与分布式事务的 SQL Server 计算机的 Binn 目录中。
当 sqljdbc_xa.dll 中调用扩展过程时,会加载新 DLL。 不需要重启 SQL Server 来加载新定义。
为自动回滚尚未准备好的事务配置服务器端超时设置
有两个注册表设置(DWORD 值)来控制分布式事务的超时行为:
XADefaultTimeout
(以秒为单位):在用户未指定任何超时时使用的默认超时值。 默认值为 0。XAMaxTimeout
(以秒为单位):用户可以设置的最大超时值。 默认值为 0。
这些设置特定用于 SQL Server 实例,应在以下注册表项下创建:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL<version>.<instance_name>\XATimeout
备注
对于在 64 位计算机中运行的 32 位 SQL Server(仅适用于 SQL Server 2014 或更低版本),应在以下项的下面创建注册表设置:HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SQL Server\MSSQL<version>.<instance_name>\XATimeout
将为每个开始的事务设置超时值,并且在超时过期后由 SQL Server 回滚该事务。 超时具体取决于这些注册表设置以及用户通过 XAResource.setTransactionTimeout() 指定的设置。 有关如何解释这些超时值的一些示例如下所示:
XADefaultTimeout = 0
,XAMaxTimeout = 0
意味着不使用默认超时值,并且在客户端上不强制执行最大超时。 在这种情况下,只有在客户端使用 XAResource.setTransactionTimeout 设置超时的情况下,事务才会有超时。
XADefaultTimeout = 60
,XAMaxTimeout = 0
表示如果客户端不指定任何超时,所有事务的超时都为 60 秒。 如果客户端指定超时,将使用该超时值。 不强制执行最大值超时值。
XADefaultTimeout = 30
,XAMaxTimeout = 60
表示如果客户端不指定任何超时,所有事务的超时都为 30 秒。 如果客户端指定了任何超时,则只要此时间小于 60 秒(最大值),就会使用它。
XADefaultTimeout = 0
,XAMaxTimeout = 30
表示如果客户端不指定任何超时,所有事务的超时都为 30 秒(最大值)。 如果客户端指定了任何超时,则只要此时间小于 30 秒(最大值),就会使用它。
配置用户定义角色
若要为特定用户授予使用 JDBC 驱动程序参与分布式事务的权限,请将该用户添加至 SqlJDBCXAUser 角色。 例如,使用以下 Transact-SQL 代码将名为“shelly”(SQL 标准登录用户名为“shelly”)的用户添加到 SqlJDBCXAUser 角色:
USE master
GO
EXEC sp_grantdbaccess 'shelly', 'shelly'
GO
EXEC sp_addrolemember [SqlJDBCXAUser], 'shelly'
SQL 用户定义的角色是按数据库定义的。 若要出于安全目的创建自己的角色,必须在每个数据库中定义角色,并在每个数据库中添加用户。 主数据库中 SqlJDBCXAUser 角色的定义非常严格,因为该角色用于授予对主数据库中驻留的 SQL JDBC 扩展存储过程的访问权限。 登录到主数据库后,必须先授予单个用户对主数据库的访问权限,然后再授予这些用户对 SqlJDBCXAUser 角色的访问权限。
示例
import java.net.Inet4Address;
import java.sql.*;
import java.util.Random;
import javax.sql.XAConnection;
import javax.transaction.xa.*;
import com.microsoft.sqlserver.jdbc.*;
public class testXA {
public static void main(String[] args) throws Exception {
// Create variables for the connection string.
String prefix = "jdbc:sqlserver://";
String serverName = "localhost";
int portNumber = 1433;
String databaseName = "AdventureWorks";
String user = "UserName";
String password = "<password>";
String connectionUrl = prefix + serverName + ":" + portNumber + ";encrypt=true;databaseName=" + databaseName + ";user="
+ user + ";password=" + password;
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
try (Connection con = DriverManager.getConnection(connectionUrl); Statement stmt = con.createStatement()) {
stmt.executeUpdate("CREATE TABLE XAMin (f1 int, f2 varchar(max))");
}
// Create the XA data source and XA ready connection.
SQLServerXADataSource ds = new SQLServerXADataSource();
ds.setUser(user);
ds.setPassword(password);
ds.setServerName(serverName);
ds.setPortNumber(portNumber);
ds.setDatabaseName(databaseName);
XAConnection xaCon = ds.getXAConnection();
try (Connection con = xaCon.getConnection()) {
// Get a unique Xid object for testing.
XAResource xaRes = null;
Xid xid = null;
xid = XidImpl.getUniqueXid(1);
// Get the XAResource object and set the timeout value.
xaRes = xaCon.getXAResource();
xaRes.setTransactionTimeout(0);
// Perform the XA transaction.
System.out.println("Write -> xid = " + xid.toString());
xaRes.start(xid, XAResource.TMNOFLAGS);
PreparedStatement pstmt = con.prepareStatement("INSERT INTO XAMin (f1,f2) VALUES (?, ?)");
pstmt.setInt(1, 1);
pstmt.setString(2, xid.toString());
pstmt.executeUpdate();
// Commit the transaction.
xaRes.end(xid, XAResource.TMSUCCESS);
xaRes.commit(xid, true);
}
xaCon.close();
// Open a new connection and read back the record to verify that it worked.
try (Connection con = DriverManager.getConnection(connectionUrl); Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM XAMin")) {
rs.next();
System.out.println("Read -> xid = " + rs.getString(2));
stmt.executeUpdate("DROP TABLE XAMin");
}
}
}
class XidImpl implements Xid {
public int formatId;
public byte[] gtrid;
public byte[] bqual;
public byte[] getGlobalTransactionId() {
return gtrid;
}
public byte[] getBranchQualifier() {
return bqual;
}
public int getFormatId() {
return formatId;
}
XidImpl(int formatId, byte[] gtrid, byte[] bqual) {
this.formatId = formatId;
this.gtrid = gtrid;
this.bqual = bqual;
}
public String toString() {
int hexVal;
StringBuffer sb = new StringBuffer(512);
sb.append("formatId=" + formatId);
sb.append(" gtrid(" + gtrid.length + ")={0x");
for (int i = 0; i < gtrid.length; i++) {
hexVal = gtrid[i] & 0xFF;
if (hexVal < 0x10)
sb.append("0" + Integer.toHexString(gtrid[i] & 0xFF));
else
sb.append(Integer.toHexString(gtrid[i] & 0xFF));
}
sb.append("} bqual(" + bqual.length + ")={0x");
for (int i = 0; i < bqual.length; i++) {
hexVal = bqual[i] & 0xFF;
if (hexVal < 0x10)
sb.append("0" + Integer.toHexString(bqual[i] & 0xFF));
else
sb.append(Integer.toHexString(bqual[i] & 0xFF));
}
sb.append("}");
return sb.toString();
}
// Returns a globally unique transaction id.
static byte[] localIP = null;
static int txnUniqueID = 0;
static Xid getUniqueXid(int tid) {
Random rnd = new Random(System.currentTimeMillis());
txnUniqueID++;
int txnUID = txnUniqueID;
int tidID = tid;
int randID = rnd.nextInt();
byte[] gtrid = new byte[64];
byte[] bqual = new byte[64];
if (null == localIP) {
try {
localIP = Inet4Address.getLocalHost().getAddress();
} catch (Exception ex) {
localIP = new byte[] {0x01, 0x02, 0x03, 0x04};
}
}
System.arraycopy(localIP, 0, gtrid, 0, 4);
System.arraycopy(localIP, 0, bqual, 0, 4);
// Bytes 4 -> 7 - unique transaction id.
// Bytes 8 ->11 - thread id.
// Bytes 12->15 - random number generated by using seed from current time in milliseconds.
for (int i = 0; i <= 3; i++) {
gtrid[i + 4] = (byte) (txnUID % 0x100);
bqual[i + 4] = (byte) (txnUID % 0x100);
txnUID >>= 8;
gtrid[i + 8] = (byte) (tidID % 0x100);
bqual[i + 8] = (byte) (tidID % 0x100);
tidID >>= 8;
gtrid[i + 12] = (byte) (randID % 0x100);
bqual[i + 12] = (byte) (randID % 0x100);
randID >>= 8;
}
return new XidImpl(0x1234, gtrid, bqual);
}
}