AI 模拟终面实战:大模型如何考查候选人对分布式事务(2PC/3PC/TCC/Saga)的理解 上周我们组在搭建内部技术面试评估 Agent 时跑了几组针对分布式系统设计的实战评测。当大模型从单纯的“八股文答题机器”转变为“具备追问能力的模拟面试官”时很多自以为准备充分的候选人往往在第二轮追问中就败下阵来。在传统的后端面试里只要问到分布式事务候选人基本能一口气背出 2PC、3PC、TCC 和 Saga 的定义Prepare、Commit、三阶段超时机制、Try-Confirm-Cancel、正向与逆向补偿。然而在真实的大厂终面中面试官从来不关心你能否背诵流程图。他们真正想看的是当极端网络抖动、节点死锁、进程异常宕机发生时你的方案能否守住数据一致性的最后一道防线。今天我们拆解一组由深度推理 Agent 主导的分布式事务终面模拟记录看看大模型是如何通过三层递进式的尖锐追问击碎浮于表面的概念背诵直击分布式系统设计最核心的状态机与容错边界。第一轮交锋2PC 与 3PC 的超时幻觉与脑裂困境对话伊始AI 面试官给出了一个经典的扣库存与扣余额跨服务场景让候选人设计分布式事务方案。候选人回答这里可以使用两阶段提交2PC。协调者首先向库存服务和账户服务发送 Prepare 消息两个服务在本地锁定资源并预执行 SQL成功后向协调者反馈 YES协调者收集到全部 YES 后再发送 Commit 广播各服务提交事务并释放资源。如果需要避免 2PC 协调者单点故障导致的阻塞可以升级为 3PC引入 CanCommit、PreCommit 和 DoCommit 三个阶段并加入超时机制。这段回答是标准教科书式的回答。但 AI 面试官立刻抓住了 3PC 最具欺骗性的痛点进行追问AI 面试官追问在 2PC 第二阶段协调者向服务 A 发送了 Commit但发往服务 B 的 Commit 因网络中断丢失同时协调者物理机突然宕机。此时服务 A 已提交服务 B 处于阻塞等待。新选出的协调者如何判定当前事务该提交还是回滚你提到 3PC 引入超时可以解决阻塞那么在进入 PreCommit 阶段后如果协调者与某个参与者之间发生网络分区脑裂超时后的参与者会默认选择 Commit 还是 Rollback这是否会导致数据发生不可逆的脑裂性不一致这个追问直接切中了 3PC 的死穴3PC 解决了阻塞问题但牺牲了一致性。在 3PC 规范中参与者一旦顺利进入 PreCommit 阶段表明第一阶段所有人都同意了。此时如果参与者等待协调者的 DoCommit 发生网络超时它的默认策略是自动提交Auto-Commit。但假设此时协调者因为发现网络异常决定终止事务并向其他能连通的节点发送了 Abort那么超时自动提交的节点就会与其他回滚的节点产生直接冲突。面对大模型的追问合格的系统设计思路必须指出在不可靠的异步网络Asynchronous Network中只要存在脑裂与超时没有任何三阶段协议能够无条件保证数据强一致。工业界弃用 3PC 的根本原因正是它用一致性被破坏的代价换取了非阻塞反而不如 2PC 配合持久化重试日志与 Paxos/Raft 选主协调器来得严密。第二轮深挖TCC 落地中的悬挂、空回滚与幂等防线当候选人意识到强一致性协议在长链路微服务中的低吞吐瓶颈将方案切换为应用层两阶段的 TCCTry-Confirm-Cancel时大模型面试官迅速切入了生产环境最常出现的异常时序AI 面试官追问在 TCC 模式下如果 RPC 调用因为网络抖动在链路上严重阻塞Try 请求尚未到达参与者上游发起方就已经超时并将该分支标记为失败随即触发了 Cancel 流程。请问Cancel 先于 Try 抵达参与者参与者本地并没有找到 Try 的预留记录此时执行 Cancel 会发生什么空回滚如果在 Cancel 响应成功之后之前阻塞在网络中的 Try 请求终于姗姗来迟并成功执行系统会发生什么灾难业务悬挂你如何用代码级别的设计彻底杜绝悬挂在生产环境下如果不针对空回滚和防悬挂设计状态机账户金额就会凭空被锁死甚至多扣。为了解决这个问题参与者必须在本地维护一张包含全局分布式事务 IDxid与分支 IDbranch_id的控制表利用数据库的原子事务将业务状态与 TCC 状态捆绑import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class TccAccountParticipant { public enum TccState { TRYING, CONFIRMED, CANCELLED } /** * Try 阶段扣减可用余额增加冻结余额 */ public boolean prepare(Connection conn, String xid, String branchId, String userId, long amount) throws SQLException { // 1. 防悬挂检查如果该分支已经存在 CANCELLED 记录说明 Cancel 先到了绝对不能执行 Try String checkSql SELECT state FROM tcc_transaction_log WHERE xid ? AND branch_id ?; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setString(1, xid); ps.setString(2, branchId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 已经存在记录不管是 CANCELLED 还是其他状态均拒绝后续的 Try return false; } } } // 2. 插入处于 TRYING 状态的分支日志 String logSql INSERT INTO tcc_transaction_log(xid, branch_id, user_id, amount, state, create_time) VALUES (?, ?, ?, ?, TRYING, NOW()); try (PreparedStatement ps conn.prepareStatement(logSql)) { ps.setString(1, xid); ps.setString(2, branchId); ps.setString(3, userId); ps.setLong(4, amount); ps.executeUpdate(); } // 3. 执行真正的业务冻结扣减可用余额增加冻结字段 String updateSql UPDATE user_account SET balance balance - ?, frozen frozen ? WHERE user_id ? AND balance ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setLong(1, amount); ps.setLong(2, amount); ps.setString(3, userId); ps.setLong(4, amount); return ps.executeUpdate() 0; } } /** * Cancel 阶段解冻资源并回滚 */ public boolean rollback(Connection conn, String xid, String branchId, String userId, long amount) throws SQLException { // 1. 查询分支状态 String selectSql SELECT state FROM tcc_transaction_log WHERE xid ? AND branch_id ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(selectSql)) { ps.setString(1, xid); ps.setString(2, branchId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { // 空回滚场景Try 根本没执行过Cancel 先到了。 // 关键防御必须插入一条状态为 CANCELLED 的占位记录防止迟到的 Try 执行导致“业务悬挂” String insertCancel INSERT INTO tcc_transaction_log(xid, branch_id, user_id, amount, state, create_time) VALUES (?, ?, ?, ?, CANCELLED, NOW()); try (PreparedStatement insertPs conn.prepareStatement(insertCancel)) { insertPs.setString(1, xid); insertPs.setString(2, branchId); insertPs.setString(3, userId); insertPs.setLong(4, amount); insertPs.executeUpdate(); } return true; } String state rs.getString(state); if (TccState.CANCELLED.name().equals(state)) { // 幂等处理已经回滚过了直接返回成功 return true; } if (TccState.CONFIRMED.name().equals(state)) { // 异常时序已经提交了却尝试回滚抛出报警由人工介入 throw new IllegalStateException(Cannot rollback confirmed branch: branchId); } // 2. 正常回滚原路释放冻结金额 String refundSql UPDATE user_account SET balance balance ?, frozen frozen - ? WHERE user_id ? AND frozen ?; try (PreparedStatement refundPs conn.prepareStatement(refundSql)) { refundPs.setLong(1, amount); refundPs.setLong(2, amount); refundPs.setString(3, userId); refundPs.setLong(4, amount); refundPs.executeUpdate(); } // 3. 更新日志状态为 CANCELLED String updateLog UPDATE tcc_transaction_log SET state CANCELLED WHERE xid ? AND branch_id ?; try (PreparedStatement logPs conn.prepareStatement(updateLog)) { logPs.setString(1, xid); logPs.setString(2, branchId); logPs.executeUpdate(); } return true; } } } }代码清晰展示了面对空回滚参与者不能简单返回 OK 了事而必须落下一张“墓碑记录Tombstone Record”将状态永久锁定在CANCELLED。迟到的 Try 只要发现这张墓碑便知晓上游已经将本分支弃用从而主动中断并退出避免业务悬挂。第三轮终局Saga 模式在并发下的隔离性崩塌在终面接近尾声时大模型往往会引入一个现实世界更具挑战性的长事务模式Saga。Saga 模式由于放弃了传统的两阶段锁每个本地事务都会直接修改真实数据并提交非冻结模式。AI 面试官给出了一个经典的问题陷阱AI 面试官追问Saga 模式每个本地事务即时提交没有任何行级排他锁保护。在正向事务 $T_1$ 成功修改了账户余额、但后续事务 $T_2$ 失败需要执行逆向补偿 $C_1$ 的窗口期内另一个外部并发请求读取了 $T_1$ 提交的余额并据此进行了转账或消费。此时 $C_1$ 逆向回滚如果直接按原金额反向扣减可能导致账户余额变成负数如果外部请求依赖了中间状态这就是典型的脏读。请问 Saga 在理论上失去了 ACID 的哪一个属性在工业界高并发账务或订单场景中你有哪些架构手段来弥补这一缺陷这个问题直指 Saga 的灵魂硬伤缺乏隔离性Isolation。一个优秀的架构师候选人此时不应慌乱否认 Saga而是要给出成熟的业务补偿与隔离对策语义锁Semantic Lock在业务表里增加业务状态标识例如ORDER_PENDING_PAY或ACCOUNT_SETTLING其他读操作如果感知到此标志视同资源被加锁必须走排队或降级逻辑只增不改Commutative Updates将业务动作设计为满足交换律的操作例如账本模型记录流水账流水追加而不是覆写单行汇总字段逆向补偿表现为一条负向冲账流水悲观资金预估缓冲与风控熔断在无法完全避免脏读的大促业务场景中允许短期透支但通过异步对账系统和限额兜底把数据一致性延迟推向 T1。终面复盘AI 评测技术深度时的核心打分逻辑从这套模拟面试的逻辑可以看出大模型对技术深度的考查早已越过了“概念解释”的初级层级。它遵循着一套严格的工程评估链条协议本质还原你能否跳出协议表面流程指出异步网络中分区与超时的物理限制时序交错推演你是否具备并发脑图能在心智模型中准确推导出网络乱序到达先 Cancel 后 Try、重复 Commit、网络截断时的系统状态代码防御严密度是否具备利用数据库唯一索引、行级排他锁、防悬挂墓碑记录构建闭环防御的硬核编程习惯。在准备系统设计面试时把每一个技术名词当成一个脆弱的物理系统去挑刺和推导往往比背下十篇技术博客要管用得多。