面试后端岗位这些年,我发现一个规律:候选人简历上写“精通微服务”的不少,但能把分布式事务讲透的,十个人里最多一两个。分布式事务相关的问题几乎必考,且面试官不会只满足于“2PC、TCC、Saga”这几个名词。最近我用 DeepSeek 把近三年各家后端面经里的分布式事务真题重新洗了一遍,照着高频追问和边界条件来回过滤,整理出一份 100 道分布式事务面试题及答案。
这篇博文不做纯背诵手册,而是站在“面试官想听什么”的角度,把 100 道题分成六组:概念地基、经典协议、消息可靠性、工程实战、工具源码、高阶复盘。每道题我都标注了考察点、易错点,以及被追问时怎么接招。这篇文章适合准备大厂后端面试的同学,也适合想系统自查分布式事务知识体系的人。第一题基本是送分题,但别掉以轻心,真正的关卡从“为什么”开始。
1. 为什么分布式事务是面试中的“分水岭”
1.1 面试官到底在考什么
很多人以为分布式事务面试题考的是“背方案”。实际上,面试官真正想摸清的是三件事:你有没有踩过真实的数据不一致的坑、你知不知道每个方案要付什么代价、你能不能把方案落到具体业务场景里。这三个能力分别对应“概念理解”“方案权衡”“工程落地”。只背概念而不会权衡的人,一般会在追问到“那你们项目为什么不用 Seata”时当场卡住。
分布式事务的本质问题,是让多个独立数据源之间保持逻辑上的原子性。单机数据库里一条事务能搞定的事,拆成多个服务之后,就不能靠数据库自带的回滚来兜底了。面试官最喜欢用一个具体场景开场:“用户下单买了商品,你扣了库存但订单没创建成功,怎么办?”这个问题的背后,就是对“跨服务状态一致性”的理解。
1.2 100 道题怎么分布
为了方便大家按模块复习,我把 100 道题做了明确分组。第 1 到 15 题是基础概念,第 16 到 35 题是 2PC、3PC、TCC、Saga 等经典协议,第 36 到 50 题是本地消息表、事务消息、幂等重试,第 51 到 70 题是订单扣库存、转账、全局事务 ID 等工程实战,第 71 到 80 题是手写题与中间件选型,第 81 到 100 题是面试话术和进阶复盘。每个分组内部由浅入深,建议按顺序看,别直接跳到后面。
第 1 题:什么是分布式事务?
分布式事务指一次业务操作跨越多个独立服务或数据库,而这些参与者之间需要保持数据一致性的事务。比如转账操作,A 账户在银行服务一扣款,B 账户在银行服务二加款,两个服务必须同时成功或同时失败。单机事务用 undo log 和锁就能保证原子性,分布式事务则需要通过协调机制去协调每个参与者。
第 2 题:为什么单库事务不足以解决微服务问题?
因为微服务架构中,数据通常被拆分到不同的数据库或不同的服务内部。一个接口调用链可能涉及订单库、库存库、积分库,每个库各自为政。单库事务只能保证一个库内部的一致性,无法跨库加锁和回滚。即使物理上合并成一个库,耦合也会急剧上升,违背了服务的独立性。
2.1 高频题:分布式事务、CAP、BASE
第 3 题:什么是 ACID?分布式事务中还讲 ACID 吗?
ACID 是数据库单机事务的四个特性:原子性、一致性、隔离性、持久性。分布式事务没法完整保证这四个特性,尤其是原子性和隔离性。绝大多数分布式事务方案追求的其实是“最终一致性”,即在一段时间内允许中间状态不一致,但最终会收敛到一致状态。面试时如果说“我们用了分布式事务,所以还是 ACID”,这是很容易被反驳的。
第 4 题:什么是 CAP 定理?
CAP 指一致性、可用性、分区容错性。三者不能同时完美满足,网络分区是必然存在的,所以在分区发生时必须在“一致性”和“可用性”之间取舍。常见的理解是:在网络抖动或节点故障时,要么选择 CP,保证访问到的数据一致但可能拒绝请求;要么选择 AP,保证请求可用但可能读到旧数据。
第 5 题:为什么 CAP 只能三选二?
CAP 的思想不是平时“三个里面随便选两个”,而是说在网络不发生分区时,C 和 A 可以同时满足;一旦发生分区,分布式系统必须在 C 和 A 之间做抉择。因为网络分区意味着同一个数据在不同节点上暂时通信不了,你没法既保证两个节点数据绝对一致,又保证两边都能正常响应外部请求。典型例子:一个节点说交易成功,另一个节点无法同步,你只能要么等着同步成功(牺牲可用性),要么先返回成功但容忍短暂不一致(牺牲强一致性)。
第 6 题:什么是 BASE 理论?
BASE 是 Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致)的缩写。它是对 CAP 中 AP 模式的延伸,允许系统在部分失败时保持可用,数据在后台异步同步,最终达到一致。几乎所有的柔性事务方案,如 TCC、Saga、事务消息,本质上都是 BASE 思想的实现。
第 7 题:强一致性和最终一致性的区别?
强一致性要求操作完成后,任何一个节点读到的数据都是最新值;最终一致性只要求经过一段时间后,数据在各节点间最终一致。面试时可以把“取外卖”类比:强一致性是你下单成功后立刻看到订单已创建;最终一致性是你下单后可能短暂看到“处理中”,但过一会再看一定变成“已支付”。重点不在于“最终”两个字,而在于中间窗口的容忍度有多高。
2.2 边界题:事务边界与分库分表
第 8 题:什么是刚性事务与柔性事务?
刚性事务指强一致事务,典型代表是单库 ACID 和 2PC/XA 方案。柔性事务指允许中间状态、通过补偿或异步手段达到最终一致,典型代表是 TCC、Saga、本地消息表。面试官想听的是:你清楚刚性事务的代价是性能和可用性牺牲,柔性事务的代价是编程复杂度和一致性窗口。
第 9 题:分布式事务里的“一致性”和并发隔离的“一致性”有何不同?
并发一致性更多指多并发读写下的隔离级别,比如脏读、不可重复读、幻读;分布式事务的一致性往往指多个服务间数据状态的最终相同。两者都来源于“一致性”这个词,但面试官常设陷阱:你把“隔离性”问题当成“分布式事务”问题来答,就会被带偏。比如库存超卖,本质是并发控制和分布式事务叠加的问题,需要同时考虑锁和事务边界。
第 10 题:什么是事务边界?怎么划分?
事务边界指一组操作组成的原子单位。在分布式系统里,边界不能随随便便包住整个调用链,否则锁范围大、性能差。建议的划分方式是:聚合根内部强一致,聚合根之间最终一致。比如订单聚合可以自己管理订单状态,库存聚合管理库存数量,两者之间用事务消息或补偿流程连接。
第 11 题:什么是本地事务?
本地事务就是单个数据库资源上的事务,由数据库自身管理。通常用 @Transactional 或 JDBC 事务来申明。本地事务会出现跨服务时无能为力。面试中常考的挖坑点是:一个服务内调用了两个数据库操作,但第二个失败后,第一个到底回不回滚?如果第一个操作已经提交到另一个服务,是不会自动回滚的。
第 12 题:什么是全局事务?
全局事务是跨多个资源管理器的事务,通常需要一个事务管理器来统一协调。常见的全局事务协议是 XA。全局事务能提供更强的原子性,但代价是吞吐量低、链路阻塞风险高。面试时可以用“会议室预订”类比:全局事务像会议主持人,每个参会者都要先确认能来,主持人最后拍板“开会”。
第 13 题:什么是 XA 事务?JTA 是什么?
XA 是 X/Open 组织定义的分布式事务规范,定义了事务管理器 TM 和资源管理器 RM 之间的接口。JTA 是 Java 平台上的 XA 事务标准 API。Spring 里用 @Transactional 配合 JtaTransactionManager,可以管理多个支持 XA 的数据源。实际开发中,XA 对数据库和中间件要求高,性能开销也大,微服务场景已经很少直接用。
第 14 题:分库分表之后,本地事务为什么不可用?
分库分表后,一张订单表可能分布到 16 个库、128 张表里。单库的 undo log 和行锁只能约束其中一个分片,没法跨库回滚。此时要么引入分布式事务中间件,要么重构业务,让一个操作尽量落在同一个分片上,比如按用户 ID 路由,让一个用户的订单和支付在同一个库里。
第 15 题:是不是每个微服务调用都要上分布式事务?
不是。分布式事务有成本,能不用就不用。判断标准是看数据不一致的影响:如果写挂了一条缓存可以重放,那就用异步补偿;如果扣了用户钱但订单没生成,这是资金类核心链路,才必须强一致或高可靠最终一致。面试时如果能主动说出“我先看业务能不能拆”,比上来就堆方案要好得多。
3. 从 2PC 到 Saga,协议背后的演进逻辑(16~35 题)
3.1 2PC 和 3PC:强一致方案的得意与失意
第 16 题:2PC 的完整流程是什么?
两阶段提交分为准备阶段和提交阶段。准备阶段,事务协调者向所有参与者发送 prepare 请求,参与者执行本地事务但先不提交,返回“可以提交”或“不可以提交”。如果全部返回成功,协调者再发 commit,参与者正式提交;如果任意一个失败,协调者发 rollback,参与者回滚。
第 17 题:2PC 为什么是“强一致”?
因为 2PC 在准备阶段用全局锁把所有参与者的资源锁定住,其他事务无法修改这些数据,直到最终提交或回滚。从外部看,分布式事务的最终结果就是全部成功或全部失败,没有中间状态。
第 18 题:2PC 存在哪些问题?
最大的问题是同步阻塞:准备阶段资源被锁住,如果协调者宕机,参与者会一直等待。其次是协调者单点故障:协调者本身没有高可用的话,整个事务卡死。还有一个更隐蔽的问题——脑裂:协调者发送 commit 后宕机,部分参与者收到并提交,另一部分没收到,最终数据不一致。所以 2PC 只能保证正常情况下的强一致,无法保证故障情况下的绝对一致。
第 19 题:3PC 的三段流程是什么?
3PC 引入超时机制,并把 2PC 的准备阶段拆成 CanCommit 和 PreCommit 两段。具体是:先询问所有参与者能否提交,能则进入预提交阶段;预提交阶段参与者执行事务但不提交,同时等待协调者确认;最后协调者发 DoCommit 提交。任何阶段参与者超时未收到指令,会自动提交或回滚,从而减少阻塞。
第 20 题:3PC 相比 2PC 改进了什么?
3PC 降低了协调者故障导致的长时间阻塞风险。参与者在 PreCommit 之后如果超时未收到 DoCommit,会自行提交,因为之前先收到过 CanCommit 的确认。这种机制让参与者在异常情况下依然能往前走,而不是无限等待。
第 21 题:为什么 3PC 也不能保证绝对一致?
3PC 的容错建立在“PreCommit 后默认提交”的假设上。但如果协调者发送 PreCommit 后宕机,部分参与者提交,另一部分在超时前还没提交,甚至有个别参与者之前 CanCommit 失败,集群依然可能出现不一致。所以 3PC 只是改善了可用性,没有真正解决分布式一致性的终极问题。
第 22 题:什么是补偿事务?
补偿事务是为已成功执行的本地操作提供一个反向操作,当整个业务链路失败时,把这些本地操作还原到业务初始状态。比如调用库存服务扣减 10 件成功后,发现订单服务异常,就调用库存服务的“回加 10 件”接口来实现补偿。
第 23 题:补偿事务和回滚是一回事吗?
不是一回事。回滚是数据库层面的 undo,不加业务规则;补偿是业务层面的反向操作,要明确知道原来做了什么、如何反向执行。补偿操作本身也可能失败,所以补偿流程必须具备重试和幂等能力。这是面试官特别爱追问的点:补偿接口写得对不对,取决于你有没有处理重试。
3.2 TCC:空回滚、幂等、悬挂三道送命题
第 24 题:什么是 Saga 模式?
Saga 是一种长事务解决方案,把一个分布式事务拆成多个本地事务,每个本地事务完成后发布事件或调用下一个服务;一旦某个步骤失败,就反向调用前面已完成步骤的补偿操作。Saga 没有全局锁,事务之间可以并发,因此性能比 2PC 好,但一致性窗口更大。
第 25 题:编排式 Saga 和协同式 Saga 的区别?
编排式 Saga 由一个中央流程引擎控制每一步的执行和补偿,好处是流程集中、容易监控;协同式 Saga 没有中央协调者,由各个服务通过事件驱动自动调用下一步,好处是去中心化,缺点是流程散落各处,排查链路困难。面试时最好结合自己的项目说明用了哪种。
第 26 题:Saga 怎么正确回滚?
Saga 回滚不是直接抛异常,而是要执行每个已成功步骤对应的 Compensating Action。这些补偿动作必须是有序的倒序执行,而且每一步补偿也要考虑幂等。比如订单服务先扣库存、后创建订单,创建失败时,需要先取消订单(如果有),再回补库存。顺序错了,可能把用户刚下的另一单也覆盖掉。
第 27 题:Saga 没有隔离性,怎么补救?
Saga 的本地事务之间没有全局锁,一个事务还没提交,另一个事务就可能读到中间数据。补救手段包括:语义锁(给数据加一个业务状态,比如“库存锁定中”)、隔离表(把未提交的中间状态放到临时表)、串行化访问(对同一聚合根加分布式锁)。面试时说“Saga 无隔离所以简单”会减分,说“通过业务状态控制并发”才加分。
第 28 题:什么是 TCC?
TCC 把一个分布式事务拆成 Try、Confirm、Cancel 三个阶段。Try 阶段做资源预留和业务检查,比如扣减库存时先把货“锁定”而不是真正扣减;Confirm 阶段提交业务操作,比如把锁定的库存真正扣掉;Cancel 阶段释放预留,比如把锁定的库存释放回来。
第 29 题:TCC 三个阶段各自的职责?
Try:检查业务数据是否满足条件,并预留资源、冻结资金、锁定库存。Confirm:确认执行真正的业务操作,Confirm 一般不再做业务校验,直接把 Try 阶段预留的数据变成实际数据。Cancel:把 Try 预留的资源释放回滚,恢复初始状态。三个阶段的实现都要幂等,Confirm 和 Cancel 都可能因为重试而执行多次。
第 30 题:TCC 和 2PC 有什么关系?
TCC 可以看成业务层面的 2PC。2PC 的 prepare 对应 TCC 的 Try,commit 对应 Confirm,rollback 对应 Cancel。区别在于:2PC 是资源管理器层面的通用事务,TCC 是业务层面显式的三个接口。TCC 更灵活,可以控制锁粒度,但也要求每个参与方都实现 Confirm 和 Cancel。
第 31 题:什么是空回滚?
空回滚指 Cancel 执行时,对应的 Try 根本没执行成功或还没执行到,Cancel 却把不存在的资源“回滚”了一遍。比如库存锁定接口超时返回失败,主流程走了 Cancel,可库存实际上根本没被锁过。如果不做控制,Cancel 就会反复释放不存在的锁,甚至影响其他正常操作。
第 32 题:什么是幂等?
幂等指同一个操作执行多次和执行一次结果相同。放在 TCC 里,Confirm 或 Cancel 由于网络重试可能执行两次,必须保证第二次执行不会产生额外影响。实现幂等常用方法有:事务状态表、唯一索引、去重表。
第 33 题:什么是悬挂?
悬挂是 TCC 里的老大难问题。它指 Try 请求因为网络延迟,在 Cancel 执行之后才到达。此时流程已经是 Cancel,Try 却未执行却被执行了,预留资源没人释放,形成“悬挂”。解决思路是在 Cancel 阶段判断分支事务状态,如果 Try 还没执行,则标记状态让之后到达的 Try 直接失败,避免资源被锁死。
第 34 题:TCC 如何实现幂等控制?
典型做法是为每个分支事务生成唯一的事务 ID,在事务表中记录状态。Confirm 或 Cancel 执行前先查状态,已处理过就直接返回成功。同时要配合唯一键约束,防止并发情况下重复插入状态记录。面试时如果能说“我用一个 transaction_status 表把 Try、Confirm、Cancel 的状态串起来”,会显得工程经验很足。
第 35 题:什么是分布式事务框架里的分支事务?
分支事务指全局事务中的每一个参与方操作。比如一个下单全局事务,包含库存服务、订单服务、支付服务,每个服务里执行的部分就是一个分支事务。框架通过事务 ID 和分支事务 ID 来追踪状态,2PC 和 TCC 都沿用这个概念。
下表是四种经典方案的对比,面试前可以快速过一眼:
| 维度 | 2PC | 3PC | TCC | Saga |
|---|---|---|---|---|
| 一致性强度 | 强 | 较强 | 最终一致 | 最终一致 |
| 资源锁 | 全局锁 | 全局锁带超时 | 业务预留 | 无全局锁 |
| 侵入性 | 低 | 低 | 高 | 高 |
| 异常处理 | 自动回滚 | 自动回滚/提交 | 需实现 Cancel | 需开发补偿 |
| 适用场景 | 小规模强一致 | 对阻塞要求高 | 资金、积分 | 长流程、大数据量 |
4. 本地消息表、事务消息与可靠性(36~50 题)
4.1 本地消息表和事务消息怎么选
第 36 题:什么是本地消息表?
本地消息表是一种经典实现最终一致性的方案。核心思路是:把业务操作和消息写入放在同一个本地事务里,业务提交时消息表同时写入一条消息。之后由后台任务轮询消息表,把未发送的消息发给 MQ 或其他服务。因为业务和消息是同库事务,所以不会出现“业务成功但消息丢”的情况。
第 37 题:本地消息表的基本流程是什么?
以订单服务为例:订单表和消息表在同一数据库,同一次事务里先插入订单,再插入“待发送消息”;事务提交后,定时任务扫描消息表,把消息投递到 MQ;消费者收到消息后执行库存扣减,成功后回调消息发送方确认;消息发送方更新消息状态为已发送。如果投递失败,定时任务会重试。
第 38 题:本地消息表为什么能替代 2PC?
2PC 靠全局锁实现强一致,本地消息表不锁资源,只把“业务操作和状态记录”绑定在一个本地事务里,剩下的靠异步重试达到最终一致。它比 2PC 性能好、实现简单,但要求下游必须幂等,而且消息可能延迟,所以只适用于最终一致性业务。
第 39 题:什么是事务消息?
事务消息是消息队列提供的一种特殊消息类型。发送方先把事务消息发送到 MQ 的 half 状态,MQ 并不马上投递;发送方继续执行本地事务,根据本地事务结果对 half 消息二次确认——提交或回滚。如果发送方在本地事务执行期间宕机,MQ 会通过回查机制询问发送方,再决定最终投递状态。
第 40 题:事务消息和本地消息表是什么关系?
两者解决的问题是同一个:保证“本地事务结果”与“消息对外可见”一致。本地消息表把消息放在数据库里,灵活但需要自己写扫描任务;事务消息把 half 状态放到 MQ 端,由 Broker 负责状态管理和回查。面试时说“事务消息是本地消息表的 MQ 原生实现”是加分理解。
第 41 题:RocketMQ 是怎么实现事务消息的?
RocketMQ 事务消息分三步:发送 half 消息,消息进入暂不可见状态;执行本地事务,返回 commit 或 rollback;如果本地事务状态不明确,Broker 会定时向发送方发起回查,发送方通过检查本地事务表来确认最终结果。核心依赖是回查机制和幂等消费。
第 42 题:什么是最大努力通知?
最大努力通知是“尽力把结果通知到对方”的模式。比如支付结果通知给商户系统,发送方用定时任务反复调商户回调接口,直到对方返回成功,或达到最大重试次数后转入人工处理。它不保证消息一定被消费,只保证尽量通知,因此适合对一致性要求不极端的场景。
第 43 题:最大努力通知和事务消息有什么区别?
事务消息保证发送方业务成功则消息一定投递;最大努力通知只保证发送方不断重试,不保证接收方一定处理成功。比如支付宝回调商户,商户系统宕机,支付宝会不断重试很多次,再不行就人工账单对账。面试时突出“重试次数、退避策略、人工兜底”是亮点。
4.2 幂等、重试、积压与 Outbox
第 44 题:消息发送失败,事务要怎么收尾?
事务已经提交,消息发送失败时不能再回滚数据库。正确做法是把消息状态标记为待重发,由定时任务重试发送。如果一直失败,超过阈值后告警人工处理。所以消息表设计时,至少要包含状态字段、重试次数、下次重试时间、最后错误信息。
第 45 题:消息重复消费了怎么办?
消息队列一般至少保证一次投递,消费者必须幂等。常见做法有:消费前查幂等表、用业务唯一键做数据库主键或唯一索引、利用 Redis SETNX 做幂等标记。面试时能画出“查询、插入、更新”的二次校验流程会很加分。
第 46 题:幂等表到底怎么设计?
幂等表核心字段:幂等键、业务类型、请求参数、处理状态、处理结果、创建时间。幂等键一般由业务 ID 加上唯一操作码生成。消费时先 insert 幂等记录,利用主键冲突判断是否已处理。重点:插入和业务更新不能在两个事务里,否则并发请求可能同时绕过幂等判断。
第 47 题:重试机制有哪些参数?
重试参数包括:最大重试次数、初次延迟、退避倍数、最大延迟、是否保证顺序、是否跳过某些错误。比如指数退避:第一次延迟 1 秒,第二次 2 秒,第四次 4 秒,直到最大延迟。一个额外心得:对“业务性错误”不要重试,比如余额不足、参数错误,重试多少次都没用,只会积压消息。
第 48 题:消息积压会导致事务状态不一致吗?
会。比如支付成功消息积压,订单一直停留在“待支付”状态。用户可能已经付款,但系统显示未支付。缓解方式:增加消费并发度、提高队列分区数、做超时对账任务,把“已扣款但未更新订单”的数据捞出来补发。面试时提“消息积压要配套对账系统”能体现全局观。
第 49 题:怎么保证消息有序消费?
本地消息表和事务消息在顺序性上都很弱。如果要严格有序,可以把相同业务 ID 的消息路由到同一分区,消费者单线程处理。比如一个订单的所有状态变更消息都发到同一个队列,避免并发导致旧消息后处理。大多数场景不需要全局有序,只需要局部有序。
第 50 题:Outbox 模式和 CDC 是什么关系?
Outbox 模式就是本地消息表的现代化版本:业务表和 outbox 表同事务写入,之后通过 CDC(变更数据捕获)工具监听 outbox 表的 binlog,把新数据发布到 MQ。好处是不用自己写定时扫描任务,也避免轮询压力。但引入 CDC 组件会增加运维复杂度,需要权衡。
下表整理了三种主流最终一致性方案的取舍:
| 方案 | 实现难度 | 可靠性 | 适用场景 |
|---|---|---|---|
| 本地消息表 | 中 | 高 | 内部系统、无强一致要求 |
| 事务消息 | 中 | 高 | RocketMQ 环境、资金类链路 |
| 最大努力通知 | 低 | 中 | 外部回调、对账兜底 |
5. 工程实战直观拆解(51~70 题)
5.1 订单与库存场景:一套标准解法
第 56 题:订单加扣库存,标准解法是什么?
不要试图在一个事务里锁两个服务。标准解法是拆分业务:先创建订单并写一条“待扣库存”的本地事务消息,库存服务消费消息扣减库存;如果扣减失败,则通过定时任务扫描待处理订单,执行补偿流程,比如取消订单或回退库存。整个过程是最终一致,而不是强一致。
第 57 题:账户转账场景怎么设计?
最简单可靠的方式,是让转账操作在同一个库里完成:扣 A 账户、加 B 账户,在同一个本地事务里执行。如果 A 和 B 必须在不同服务,则可以用 TCC:Try 冻结 A 账户资金,Confirm 从冻结资金中实际扣减并给 B 加款,Cancel 解冻。资金类场景建议 TCC 而不是裸消息,因为资金状态需要可感知的中间状态。
第 58 题:支付、订单、积分环节怎么保证最终一致?
典型链路是:支付成功回调 -> 更新订单状态 -> 加积分 -> 发物流消息。可以用每个环节一个本地事务消息,通过 MQ 串联。比如订单服务本地事务更新“已支付”状态并发送“积分消息”,积分服务消费后加积分并打回执。任何一环失败,重试消费即可。若积分系统因为用户黑名单失败,则走补偿流程记录异常,由对账任务修复。
5.2 全局事务 ID、分布式锁与性能
第 59 题:分布式锁在事务中怎么用?
分布式锁用来保证并发场景下的互斥操作。比如同一个用户同时提交两个订单,库存服务扣库存时,要在“商品维度”加锁,保证两次扣减串行。分布式锁加锁顺序和事务顺序要一致,否则容易出现死锁。
第 60 题:业务锁、乐观锁、分布式锁的区别?
业务锁是应用层根据业务状态阻止操作;乐观锁用版本号或条件更新控制并发;分布式锁是在多节点间共享互斥,比如 Redis 锁。库存扣减可以用“UPDATE 库存 SET 剩余=剩余-1 WHERE 商品_ID=? AND 剩余>=1”这种条件更新,自带乐观锁效果,不需要额外分布式锁。这个答案面试官会很认可,因为体现了对多方案的理解。
第 61 题:Redis 分布式锁真的安全吗?续期怎么处理?
Redis 锁的经典问题是锁过期而业务没执行完。解决方式有两种:一是用 Redisson 的看门狗自动续期;二是在锁的 value 里带上线程唯一标识,释放时校验是否本线程持有。如果对安全性要求很高,还需要考虑 Redis 主从切换丢锁的情况,这时候只能引入红锁或数据库锁做兜底。
第 62 题:抢锁失败,事务怎么办?
抢锁失败绝不能盲目重试。建议是设置一个短暂等待窗口,然后查询最新库存状态决定是否继续。比如用户秒杀时,锁获取失败说明有其他请求正在扣减,此时直接返回“库存紧张”即可,避免大量自旋把 Redis 打挂。
第 63 题:全局事务 ID 怎么在链路里传递?
全局事务 ID 需要在 RPC 调用链和异步消息链路里透传。同步调用用 ThreadLocal 和拦截器把 traceId 传递下去;异步消息要在消息体里带上全局事务 ID。日志、数据库流水、消息轨迹都要用同一个 ID,方便排查。这和链路追踪的 traceId 类似,可以共用一套。
第 64 题:怎么给一张流水表设计幂等约束?
流水表通常用业务订单号加操作类型做唯一索引。比如支付流水表,唯一键是“订单编号 + 支付渠道 + 支付单号”,这样同一支付回调重试多次,只会插入第一条流水。更新时用状态机控制,状态只能从“待处理”到“成功”到“关闭”,不能逆序。
5.3 过程与性能:事务的代价
第 65 题:事务的超时时间怎么定?
超时时间不是拍脑袋定。要结合数据库连接等待时长、业务接口 P99 耗时、下游超时设置综合判断。比如下单链路 P99 是 800ms,那事务超时给 3 秒比较合理;如果给 10 秒,数据库连接池很容易被长事务拖垮。面试时说“我会按 P99 的 4 到 5 倍设置,并加入熔断降级”能体现工程功底。
第 66 题:异步化和分布式事务是一回事吗?
不是。异步化是写 MQ 后立刻返回,任务的执行延迟到后台;分布式事务要解决的是多个服务之间数据一致性的问题。很多最终一致性方案依赖异步消息,但异步之后依然要用幂等、重试、对账来保障正确性。两者是方法和技术栈的关系。
第 67 题:什么业务能接受最终一致?
判断标准是看一致性的时间窗口会带来多大风险。比如用户发一条动态,多等几秒显示没问题,可以用最终一致;扣款、提现、退款这类资金操作,用户和业务方都希望立即明确结果,最好强一致或事务消息。面试时可以举例:内容发布、点赞计数、社交动态适合最终一致;支付、库存扣减、券核销要看场景设计。
第 68 题:怎么验证分布式事务真的生效了?
靠故障注入。构造下游服务超时、数据库连接中断、MQ 宕机等场景,观察日志里补偿操作是否执行、状态是否恢复。同时准备对账程序,定时比对订单和支付状态,找出不一致数据。真正严格的团队还会做“混沌工程”,随机杀掉一个服务节点再跑核心链路。
第 69 题:分布式事务的性能损耗主要在哪?
损耗主要在三块:协调者通信开销、资源锁定时间、补偿/回查的额外调用。TCC 比裸 RPC 多出 Try 和 Cancel 的次数,XA 则在准备阶段锁住所有资源。性能优化的大方向是减少事务参与者数量、缩小事务边界、把强一致转化为最终一致。
第 70 题:审计日志和事务日志怎么配合?
事务日志记录全局事务 ID、各个分支的成功/失败时间线;审计日志记录业务操作的前后状态和操作人。排查问题时,先查审计日志定位业务动作,再查事务日志定位技术动作。面试时强调“日志里要能还原每一次状态流转”,会很有说服力。
6. 手写题与工具选型(71~80 题)
6.1 手写 2PC、TCC、本地消息表的套路
第 71 题:让我手写一个 2PC 协调器,怎么答?
不要真的去写全流程,而是描述核心结构。协调器要维护事务集合、参与方列表、状态机。demo 代码可以这样起步:
class TransactionCoordinator { String txId; List<ResourceParticipant> participants; boolean prepare() { for (ResourceParticipant p : participants) { if (!p.prepare(txId)) return false; } return true; } void commit() { for (ResourceParticipant p : participants) p.commit(txId); } void rollback() { for (ResourceParticipant p : participants) p.rollback(txId); } }重点强调:prepare 从第一个参与者返回 false 后,要调用所有已 prepare 成功的参与者进行 rollback,防止悬挂资源。
第 72 题:手写一个 TCC 框架最少要几个类?
最少需要四个核心概念:分支事务注册器、事务管理器、状态存储、调用代理。实际代码里常见的是用一个注解 @TccAction 标记 Try 方法,框架通过反射自动调用 Confirm 和 Cancel。手写时建议画出“启动事务 -> Try 全部成功 -> 记录状态 -> 调 Confirm -> 更新状态 -> 异常则调 Cancel”的代码骨架。
第 73 题:手写本地消息表的代码思路?
核心是把业务写入和消息写入放在同一个事务里。伪代码如下:
@Transactional public void createOrder(Order order) { orderDao.insert(order); MessageRecord message = new MessageRecord(); message.setBizId(order.getId()); message.setStatus("PENDING"); messageDao.insert(message); }之后定时任务扫描 PENDING 状态消息,调用 MQ 发送接口,成功后把状态改成 SENT。值得注意的是,如果 MQ 生产段有多个不同的 topic,要把 topic 和 payload 都存到消息表里,扫描任务才能正确投递。
6.2 中间件与数据库选型
第 74 题:Seata AT 模式怎么接入?
Seata AT 模式依赖数据库 undo_log 表,通过代理数据源自动生成前后镜像。接入时先部署 TC(事务协调中心),然后在服务里引入 Seata 依赖和配置数据源代理,业务代码加上 @GlobalTransactional 注解。全局事务发起时,TM 开启全局事务,RM 分支注册,TC 负责协调提交或回滚。
第 75 题:Spring JTA 怎么用?
Spring 中使用 JTA,配置一个 JtaTransactionManager,并将各个数据源封装成支持 XA 的 XADataSource。代码中仍然使用 @Transactional,但底层会走 XA 两阶段提交。由于需要 application server 或 Atomikos 等事务管理器支持,它更适合服务数量少、强一致要求高的项目。
第 76 题:RocketMQ 事务消息怎么写?
发送端先调用sendMessageInTransaction发送 half 消息,然后实现LocalTransactionExecutor在回调里执行本地事务并返回提交或回滚状态。还需要实现TransactionListener的回查逻辑,通过检查数据库事务状态来确定回查结果。常见坑是回查逻辑必须幂等,一个本地事务被回查多次结果不能变。
第 77 题:Redis 分布式锁工具类要写哪些方法?
核心方法有四个:lock、unlock、tryLock、forceUnlock。lock 使用 SET key value EX seconds NX;unlock 用 Lua 脚本比对 value 后删除,防止误删他人锁。tryLock 支持等待时间和过期时间。如果要在本地写 demo,可以用下面这个简洁版本:
public boolean tryLock(String key, String requestId, int expireSeconds) { String result = redis.opsForValue() .setIfAbsent(key, requestId, Duration.ofSeconds(expireSeconds)); return Boolean.TRUE.equals(result); } public boolean unlock(String key, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; Long result = redis.execute(script, List.of(key), List.of(requestId)); return result != null && result == 1L; }面试时能指出“value 必须带请求ID,不能只按 key 删锁”就是加分项。
第 78 题:DTM 是什么?和 Seata 有什么差异?
DTM 是一套开源的分布式事务管理器,支持 TCC、Saga、事务消息、二阶段消息等模式。Seata 更依赖 Java 生态,和 Spring Cloud 结合深。DTM 的优点是支持跨语言、实现更轻量。选型建议:团队 Java 技术栈优先 Seata,多语言异构团队看 DTM。两者都不是万能,本质上都是帮你管理分支事务状态和补偿逻辑。
第 79 题:分布式数据库比如 TiDB 还需要分布式事务吗?
TiDB 在数据库层使用 Raft 和多版本并发控制实现了强一致,单库内部的操作可以当作一个普通事务处理。但如果业务是跨多个独立的微服务、每个服务用独立数据库,TiDB 只能保证单个服务内部的事务,跨服务的分布式事务问题仍然存在。所以关键词取决于“事务边界”是否跨出了单一数据库。
第 80 题:从零设计一个分布式事务方案,思路是什么?
先列出参与者、动作和补偿动作。然后画状态机:初始状态、Try/执行成功状态、确认状态、补偿状态。再设计事务表:全局事务 ID、分支 ID、状态、时间戳、错误信息。最后设计超时和重试:定时扫描超时事务,发重新执行或补偿指令。这套思路不依赖任何框架,能套用到任何中间件上,面试官会把它当作“有架构能力”的证据。
7. 面试话术与避坑清单(81~90 题)
7.1 把题变成能说出口的“故事”
第 81 题:CAP 定理三句话怎么说?
第一句:网络分区一定会出现。第二句:分区时一致性可用性只能选一个。第三句:我们业务选的是可用性优先,最终一致性由异步消息和补偿来兜底。三句话讲完,面试官就知道你理解到点子上了。千万别把 CAP 当成任何时候都能三个全占的数学原理来背。
第 82 题:你们项目的订单、库存怎么讲才加分?
用“背景、问题、方案、效果”四段式。比如:订单和库存是独立服务,存在超卖风险;我们用 RocketMQ 事务消息,订单先落库,库存靠消息异步扣减,同时库存表加剩余库存 >= 扣减数量的条件更新兜底;压测后 QPS 保持在 XXX,未出现超卖。这样回答既具体又有数据。
第 83 题:为什么不用 2PC?
2PC 的阻塞时间太长、协调者单点风险高、对数据库和中间件的支持要求高,微服务高并发场景下很容易拖垮系统。如果面试官继续追问,可以补充:只有业务真正需要全局强一致时才考虑 XA,比如小规模内部系统,但互联网场景更倾向最终一致。
第 84 题:实现最终一致性有哪几条路?
可以梳理为四类:基于数据库的本地消息表和 Outbox;基于 MQ 的事务消息;基于业务补偿的 TCC 和 Saga;基于定时任务的对账补偿。这四类不是互相排斥,经常是组合使用,比如核心链路用 TCC,旁路用事务消息,长期一致靠对账。
第 85 题:一致性要求和性能是怎么取舍的?
取舍的本质是收益和成本。强一致方案写放大、锁开销大、TPS 低;最终一致方案 TPS 高但存在延迟窗口。实际做法是对业务分级:资金类强管控,内容类最终一致,延缓类直接异步。面试时表达“不能统一套用一个方案”往往比背一堆名词更让人信服。
第 86 题:TCC 和 Saga 怎么选型?
TCC 适合参与者少、每个分支都可能失败、需要资源预留的高一致性场景,比如扣款和冻结;Saga 适合长流程、参与者多、需要处理复杂业务补偿的场景,比如旅行订单预订多个外部供应商。TCC 侵入性强但控制力强,Saga 更灵活但隔离性差。如果面试官说“我们都很短”,那就选 TCC。
第 87 题:消息丢了可以被接受吗?
不能接受。消息丢了会导致业务流程静默中断,后面所有补偿逻辑都没法触发。至少要保证“落库后可重发”这一层,不能让消息只有一份寄存在 MQ 内存里。所以本地消息表方案比单纯 MQ 发送更可靠,因为它有落库副本。
第 88 题:怎么跟面试官讲幂等?
先用一句话定义:同一请求执行多次,结果与执行一次相同。再举自己的例子:支付回调接口用唯一支付单号做幂等键,重复回调直接查库返回成功。最后提一嘴实现要点:幂等判断和业务写入必须在同一事务里,否则并发下会失效。
第 89 题:遇到完全不会的问题怎么办?
不要编。一是承认问题有盲区,二是描述你能想到的相关概念,三是表明会后排查补充。比如被问“Seata Raft 模式怎么部署”,你可以说:“Raft 细节我不是很熟,但我了解 Seata 的高可用原理是让 TC 通过 Raft 做状态复制,我查过文档再深入回答。”诚实加上结构化的现学现卖思路,比撒谎更能保住印象分。
第 90 题:怎么把分布式事务讲成亮点而不是死穴?
用“业务实例驱动”。每次回答都绑一个具体场景:某次促销活动,用户在 10 秒内下单量暴增,库存服务偶发超时,导致部分订单显示异常。我如何通过状态流加重试最终解决。这样面试官看到的是你能从问题中发现风险并设计解决方案,而不只是背诵了一份题库。
下表的“追问应对”可以直接背下来,实战时很有用:
| 面试官追问 | 应对思路 |
|---|---|
| 协调者宕机怎么办 | 高可用/多副本/WAL 日志恢复 |
| 消息丢了怎么办 | 本地消息表落库 + 定时任务补发 |
| 消费重复怎么办 | 幂等表 + 唯一索引 + 状态机 |
| 事务超时怎么设 | 参考 P99 耗时和下游超时 |
| 长事务怎么优化 | 缩短事务边界、异步化、分阶段提交 |
8. 进阶场景与面试复盘(91~100 题)
8.1 多数据源、跨机房与事件驱动
第 91 题:跨机房的分布式事务怎么做?
跨机房最核心的是网络延迟和分区。建议尽量做单机房强一致、跨机房最终一致。可以按用户地域路由,让同一用户的写操作落在同一机房。确实要做跨机房同步时,用双向同步加冲突检测,配合消息补偿。面试里直接上 XA 跨机房基本等于自杀方案,因为锁等待时间根本不可控。
第 92 题:多数据源事务有哪几种解法?
常见有三种:XA/JTA 强一致、共享存储(多个服务连接同一个数据库)、业务层面的 TCC 或事务消息。共享存储性能最好但耦合高,适合内部系统。面试时说出“数据源合并需要评估扩展性,不能为了省事把业务全塞在同一库”会显得有判断力。
第 93 题:事务消息比普通消息慢多少?
事务消息多一次 half 消息发送、一次本地事务确认、可能还有一次回查。RocketMQ 场景下整体吞吐会低于普通消息,大约比普通异步消息慢 20% 到 50%,具体取决于回查频率和消息体大小。对性能敏感的非关键链路,不用为了保险硬上事务消息。
第 94 题:海量订单下对账怎么做?
对账的核心是分治。按用户维度、时间维度、状态维度分片,每天跑批比对订单表和支付流水表。对账发现差异后进入补偿队列,按优先级处理。海量数据下,比对逻辑不能把所有数据加载到内存,要分批拉取、增量比对。再加一条心得:对账系统本身也要幂等,重复跑批不能重复补偿。
第 95 题:实时性和一致性冲突怎么办?
实时性要求高就要尽量减少中间节点,比如本地事务后直接返回,不需要等下游结果;一致性要求高就要让下游确认后才返回。折中方案是:主流程实时完成,旁路异步做一致性校验。比如扣款成功后立即返回用户“支付成功”,但库存更新走异步消息,哪怕延迟两秒,对用户感知影响不大。
第 96 题:分布式事务和事件驱动是什么关系?
事件驱动是系统设计风格,通过发布事件解耦服务;分布式事务是一种资源一致性保障机制。事件驱动中,事件发布和应用状态变更之间天然存在“要么同时成功、要么同时失败”的矛盾,所以需要本地消息表或事务消息来保证事件可靠发布。可以简单理解:事件驱动把事务边界变成事件流,而事务可靠性是让事件流不丢失的底座。
第 97 题:领域事件能替代分布式事务吗?
不能完全替代,但能减少对分布式事务的依赖。领域事件记录了业务事实,比如“订单已支付”是一个事实,下游订阅事件各做各的更新,状态不一致通过事件回溯修正。它和分布式事务是不同层面的东西,一个是建模方式,一个是技术保障。只讲事件不讲可靠落盘,会被面试官追问“事件发布失败怎么办”。
第 98 题:如何从业务层面绕过分布式事务?
方法是重新划定事务边界让依赖收敛。比如用更粗粒度的聚合:订单和库存明细归为同一个聚合根落在同一个库里;再比如简化操作:取消“库存预占”这种强一致动作,改为生成订单后再异步分配库存,抢不到就取消。绕过不是不保证一致性,而是把一致性问题的发生范围缩小到一个库内或一个服务内。
第 99 题:评价一个分布式事务方案用什么指标?
我常用的指标有五个:事务吞吐量 TPS、事务成功率、数据不一致率(对账差异数量)、平均补偿耗时、人工介入率。上线一个方案后先测 TPS 和成功率;运行一段时间后看对账差异率和人工介入次数。如果人工介入率高于千分之一,说明方案不可靠,隐患多。
第 100 题:一次分布式事务面试复盘的重点?
面试结束后,别只复盘对错。把面试里被追问的每个问题都记下来,回看自己哪个环节答得含糊。比如答 TCC 时没提“空回滚”,说明补偿意识不够;答 Saga 时没提“隔离性”,说明只看了表面流程。用 DeepSeek 再把遗漏的问题扩展开,逐个写出自己的业务案例,比再背五十道新题管用。
我个人的体会是:分布式事务面试能不能过,核心不在于你知道多少方案名字,而在于遇到反例时能不能看出方案的边界。这两年我用 AI 工具整理面试题越来越频繁,但最后的判断和场景化能力还是要靠自己完善。建议大家把这份清单当作自测框架,每一组题目都对着自己的项目经历过一遍,想清楚“这套方案如果调挂了,我能不能十分钟内查出来、能不能自动恢复”。能说到这一步,这份题单的价值就到了位。