2.5 Java帝国之宫廷内斗
把跨资源事务拆成 JDBC 参与者、JTA 协调、prepare 投票、全局决定和恢复,理解原子性与可用性的代价。
学习目标
- 能沿 JTA 事务、协调者、JDBC 参与者和两个资源追踪 prepare/commit/rollback 状态
- 能解释两阶段提交如何换取全局原子决定,以及为什么会引入锁、阻塞和恢复日志
- 能用正常、边界和单一故障样本定位投票拒绝、协调者失联、重试重复和基本可用降级
2.5 Java帝国之宫廷内斗
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.5 Java帝国之宫廷内斗。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
跨两个数据库、消息资源或其他可参与资源写入时,各自本地提交不足以证明全局原子性。JTA 为应用提供全局事务边界,协调者先让每个 JDBC 参与者执行并准备,再根据所有投票广播 commit 或 rollback。它减少“半边成功”的概率,却要求参与者在不确定时保留锁和 prepared 状态,因而增加阻塞、超时和恢复复杂度。
三个会让全局事务失真的陷阱
十个目录节点到协议证据
JDBC大臣
↡代表登记在全局事务中的 JDBC 连接与本地数据库资源,负责执行 SQL、准备、锁定和报告投票。不是一个会自动服从命令的对象。参与者必须能保存 prepare 状态、响应 commit/rollback、报告失败,并把本地日志和锁纳入恢复。连接池、自动提交和隔离级别会影响它能否诚实投票。
密谋
↡协调者与参与者在第一阶段交换本地可提交条件、写入预备日志并等待全局决定的协议阶段。可理解为 prepare 阶段的内部协商。参与者要检查约束、锁和日志;返回 yes 代表“我可以按协议提交”,不是“业务已经完成”。任何一个 no 都足以让协调者选择回滚。
两阶段提交
↡先收集所有参与者 prepare 投票,再广播统一 commit 或 rollback 决定的分布式原子提交协议。的第一阶段保证协调者知道谁能提交,第二阶段传播唯一决定。它不能消灭协调者、网络或参与者故障;prepare 后失联会留下 in-doubt 与阻塞窗口,因此必须有超时、日志和恢复协议。
JTA
↡Java 应用通过统一事务接口声明全局事务、登记资源、提交或回滚的协调抽象。把应用和具体事务管理器解耦,但事务管理器仍需知道资源、超时、传播和恢复。JTA 的提交结果不覆盖未登记的外部服务,也不自动让本地非事务操作具备原子性。
塞翁失马,焉知非福
↡一次资源或性能损失可能暴露更深的边界问题,要求把失败样本转化为协议与恢复证据。不是为故障找乐观解释,而是提醒保留失败轨迹:某次超时也许揭示锁没有释放,某次回滚也许揭示事件发送早于提交。修复要让同一故障重放时得到更清晰的拒绝或恢复。
基本可用
↡在全局决定未知、资源部分不可达或一致性代价过高时,明确允许哪些只读、排队、隔离或降级行为。不是无条件放行。设计时先声明业务可接受的降级:读旧快照、排队等待、拒绝写入或进入补偿;每种选择都要标注可能的陈旧、重复或延迟,并能在恢复后对账。
走漏风声
↡协调决定、参与者日志或外部事件在错误时机泄露,导致未提交状态被下游当成最终事实。提示观察通知顺序。提交事件应在最终决定可确认后发送,或携带幂等键与可追踪状态;prepare、锁等待和本地中间值不应冒充业务成功。
宫廷激辩
↡围绕原子性、可用性、延迟、锁和补偿的工程取舍进行可验证讨论,而不是用单一指标决定协议。要求把争论变成场景表:哪些资源必须原子,哪些可以最终一致,协调者失联时是否阻塞,业务能接受多长恢复时间。没有这些输入,“选择 2PC”或“拒绝 2PC”都只是口号。
2.5 Java帝国之宫廷内斗
这场“内斗”的工程事实是多个事务参与者共享一个全局决定,却各自拥有本地锁、日志和失败模式。2.5 Java帝国之宫廷内斗不是在赞美或否定 JTA,而是训练读者沿 prepare、决定、释放和恢复验证原子性,以及诚实记录可用性代价。
最小两阶段提交合同
enum Vote { YES, NO }
enum Decision { COMMIT, ROLLBACK, IN_DOUBT }
record Participant(String id, Vote vote, boolean prepared, boolean released) {}
static Decision decide(List<Participant> participants, boolean coordinatorAlive) {
if (!coordinatorAlive) return Decision.IN_DOUBT;
return participants.stream().allMatch(p -> p.vote() == Vote.YES && p.prepared())
? Decision.COMMIT
: Decision.ROLLBACK;
}合同把 prepared、最终 Decision 和 released 分开。协调者存活且所有参与者都准备好,才可广播 commit;任何 no 选择 rollback;协调者失联时只能记录 in-doubt,不能凭超时猜测成功。外部副作用还需要独立的幂等或补偿设计。
四步复核一个全局事务
1. 开启 JTA 事务并登记参与者
列出事务边界、JDBC 连接、资源身份、超时和传播方式。正常样本登记两个参与者;边界样本缺少一个资源或混入未登记副作用,观察合同如何拒绝。
Lab
两阶段提交决策台
只改变一个参与者或协调者条件,观察 commit、rollback 和 in-doubt 的差别。
两个参与者都 prepare=yes
A yes + B yes → decision commit → acknowledgements → locks released
判定
commit:广播后幂等确认
当前样本:全票提交;保存全局事务 ID、投票、决定、锁和恢复日志。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 所有参与者 prepare=yes,协调者可达 | 统一 commit 后释放资源 | 投票、决定日志、确认、锁释放 |
| 边界 | 一个参与者 no、超时或重复决定 | 全局 rollback 或幂等确认 | 参与者状态、超时、重试与版本 |
| 单一故障 | prepare 后协调者失联 | in-doubt 可恢复,不猜测 commit | 日志、锁、恢复来源、降级策略 |
故障诊断:先确认决定是否存在
- 登记层:确认 JTA 事务是否包含所有需要原子的资源,外部邮件/HTTP/缓存是否另有合同。
- 投票层:查看每个参与者的本地约束、日志、锁和 prepare 结果;一个 no 不应被转换为 yes。
- 决定层:查协调者的持久化决定和广播确认;区分 rollback、commit 与 in-doubt,不能以连接超时推断结果。
- 恢复层:根据日志恢复参与者,释放锁并执行幂等补偿;基本可用策略必须保留一致性和陈旧性说明。
如果数据库都已提交但消息未发出,检查 outbox;如果一个参与者持锁等待,先查协调者决定日志;如果重复提交,检查决定幂等和客户端重试键。每个故障都要能从唯一业务身份重放。
最小全局事务证据包与反例
证据包包含全局事务 ID、JTA 传播、参与者列表、连接/资源身份、SQL、投票、prepare 日志、协调决定、广播确认、锁、超时、重试、外部副作用和恢复结果。敏感数据可脱敏,但不能删除决定链和参与者身份。
反例一是参与者全部 prepare 后协调者失联,系统为了“基本可用”直接放行同一资源的下一次写入;反例二是数据库回滚但邮件已发送。保留未知状态,再用决定日志、outbox 或补偿重放,证明修复没有把未知变成猜测。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- JDBC大臣
登记在全局事务中的 JDBC 参与者,负责本地 SQL、prepare、决定确认和资源释放。
- 密谋
参与者检查本地条件并写预备日志、返回投票的 prepare 阶段。
- 两阶段提交
先收集 prepare 投票,再广播统一 commit 或 rollback 的协议。
- JTA
Java 应用声明全局事务、登记资源并交由事务管理器协调的抽象。
- 塞翁失马,焉知非福
将失败样本转化为协议、恢复和可重放证据的复盘视角。
- 基本可用
在决定未知或资源不可达时明确允许的只读、排队、隔离或补偿策略。
- 走漏风声
未提交状态或中间决定过早泄露,被下游误当成最终业务事实。
- 宫廷激辩
围绕原子性、可用性、延迟、锁和补偿做场景化工程取舍。
练习
练习
问题 1: 两个参与者都返回 prepare=yes,但协调者在广播决定前宕机。系统为什么不能直接当作已提交?
问题 2: JTA 事务回滚了数据库,但邮件已经发出。JTA 哪里没有覆盖?
问题 3: 为了保持基本可用,协调者失联时允许读写同一订单。怎样证明这不是静默破坏一致性?
本页小结
2.5 Java帝国之宫廷内斗的关键不是把 JTA 当成魔法,而是能沿参与者投票、协调决定、锁、日志和恢复区分 commit、rollback 与 in-doubt。完成标准是知道两阶段提交换来的原子性代价,并为基本可用和未登记副作用设计明确的降级、幂等或补偿合同。