关键词 |
概念 |
XA | XA是指X/Open Distributed Transaction Processing(DTP)协议规范中定义的一种事务处理标准。XA协议规范定义了事务管理器(Transaction Manager)与资源管理器(Resource Manager)之间的接口规范,使得不同的数据库系统和中间件可以支持分布式事务的实现,确保事务的正确性和一致性。XA协议规范包含了两阶段提交(2PC)的流程,2PC是指分布式事务的两个阶段:准备阶段和提交阶段。在准备阶段,事务管理器向所有参与者(Resource Manager)发送prepare请求,每个参与者都将事务结果保存在本地日志中,等待最终提交命令。在提交阶段,如果所有参与者都能够正常响应提交请求,则最终提交命令会被发送给所有参与者,此时分布式事务被提交成功,否则分布式事务被回滚。 |
比如,现在要从支付宝向余额宝转5000元,那么就需要从支付宝扣5000元,同时向余额宝加5000元,并且保证这两个操作在同一个事务中。由于支付宝与余额宝分属于各自不同的数据库,就需要进行分布式事务。当需要开始两阶段提交时,首先将请求交给事务协调器。事务协调器在收到请求以后,就会同时向事务的各方发送各自的请求,比如支付宝扣5000元,余额宝加5000元。但是,事务的各方在执行操作时,只操作不提交(只写了日志,没有commit),这就是两阶段提交的第一阶段。当事务的各方成功完成各自的操作并反馈给事务协调器后,开始进入第二阶段(commit)。
只操作不提交,就意味着,事务的各方还是处于更新前的状态。当事务的各方成功完成各自的操作并进入第二阶段时,事务协调器就会同时发出提交命令。众所周知,数据库的提交操作是在一瞬间就执行完了的,我们达到的效果就是,在那一瞬间事务的各方就由更新前状态转变为了更新后状态,从而保障了事务的一致性。这就是“两阶段提交”。
采用两阶段提交,在第一阶段是“只操作不提交”,那么如果事务中的某一方执行失败,则同时回滚,从而保障了事务一致性。然而,在这个过程中,假设支付宝早早就完成了自己的操作,它也只能处于锁定状态,等待余额宝操作完成。在这段时间里,由于处于锁定状态,其他用户就不能操作,只能等待。如果支付宝等待了半天,等来的却是余额宝执行失败,不得不回滚,那么支付宝的等待既影响了系统性能,又影响了系统整体的吞吐量。因此,系统需要通过设计,有效提高事务的成功率,即“要么就不做,要做就必须要成功”。因此,在两阶段提交的基础上,在第一阶段的前面再增加一个校验的阶段,校验事务的各方是否能完成各自的事务,这就是“三阶段提交”。如支付宝账号没有5000元钱,或者余额宝账号无效,那么就没有必要执行该事务了。
降低失败的概率
维度 | 优点 | 缺点 |
性能 | 适合简单业务场景,对于性能要求不高 | “只操作不提交”会锁定数据库资源一段时间,其他请求只能等待,当业务复杂或者分库较多的场景下,会成为性能瓶颈 |
XA支持 | Oracle数据库 IBM DB2数据库 Microsoft SQL Server数据库 MySQL数据库 PostgreSQL数据库 Sybase Adaptive Server Enterprise(ASE) Informix Dynamic Server SAP HANA Teradata |
|
数据一致性 | 当二阶段提交某一个节点出现异常,无法回滚已经提交的数据 | |
复杂度 | 简单 |
网络连接断开,或者数据库异常,事务协调器异常流程中断,需要有监控告警,人工接入
此示例包括以下步骤:
此代码示例使用MySQL数据库的XA协议和Java事务API,演示了如何使用XA事务管理器在两个数据库之间执行分布式事务。
package org.example;
import com.mysql.cj.jdbc.MysqlXADataSource;
import javax.sql.XAConnection;
import javax.sql.XADataSource;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
public class XATransactionExample {
public static void main(String[] args) throws SQLException {
XADataSource ds1 = getDataSource("jdbc:mysql://localhost:3306/testdb", "user1", "password1");
XADataSource ds2 = getDataSource("jdbc:mysql://localhost:3306/testdb", "user2", "password2");
Connection conn1 = null;
Connection conn2 = null;
XAConnection xaConn1 = null;
XAConnection xaConn2 = null;
try {
xaConn1 = ds1.getXAConnection();
xaConn2 = ds2.getXAConnection();
conn1 = xaConn1.getConnection();
conn2 = xaConn2.getConnection();
conn1.setAutoCommit(false);
conn2.setAutoCommit(false);
XAResource xaRes1 = xaConn1.getXAResource();
XAResource xaRes2 = xaConn2.getXAResource();
// create Xid
byte[] gid = new byte[]{0x01};
byte[] bqual1 = new byte[]{0x01};
byte[] bqual2 = new byte[]{0x02};
Xid xid1 = new MyXid(0x1234, gid, bqual1);
Xid xid2 = new MyXid(0x5678, gid, bqual2);
// start XA transaction
xaRes1.start(xid1, XAResource.TMNOFLAGS);
xaRes2.start(xid2, XAResource.TMNOFLAGS);
// update account balance
Statement stmt1 = conn1.createStatement();
stmt1.executeUpdate("UPDATE account SET balance=balance-100 WHERE id=1");
Statement stmt2 = conn2.createStatement();
stmt2.executeUpdate("UPDATE account SET balance=balance+100 WHERE id=2");
// prepare XA transaction
int ret1 = xaRes1.prepare(xid1);
int ret2 = xaRes2.prepare(xid2);
if (ret1 == XAResource.XA_OK && ret2 == XAResource.XA_OK) {
// commit XA transaction
xaRes1.commit(xid1, false);
xaRes2.commit(xid2, false);
} else {
// rollback XA transaction
xaRes1.rollback(xid1);
xaRes2.rollback(xid2);
}
} catch (SQLException ex) {
ex.printStackTrace();
} catch (XAException ex) {
ex.printStackTrace();
} finally {
try { if (xaConn1 != null) xaConn1.close(); } catch (SQLException ex) {}
try { if (xaConn2 != null) xaConn2.close(); } catch (SQLException ex) {}
}
}
private static XADataSource getDataSource(String url, String user, String password) throws SQLException {
MysqlXADataSource ds = new MysqlXADataSource();
ds.setURL(url);
ds.setUser(user);
ds.setPassword(password);
ds.setReconnectAtTxEnd(false);
return ds;
}
static class MyXid implements Xid {
private int formatId;
private byte[] globalTransactionId;
private byte[] branchQualifier;
public MyXid(int formatId, byte[] globalTransactionId, byte[] branchQualifier) {
this.formatId = formatId;
this.globalTransactionId = globalTransactionId;
this.branchQualifier = branchQualifier;
}
public int getFormatId() {
return formatId;
}
public byte[] getGlobalTransactionId() {
return globalTransactionId;
}
public byte[] getBranchQualifier() {
return branchQualifier;
}
}
}
maven依赖
mysql
mysql-connector-java
8.0.28