为了保证分布式事务提交的原子性,XA 协议采用了两阶段提交的策略。当 XA 事务执行完事务逻辑之后便可以进入到提交阶段,TM(事务管理器,两阶段提交中的协调者)端向所有 RM(资源管理器,两阶段提交中的参与者)端发送 XA prepare 请求。TM 端收齐所有 RM 端的 XA prepare 成功响应后便可以向所有的 RM 端发送 XA commit 请求。如果 TM 端没有收到某 RM 端的 XA prepare 响应,便无法确定该 XA 事务的状态,从而无法根据状态来结束该 XA 事务。
OBServer 作为 RM 端,当成功执行 XA prepare 之后(此时该 XA 事务在 OBServer 端的状态为 prepared),OBServer 便不能自行决定该 XA 事务的提交或者回滚。处于 prepared 状态的 XA 事务依然持有数据库资源(例如行锁),因此其他事务可能产生锁冲突。如果 XA 事务长时间处于 prepared 状态,可能会影响业务执行。因此,在特殊情况下,可以采取一定策略手动结束相应的 XA 事务。
查询 XA 事务信息
可以通过内部表来查询 XA 事务信息。在 OceanBase 数据库 Oracle 模式下,通过以下语句来查询当前系统中所有活跃的 XA 事务信息。
obclient> select * from sys.all_virtual_tenant_global_transaction_agent;
该内部表中各列的说明如下所示:
| 列名 | 说明 |
|---|---|
| gmt_create | 该记录的创建时间。 |
| gmt_modified | 该记录最后的修改时间。 |
| gtrid | 该 XA 事务的全局事务标识。 |
| bqual | 该 XA 事务的分支标识。 |
| format_id | 该 XA 事务标识的格式类型。 |
| trans_id | 该 XA 事务在 OBServer 端内部对应的事务 id。 |
| coordinator | 该 XA 事务在 OBServer 端内部对应的协调者。默认为 NULL,进入提交阶段后为协调者分区。 |
| scheduler_ip | 该 XA 事务在 OBServer 端内部 scheduler 所在服务器的 ip 地址。 |
| scheduler_port | 该 XA 事务在 OBServer 端内部 scheduler 所在服务器的端口号。 |
| is_readonly | 该 XA 事务是否为只读事务。 |
| state | 该 XA 事务状态。 |
| end_flag | 该 XA 事务的 flag 信息。 |
警告
该内部表为租户级内部表,客户端需要连接相应的租户来查询该租户下 XA 事务的信息。
format_id 为 -2 的记录是 tightly 模式下特殊记录。
该内部表中,state 表示了 XA 事务在 OBServer 端的状态,该列为数值类型,不同值表示不同状态,具体说明如下:
| 状态值 | 状态 | 说明 |
|---|---|---|
| 1 | ACTIVE | 该 branch 已成功执行 xa start。 |
| 2 | IDLE | 该 branch 已成功执行 xa end。 |
| 3 | PREPARED | 该 XA 事务已成功执行 xa prepare。 |
| 4 | COMMITTED | 该 XA 事务已提交。 |
| 5 | ROLLBACKED | 该 XA 事务已回滚。 |
因此,我们可以通过以下语句来查询已经处于 prepared 状态的 XA 事务。
obclient> select * from sys.all_virtual_tenant_global_transaction_agent where format_id > -1 and state = 3;
结束处于 prepared 状态的 XA 事务
通过内部表可以获取到已经处于 prepared 状态的 XA 事务,我们通过 gmt_modified 列信息便可以知道该 XA 事务何时进入到 prepared 状态,从而可以进一步得知该 XA 事务处于 prepared 状态的时间。如果想手动结束一个处于 prepared 状态的事务,可以通过租户客户端连接至 OceanBase,执行相应 PL 语句。
注意
手动结束 XA 事务可能引入一致性问题。例如,存在两个 RM,我们手动提交了 OBServer 端的 XA 事务,而另一个 RM 端可能回滚了该 XA 事务,从而破坏了事务的原子性。因此,请谨慎使用!
我们已经知道如何通过内部表查询 XA 事务的信息。我们主要通过查询得到的 xid 来结束相应的 XA 事务。xid主要包含三个部分:gtrid、bqual 和 format_id。需要注意的是,gtrid 和 bqual 可能是二进制类型,无法通过字符信息展示出来,我们需要将其转换成十六进制。通过内部表查询我们可以获取到 XA 事务的trans_id,假如我们想要结束的 XA 事务对应的 trans_id 的 hash 值为 123456,我们通过如下 SQL 来获取该 XA 事务 xid。
obclient> select rawtohex(gtrid), rawtohex(bqual), format_id from sys.all_virtual_tenant_global_transaction_agent where state = 3 and format_id > -2 and trans_id like '%123456%';
通过以上语句,我们可以获取得到十六进制形式的 gtrid 和 bqual。假设,gtrid 为 aaaa,bqual 为bbbb,format_id 为 123。
我们通过执行以下语句来提交该 XA 事务(通过 obclient 连接至相应租户):
set serveroutput on;
declare
l_xid DBMS_XA_XID;
l_ret PLS_INTEGER;
BEGIN
l_xid.formatid := 123;
l_xid.gtrid := hextoraw('aaaa');
l_xid.bqual := hextoraw('bbbb');
l_ret := DBMS_XA.XA_COMMIT(xid => l_xid, onePhase => FALSE);
dbms_output.put_line(l_ret);
END;
/
通过执行以下语句来回滚该 XA 事务(通过 obclient 连接至相应租户):
set serveroutput on;
declare
l_xid DBMS_XA_XID;
l_ret PLS_INTEGER;
BEGIN
l_xid.formatid := 123;
l_xid.gtrid := hextoraw('aaaa');
l_xid.bqual := hextoraw('bbbb');
l_ret := DBMS_XA.XA_ROLLBACK(xid => l_xid);
dbms_output.put_line(l_ret);
END;
/
需要注意的是,为了获取 XA 语句的执行结果,需要将其返回值打印出来。因此,在执行 PL 之前,需要先执行以下 SQL 语句:
set serveroutput on;
返回值为 0 说明 XA 语句执行成功,如果返回其他错误码,需通过日志进一步确认问题。
批量结束处于 prepared 状态的 XA 事务
如果 TM 端出现异常,可能导致 RM 端出现大量处于 prepared 状态的事务。上述方法无法快速批量的结束 XA 事务,从而导致系统中可能会出现以下两种情况:
- 由于处于 prepared 状态的事务依然持有锁资源,这将可能阻塞后续 XA 事务语句的请求访问。
- 系统无法回收处于 prepared 状态的 XA 事务的 clog 日志,因此可能导致 clog 盘满,也影响系统转储。
因此,我们需要提供一种方法,能够快速结束处于非预期的 prepared 状态的 XA 事务。根据经验,通常一个 XA 事务处于 prepared 状态超过 10 分钟,可能该事务后续将无法再收到 TM 端的提交或者回滚请求。如果业务允许,我们可以采用以下 PL 块来批量回滚这些 XA 事务(需要注意的是, formatid 我们需要提前获取设置)。
set serveroutput on;
DECLARE
CURSOR cur
IS
SELECT GMT_MODIFIED,
state,
rawtohex(GTRID) gtrid,
rawtohex(BQUAL) bqual,
FORMAT_ID formatid
FROM sys.all_virtual_tenant_global_transaction_agent
WHERE state = 3
AND ROUND((sysdate - cast(GMT_MODIFIED as date)) * 86400) > 600;
l_xid DBMS_XA_XID;
l_ret PLS_INTEGER;
BEGIN
FOR rec IN cur
LOOP
l_xid.formatid := rec.formatid;
l_xid.gtrid := hextoraw(rec.gtrid);
l_xid.bqual := hextoraw(rec.bqual);
l_ret := DBMS_XA.XA_ROLLBACK(xid => l_xid);
dbms_output.put_line(l_ret);
END LOOP;
END;
/
同理,仅需要修改其中的条件,我们也可以采用该方法批量回滚处于 IDLE 状态的 XA 事务。