客户的财务系统用了 DB Link 做跨库结算,某天晚上结算跑批到一半,协调器实例(Instance A)突然重启。第二天早上一看,Instance B 的 DBA_2PC_PENDING 表里躺了 217 条 in-doubt 分布式事务,而且很多表被 ORA-01591 锁住,新事务进不来。
1 两阶段提交的坑:Prepare 之后 Commit 之前
分布式事务的标准流程是:第一阶段 Prepare:协调器问所有参与者(Participant)"你们能提交吗?",参与者把本地事务状态改成 prepared,写 redo,然后回复 YES。第二阶段 Commit:协调器收到所有 YES 后,写 commit 记录,然后通知所有参与者正式提交。故障就发生在第二阶段中间:协调器已经写了 commit 记录,但还没通知完所有参与者,就重启了。参与者 B 一直没收到 commit 通知,它的本地事务就卡在 in-doubt 状态,持有的锁也一直不释放。这就是 ORA-01591 的根源。
Oracle 的 RECO 进程负责自动恢复 in-doubt 事务,默认每隔 10 秒扫描一次 DBA_2PC_PENDING。但 RECO 的恢复逻辑是:先联系协调器,问这个事务到底提交还是回滚。如果协调器也重启了,RECO 需要等协调器的 SMON 把事务状态恢复完才能回答。我们那次协调器重启后,SMON 忙于其他恢复,RECO 问了半小时都没得到答复,217 条 in-doubt 事务就挂了半小时。
2 实验:手动制造 in-doubt 事务
-- 协调器实例 A
UPDATE accounts@db_link_b SET balance = balance - 100 WHERE acc_id = 1;
UPDATE accounts@db_link_c SET balance = balance + 100 WHERE acc_id = 2;
-- 在 COMMIT 之前,kill 协调器实例 A 的 SMON 或者整个实例
-- 参与者实例 B
SELECT * FROM DBA_2PC_PENDING;
-- LOCAL_TRAN_ID STATE MIXED
-- 1.2.3 prepared no
-- 1.2.4 committed no -- 这个已经收到 commit 通知
-- 1.2.5 prepared no -- 卡住的
-- 查看被锁住的资源
SELECT session_id, lock_type, mode_held, mode_requested
FROM DBA_DDL_LOCKS WHERE name LIKE '%ACCOUNTS%';
-- 发现 accounts 表被 TM 锁以 Row-X 模式持有
-- 手动干预(如果确认应该提交)
COMMIT FORCE '1.2.5';
-- 或者回滚
ROLLBACK FORCE '1.2.5';
-- 清理 DBA_2PC_PENDING(必须在所有参与者都 force 之后)
EXECUTE DBMS_TRANSACTION.PURGE_LOST_DB_ENTRY('1.2.5');
我们在测试环境复现了三次这个故障,发现 RECO 的恢复时间跟 in-doubt 事务数量成正比:10 条以下 2 分钟恢复,50 条 8 分钟,200 条以上超过 30 分钟。原因是 RECO 是单进程串行扫描,每条事务都要联系协调器,网络 RTT 累积起来很可观。
给客户的建议:第一,分布式事务的结算跑批,协调器实例必须做 RAC 或者至少有个物理备库快速切换,别让单点故障拖垮整个结算;第二,跑批脚本里加异常处理,如果 COMMIT 阶段出现异常,立即记录所有 DB Link 的事务 ID,方便 DBA 第二天手动 force;第三,把分布式事务改成消息队列(比如 Kafka + 本地事务),虽然架构复杂点,但彻底规避 2PC 的协调器单点问题。说实话,现在新系统还坚持用 DB Link 做分布式事务的,都是在给自己埋雷。