XA、JTA与2PC:资源登记、原子提交和崩溃恢复
一、学完本页要真正会什么
XA、JTA、2PC经常同时出现,但它们不在同一层:2PC是原子提交协议思想,XA规定事务管理器与资源管理器怎样交互,JTA是Java应用使用事务管理器的API,Narayana、Atomikos或应用服务器事务管理器才是具体协调实现。
学完本页,你应该能够:
- 区分2PC、XA、JTA、TM、RM、XAResource和Xid。
- 解释应用怎样把两个XA资源enlist到同一全局事务。
- 画出start、end、prepare、commit/rollback完整流程。
- 解释Prepare返回
XA_OK和XA_RDONLY的差异。 - 说明一阶段提交优化为什么只适合特定条件。
- 解释Prepared、in-doubt、恢复扫描和事务日志。
- 区分正常回滚、启发式提交/回滚和混合结果。
- 说明连接池为什么必须正确处理XA连接和事务关联。
- 解释JTA事务传播、超时和Spring代理只负责什么。
- 按全局Xid、分支Xid、TM日志和数据库XA状态排查生产故障。
二、六个概念必须分开
| 概念 | 所在层次 | 作用 |
|---|---|---|
| 2PC | 协议思想 | 先Prepare收集投票,再统一Commit或Rollback |
| XA | 资源接口规范 | 定义TM如何控制数据库等RM的分支事务 |
| JTA | Java API | Java应用获取事务、提交、回滚、挂起、恢复 |
| Transaction Manager | 协调实现 | 维护全局事务、资源列表、日志、决议和恢复 |
| Resource Manager | 资源系统 | 数据库、支持XA的消息系统等真正保存状态 |
| XAResource | 驱动/适配对象 | 暴露start、end、prepare、commit、rollback、recover等操作 |
关系:
flowchart TD
A["业务代码与Spring事务边界"] --> B["JTA API"]
B --> C["事务管理器TM"]
C --> D["XAResource A"]
C --> E["XAResource B"]
D --> F["数据库A的资源管理器"]
E --> G["数据库B的资源管理器"]
C --> H["持久化协调日志和恢复状态"]只使用@Transactional不会让普通数据源自动变成XA资源;只创建JtaTransactionManager也不会自动配置可恢复的底层TM和XA连接池。
三、Xid如何标识全局事务和分支
XA规范中的Xid概念上包含:
formatId:标识Xid格式。globalTransactionId:同一全局事务共享的全局部分。branchQualifier:区分该全局事务下不同资源分支。
全局事务 G9001
├─ 数据库A分支:G9001 + branch-A
└─ 数据库B分支:G9001 + branch-B事务管理器必须稳定生成并持久化Xid映射。应用业务订单号不是XA Xid,但生产排查应该把订单号、traceId和全局Xid关联起来。
四、资源怎样加入同一个JTA事务
抽象过程:
flowchart TD
A["应用开始JTA事务"] --> B["TM创建全局事务上下文"]
B --> C["业务第一次取得XA数据源A连接"]
C --> D["连接池/集成层将XAResource A enlist"]
D --> E["TM对A调用start分支"]
E --> F["业务执行数据库A SQL"]
F --> G["业务取得XA数据源B连接并enlist B"]
G --> H["TM对B调用start并执行SQL"]
H --> I["业务方法结束,请求全局提交"]enlistResource的本质是把资源分支加入当前全局事务。连接池需要确保:
- 返回的是能提供XAResource的XA连接。
- 当前连接与JTA事务正确关联。
- 事务结束前连接不会被错误地当普通连接复用。
- 发生挂起/恢复时关联状态正确切换。
- 进程重启后TM仍能找到同一资源并执行recover。
五、XA分支生命周期
XAResource常见操作语义:
| 操作 | 含义 |
|---|---|
start(xid, flags) | 把后续资源操作关联到该分支 |
end(xid, flags) | 结束当前资源关联,不等于提交 |
prepare(xid) | 资源持久化可提交状态并投票 |
commit(xid, onePhase) | 提交分支 |
rollback(xid) | 回滚分支 |
recover(flags) | 扫描资源中Prepared但未完成的分支 |
forget(xid) | 在规范允许的启发式结果处理后忘记记录 |
end只是告诉资源“当前执行线程不再继续在该分支上工作”,不能把它理解成事务成功。
六、两阶段提交完整正常流程
flowchart TD
A["业务请求TM提交"] --> B["TM结束各XA分支"]
B --> C["阶段一:向资源A发送Prepare"]
C --> D["资源A刷恢复信息并返回投票"]
D --> E["向资源B发送Prepare"]
E --> F["资源B刷恢复信息并返回投票"]
F --> G{"所有需要提交的资源是否成功Prepare"}
G -- "是" --> H["TM持久化全局Commit决议"]
H --> I["阶段二:向各资源发送Commit"]
I --> J["Commit失败的分支进入恢复重试"]
G -- "否" --> K["TM持久化Rollback决议"]
K --> L["通知已参与资源Rollback"]正确顺序中,TM应在向参与者宣布最终决议前把决议持久化到恢复日志。否则向A发送Commit后宕机,重启时没有证据判断原决议。
七、Prepare到底承诺什么
资源返回成功Prepare,通常意味着:
- 已将分支更新和恢复信息写入持久化日志。
- 即使数据库或应用重启,也能通过Xid找到该分支。
- 已保留完成Commit或Rollback所需的锁/事务上下文。
- 不会自行把Prepared分支当普通事务回滚。
常见投票:
| 结果 | 含义 | 二阶段 |
|---|---|---|
XA_OK | 分支有需要提交的修改且已Prepared | 等待Commit或Rollback |
XA_RDONLY | 分支没有产生需要提交的修改 | TM可不再发送二阶段Commit |
| XA异常 | 无法Prepare或协议错误 | 全局通常进入Rollback |
只读优化减少一次二阶段调用,但不能由应用凭“我觉得只读”伪造,必须由资源按实际分支状态判断。
八、一阶段提交优化是什么
若全局事务最终只有一个真正参与的XA资源,TM可能对它执行commit(xid, true),跳过Prepare,减少一轮日志和网络交互。这叫one-phase commit optimization。
前提是事务管理器能确认协议和资源条件允许。多个独立更新资源不能为了性能强行一阶段提交,否则第一个提交后第二个失败时失去原子性。
“最后一个资源优化”或Last Resource Commit Optimization允许一个非XA资源与XA资源组合,但存在更复杂的崩溃窗口和启发式结果,支持方式因TM而异。关键资金场景不能把它当成与纯XA等价,必须阅读具体TM文档并做故障注入。
九、事务管理器日志保存什么
可恢复TM至少要保存足以重建的状态:
- 全局Xid和事务状态。
- 参与的资源唯一名称。
- 每个分支Xid和Prepare结果。
- 最终Commit/Rollback决议。
- 二阶段完成情况。
- 超时、重试和启发式结果。
资源唯一名称必须稳定。应用重启后若同一个数据库XA资源换了完全不同的恢复标识,TM可能无法把日志中的分支与新实例中的XAResource对应起来。
协调日志存储也要高可用和持久化。把日志放临时容器文件系统,Pod重建后丢失,会破坏Prepared事务恢复。
十、什么是in-doubt事务
参与者已Prepared,却不知道全局最终决议,这个分支处于in-doubt。
flowchart TD
A["资源A和B都Prepared"] --> B["TM写Commit决议"]
B --> C["A收到Commit并完成"]
B --> D["TM在通知B前宕机"]
D --> E["B仍保留Prepared分支和锁"]
E --> F["TM重启读取日志并recover资源B"]
F --> G["找到B的Xid并重发Commit"]in-doubt不是“数据库坏了”,而是资源为了原子性等待最终决议。它解释了XA为什么可能阻塞:分区期间参与者宁可暂时不可用,也不能自行做出与其他参与者冲突的决定。
十一、recover扫描和崩溃恢复
TM重启后抽象恢复过程:
- 读取协调日志,找到未完成全局事务和最终决议。
- 为每个稳定资源重新获得XAResource。
- 调用
recover扫描资源中的Prepared Xid。 - 对齐TM日志和RM扫描结果。
- 已有Commit决议的分支重发Commit。
- 已有Rollback决议的分支重发Rollback。
- 记录完成状态并清理可安全删除的协调日志。
- 对“资源有Xid但TM无日志”等孤儿情况告警并按TM策略处理。
Recover调用有扫描开始/继续/结束等flags语义,具体封装由事务管理器负责。业务代码通常不直接调用,但运维必须知道恢复扫描需要数据库权限、网络、稳定资源名和正确驱动。
十二、Heuristic Outcome是什么
启发式结果表示资源没有完全遵循协调器的全局决议,而是因超时、管理员命令或资源内部策略自行决定。
| 结果概念 | 含义 |
|---|---|
| Heuristic Commit | 全局要求回滚,但某资源自行提交 |
| Heuristic Rollback | 全局要求提交,但某资源自行回滚 |
| Heuristic Mixed | 部分提交、部分回滚 |
| Heuristic Hazard | 无法确定一个或多个分支最终结果 |
这类结果已经超出自动“全有或全无”,必须:
- 保留Xid、决议、资源响应和业务流水。
- 停止无证据的自动覆盖。
- 对账确定各资源事实。
- 执行业务补偿或人工会计调整。
- 完成审计后再按TM规范forget启发式记录。
forget不是删除错误的快捷键,它用于协调器已正确处理启发式结果后的协议清理。
十三、JTA超时不等于所有数据库瞬间回滚
事务超时通常让TM将事务标记为只能回滚或进入超时处理。之后仍需结束分支并通知资源Rollback。若资源暂时不可达,回滚还要恢复重试。
业务线程收到超时异常时:
- 某些分支可能尚未执行。
- 某些分支可能已执行但未Prepare。
- 某些分支可能已Prepared。
- TM可能已持久化Rollback决议但尚未通知完。
因此要记录全局Xid并查询TM与资源状态,不能只凭HTTP超时判断所有库已回滚。
十四、Spring与JTA的真实关系
Spring的JtaTransactionManager主要把Spring事务抽象适配到底层JTA能力。完整系统还需要:
- 一个实际可恢复的JTA事务管理器。
- 每个资源对应的XADataSource/XAResource。
- 能正确enlist和回收XA连接的连接池/集成。
- 稳定资源名称和恢复配置。
- 持久化事务日志。
业务代码可以仍使用:
@Transactional(rollbackFor = Exception.class)
public void transfer(Long payerId, Long merchantId, BigDecimal amount) {
payerRepository.decrease(payerId, amount);
merchantRepository.increase(merchantId, amount);
}但这段代码是否跨库原子,不取决于注解文字,而取决于两个Repository实际使用的资源是否加入同一个JTA全局事务。若其中一个仍是普通本地DataSource,它可能独立提交。
不要用下面这种不完整示例误导生产:
// 仅new空适配器并不会自动创建可恢复TM、XA数据源和事务日志
return new JtaTransactionManager();正确配置因Spring Boot代际、TM产品、连接池和数据库而异,应使用对应官方Starter/集成文档并验证依赖矩阵。
十五、挂起、恢复和事务上下文
JTA支持事务挂起和恢复。Spring传播行为如REQUIRES_NEW可能:
- 挂起外层JTA事务和其资源关联。
- 创建新的全局事务。
- 内层事务完成后恢复外层事务。
风险:外层事务的连接和资源可能仍被占用,内层又需要新的连接。高并发下容易耗尽连接池。事务上下文通常与执行线程关联,直接切换到未受管理的线程不会自动加入原全局事务。
十六、同一资源管理器和分支合并
XAResource.isSameRM用于判断两个XAResource是否属于同一个底层资源管理器。TM可能据此优化分支关联或避免不必要的独立Prepare。
错误的驱动/包装实现可能让TM误判资源身份。生产必须使用经过兼容验证的驱动和连接池,不要自行包装XAResource而忽略isSameRM、recover和异常码语义。
十七、XA异常码为什么重要
XAException中的错误类别帮助TM判断:
- 可以重试的资源错误。
- 分支不存在或协议顺序错误。
- 资源已经回滚。
- 启发式提交/回滚/混合。
- RM故障或通信问题。
应用日志不能只打印“XA failed”。至少记录全局Xid、分支Xid、资源名、阶段、XA errorCode、数据库错误和TM状态,才能判断是否安全重试。
十八、商业场景:跨库记账
假设付款账户库和商户账户库必须原子记账:
flowchart TD
A["开始JTA事务并生成Xid"] --> B["付款库条件扣减并写流水"]
B --> C["商户库增加余额并写流水"]
C --> D["TM对两个资源Prepare"]
D --> E{"所有Prepare成功"}
E -- "是" --> F["持久化Commit决议并提交两个分支"]
E -- "否" --> G["持久化Rollback决议并回滚"]
F --> H["对账按业务流水验证双方"]即使使用XA,仍需要业务流水唯一键:客户端超时后可能重复提交同一转账;XA只保证一次全局事务内部的原子性,不自动识别两次不同Xid其实是同一业务意图。
十九、适合与不适合的场景
| 场景 | 结论 | 原因 |
|---|---|---|
| 同库多表 | 不需要XA | 本地事务更简单可靠 |
| 两个XA数据库短事务原子记账 | 可评估 | 业务不允许中间结果,资源受控 |
| 数据库与支持XA的MQ | 谨慎评估 | 两边驱动和恢复必须真实支持 |
| 秒杀热点库存 | 通常不适合 | Prepare持锁和热点冲突 |
| 人工审批、物流长流程 | 不适合 | 不能长时间保留Prepared资源 |
| 外部支付与短信 | 无法直接纯XA | 外部资源通常不暴露XAResource |
| 通知、积分、ES同步 | 优先消息最终一致 | 无需阻塞主交易资源 |
二十、容量和性能成本怎样评估
压测至少观察:
- 全局事务TPS。
- Prepare、Commit、Rollback的P95/P99。
- 数据库行锁等待和死锁。
- Prepared/in-doubt数量和最长年龄。
- TM日志写延迟。
- XA连接池使用率和等待时间。
- 参与资源数量与失败率。
- 故障恢复时长。
参与者越多,全局成功率和尾延迟通常越受最慢资源影响。不要只测正常提交,还要断开TM、重启数据库、丢弃Commit响应并验证恢复。
二十一、生产排查Runbook
21.1 @Transactional没有跨库回滚
- 确认实际选中的PlatformTransactionManager是否为JTA适配器。
- 确认底层TM是否真正启动并持久化日志。
- 确认两个DataSource都是XADataSource或正确XA包装。
- 确认两个资源都被enlist到同一全局Xid。
- 检查是否手工获取普通连接绕过JTA。
- 检查Spring代理、自调用、异常回滚规则和跨线程。
21.2 数据库存在Prepared/in-doubt事务
flowchart TD
A["从数据库列出Prepared Xid"] --> B["从TM日志查同一Xid和最终决议"]
B --> C{"TM是否有可靠Commit/Rollback决议"}
C -- "有" --> D["恢复TM资源连接并按决议重放"]
C -- "没有" --> E["保留现场,核对所有参与者和业务流水"]
E --> F["按TM/数据库官方流程人工决策并审计"]不能看到锁等待就直接在数据库中Commit Prepared。另一个资源可能已回滚,草率提交会制造资金不一致。
21.3 TM重启后无法恢复
检查:
- 协调日志是否在持久卷/可靠数据库中。
- 资源唯一名称是否变化。
- XA驱动和认证是否可用。
- recover权限和网络。
- Xid序列化是否兼容升级前版本。
- 数据库中Prepared分支与TM日志是否能对应。
21.4 提交很慢
拆分测量:业务SQL、Prepare、TM日志、Commit、连接池等待、锁等待和网络。只增加全局超时可能让锁保留更久。先缩短事务、移出远程慢调用、减少参与者并解决热点。
21.5 出现启发式结果
冻结自动“重试到成功”的脚本,导出TM日志、资源分支事实和业务流水,逐资源确认结果,执行受审计的业务补偿;处理完成后再按TM规范清理启发式记录。
二十二、JDK 8 Demo:持久化决议为什么不能改变
import java.util.LinkedHashMap;
import java.util.Map;
public class TwoPhaseDecisionDemo {
enum Decision { NONE, COMMIT, ROLLBACK }
static final class Coordinator {
private Decision durableDecision = Decision.NONE;
private final Map<String, Boolean> completed =
new LinkedHashMap<String, Boolean>();
void decideCommit(String... branches) {
if (durableDecision != Decision.NONE) {
throw new IllegalStateException("decision already durable");
}
durableDecision = Decision.COMMIT;
for (String branch : branches) {
completed.put(branch, Boolean.FALSE);
}
}
void acknowledge(String branch) {
if (durableDecision != Decision.COMMIT) {
throw new IllegalStateException("no commit decision");
}
completed.put(branch, Boolean.TRUE);
}
void recover() {
for (Map.Entry<String, Boolean> entry : completed.entrySet()) {
if (!entry.getValue().booleanValue()) {
System.out.println("replay COMMIT to " + entry.getKey());
entry.setValue(Boolean.TRUE);
}
}
}
}
public static void main(String[] args) {
Coordinator coordinator = new Coordinator();
coordinator.decideCommit("db-a", "db-b");
coordinator.acknowledge("db-a");
coordinator.recover();
System.out.println("decision=" + coordinator.durableDecision);
}
}输出:
replay COMMIT to db-b
decision=COMMIT真实TM必须把决议和分支完成状态写入可靠存储;Demo内存字段只解释恢复规则。
二十三、常见错误与后果
| 错误 | 后果 | 正确方向 |
|---|---|---|
| 只new JtaTransactionManager | 没有真实TM、XA资源和恢复日志 | 使用产品集成并验证enlist/recover |
| 一个XA库加一个普通库 | 普通库无法参与Prepare | 全部资源受协议管理或改用补偿方案 |
| XA事务内调用慢HTTP | Prepared和连接锁长期占用 | 保持纯资源短事务,外部调用拆出 |
| TM日志放容器临时盘 | Pod重建后无法恢复决议 | 持久化可靠日志和稳定资源名 |
| 业务超时就当所有库回滚 | 可能已有Prepared或Commit决议 | 按Xid查询TM和RM事实 |
| 手工提交Prepared解除锁 | 可能违背全局决议 | 对齐TM日志后按官方恢复流程 |
| XA替代业务幂等 | 两个不同Xid仍可重复转账 | 业务流水唯一键和结果查询 |
| 不做故障注入 | 正常测试无法证明可恢复 | 测TM/DB重启、网络丢包和响应丢失 |
二十四、面试标准回答
24.1 XA、JTA和2PC有什么关系
2PC是先Prepare再统一Commit/Rollback的原子提交协议;XA定义事务管理器与数据库等资源管理器交互的接口,包括start、end、prepare、commit、rollback和recover;JTA是Java应用使用事务管理器的标准API。具体TM负责生成Xid、登记XAResource、持久化决议和崩溃恢复。
24.2 Prepare后为什么会阻塞
参与者返回Prepared意味着已经持久化可恢复状态并保留完成二阶段所需资源,不能自行改变结果。若协调器暂时不可达,它处于in-doubt,相关锁或事务上下文可能继续保留,直到TM按持久化决议恢复。
24.3 XA为什么不能替代幂等
XA保证一个全局Xid内部多个资源原子提交,但客户端超时后若用新Xid重新发起相同业务,两个全局事务都可能成功。仍要用订单号、支付流水等稳定业务键和唯一约束识别同一业务意图。
24.4 启发式结果是什么
资源在没有遵循全局决议的情况下自行提交或回滚,可能形成Heuristic Commit、Rollback、Mixed或Hazard。它意味着自动原子性已破坏,必须保留事务日志、逐资源对账并执行业务补偿,不能简单forget或删除记录。
二十五、关联知识点
本章小结
XA的价值不只是两阶段流程图,而是标准化资源分支、持久化投票、全局决议和崩溃恢复。JTA负责Java调用边界,具体TM和XA资源决定系统能否真正恢复。理解in-doubt、recover和启发式结果,才算理解XA生产语义。
