想象你是公司的行政专员小王,要组织一场跨部门团建聚餐。这件事牵扯三方:餐厅得留出足够的桌位、各部门同事得提前报名、财务得批下活动经费。麻烦的是,这三件事分别由不同的人负责——一个环节掉链子,整个聚餐就可能泡汤。 你可能会想:这不就是“先打电话问清楚、再统一确认、有人来不了就取消”吗?没错!这正是分布式事务里的 TCC 模式(Try-Confirm-Cancel)在生活中的样子。它把一次跨服务的复杂事务拆成三步:先尝试预留资源,再确认真正执行,不行就取消补偿。这套思路,让分布在不同服务里的数据最终保持一致,是微服务架构后端开发者绕不开的必修课。

岗位关联

掌握 TCC,你就能在微服务架构中优雅地解决跨服务数据一致性问题,理解分布式系统设计中“业务补偿”的核心思想,是后端工程师进阶架构设计的关键能力。

目录
  1. 可视化演示
  2. 问题背景:跨服务事务的困局
  3. Try阶段:先报名预占资源
  4. Confirm阶段:人到齐正式开席
  5. Cancel阶段:取消不伤和气
  6. 对比两阶段提交:TCC的取舍
  7. 补偿机制设计:三个防守要点
  8. 落地实战要点:上线前必须做的事
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示TCC事务三阶段:Try-Confirm-Cancel多服务协调流程、TCC与2PC对比分析、补偿机制幂等性设计,三模式递进呈现从事务协调到性能对比到可靠性保障。

本节配有交互动效(进入视口后自动加载,约 3KB)。

TCC三阶段事务协调流程 TRY: 协调器调用ORD/ACC/INV三个服务预留资源,不执行真正业务操作 CFM: 全部TRY成功后确认提交,执行真正的业务操作 TCC = Try(reserve) + Confirm(commit) + Cancel(rollback) 3服务协调,延迟45ms,成功率96%

问题背景:跨服务事务的困局

公司要组织一次跨部门团建聚餐。行政小王需要确保三件事同时搞定:餐厅有足够的桌位、各部门同事都能出席、财务批准经费。这三件事分属不同负责人,任何一环出问题,整个聚餐就泡汤了。更要命的是,每个负责人只关心自己的一摊事,小王手里没有任何工具能“一键”保证三件事同时成功——这正是分布式系统里跨服务事务的真实困境。

在软件世界里,一次下单操作往往要同时扣减库存、生成订单、累加积分,而这三个数据分属不同的微服务,各自使用独立的数据库。单机数据库的本地事务(ACID)只能保证一个库内的原子性,一旦跨越服务边界,本地事务就使不上劲了。如果扣了库存却没能生成订单,用户就会遇到“钱付了货没到”的严重事故。

TCC 模式(Try-Confirm-Cancel)就是为解决这个困局而生的:它把一次全局事务拆成三个阶段,通过“先预留、再确认、失败补偿”的方式,在业务层实现最终一致性。其核心逻辑可以用一条公式概括——只有所有服务的 Try 都成功,才允许执行 Confirm:

回到团建的比喻:小王必须先电话确认餐厅有位、同事能来、财务批款,这三条都满足了,才敢正式下发活动通知。任何条件不满足,聚餐方案就要重新议。TCC 正是用同样的思路,把“全局事务的成功”建立在一个个可验证的“预留成功”之上。

Tsuccess≡⋀i=1nTry⁡i=OK  ⇒  ⋀i=1nConfirm⁡i=DONET_{\text{success}} \equiv \bigwedge_{i=1}^{n} \operatorname{Try}_i = \text{OK} \;\Rightarrow\; \bigwedge_{i=1}^{n} \operatorname{Confirm}_i = \text{DONE}
这条公式的意思是:所有参与者的“预留尝试”都成功,才触发所有人的“确认执行”。任何一个预留失败,整个事务都不能进入确认阶段。
  1. Try成功集合$\bigwedge_{i=1}^{n} \operatorname{Try}_i = \text{OK}$ 表示所有参与事务的服务,其 Try 操作都返回了成功,这是进入下一阶段的前提条件。
  2. 逻辑蕴含$\Rightarrow$ 表示左边的条件成立时,右边必须成立——也就是说“全部 Try 成功”是执行 Confirm 的硬性门槛。
  3. Confirm完成$\bigwedge_{i=1}^{n} \operatorname{Confirm}_i = \text{DONE}$ 表示所有服务的确认操作都执行完成,此时事务才算真正成功。

Try阶段:先报名预占资源

团建的报名阶段,小王在群里发起了接龙。每位同事回复“能来”,小王就在表格里记下名字和人数。注意,这时候只是“登记意向”,并没有真正订餐,也没有产生任何费用——大家只是口头占了个位。这个“占位但不消费”的动作,就是 TCC 的第一阶段:Try(尝试)。

对应到技术上,事务协调器会依次调用每个参与服务的 Try 接口。Try 接口要做两件事:业务检查和资源预留。业务检查是确认服务当前的状态是否允许执行本次事务——比如库存是否充足、用户账户是否冻结;资源预留则是把这次事务需要的资源先“锁”在一边——比如扣减可用库存到冻结库存、把用户积分预占起来。Try 执行完毕后返回两个业务结果:OK(预留成功)或 FAIL(预留失败)。

就像接龙里说“能来”的同事,只是口头答应,座位并未真正使用。Try 阶段返回 OK 只代表“资源已预占、随时可执行”,绝不代表事务已经提交。这个“只占位不消费”的巧妙设计,让 TCC 避免了传统方案中长时间锁库的性能损耗。

Try⁡i(tx)=Check⁡i(tx)  ∧  Reserve⁡i(tx)\operatorname{Try}_i(\text{tx}) = \operatorname{Check}_i(\text{tx}) \;\wedge\; \operatorname{Reserve}_i(\text{tx})
Try 的成功 = 业务检查通过 + 资源预留完成。两者缺一不可,相当于“条件都符合且座位已占好”。
  1. 业务检查$\operatorname{Check}_i(\text{tx})$ 负责检查本次事务的业务前置条件,比如库存是否足够、用户状态是否正常,这一步相当于确认“这位同事是不是真能来”。
  2. 资源预留$\operatorname{Reserve}_i(\text{tx})$ 负责把资源冻结起来,预占操作本质是一种“记账式冻结”——数据还在账上,但已不可被其他事务使用。
  3. 组合与结果$\wedge$ 表示业务检查与资源预留必须同时成功;Try 接口最终返回 OK 或 FAIL,给协调器一个明确的投票结果。

Confirm阶段:人到齐正式开席

开席当天,小王在餐厅门口拿着名单点人数。人到齐了,他通知服务员:“这桌可以上菜了!”直到这一刻,预留的座位才真正派上用场,餐厅开始真正消耗食材、提供服务。把“预占”变成“实实在在的执行”,就是 TCC 的第二阶段:Confirm(确认)。

Confirm 阶段的前提是:所有服务的 Try 全部返回 OK。协调器随后逐个调用每个服务的 Confirm 接口,把 Try 阶段预占的资源正式转为实际业务操作——冻结的库存真正扣减、锁定的积分真正入账、草稿订单真正落库。这里有一个关键设计:Confirm 操作必须幂等。因为在分布式环境中,网络可能抖动、消息可能重复投递,同一个 Confirm 请求可能被送达多次,但业务结果必须与执行一次完全一致。

Confirm 阶段同样可能出现失败。比如某个服务在 Confirm 时宕机了,协调器不能直接宣告事务失败,而要通过重试机制持续推动 Confirm 完成——因为此时资源已经被预占,不执行到底就会造成资源悬空。这就像小王发现有个同事进餐厅时走丢了,他得想办法把人找回来,而不是直接取消整桌菜。

Confirm⁡i(k)(tx)≡Confirm⁡i(tx),∀k≥1\operatorname{Confirm}_i^{(k)}(\text{tx}) \equiv \operatorname{Confirm}_i(\text{tx}), \quad \forall k \geq 1
这个公式说明:无论 Confirm 被调用多少次(k 次),效果都等于只调用一次。用生活的话说:服务员重复问三遍“这桌开炒吗”,你回答三遍“开炒”,厨房也只会炒一份菜。
  1. 重试次数$\operatorname{Confirm}_i^{(k)}$ 表示第 $k$ 次调用服务 $i$ 的确认接口,其中 $k \geq 1$,用来刻画网络重试的场景。
  2. 等价关系$\equiv$ 表示任意次调用的最终效果与单次调用完全一致,既不会重复扣库存,也不会重复加积分。
  3. 全称约束$\forall k \geq 1$ 强调无论重试多少次,业务结果都不能有所偏移——这是分布式系统里“幂等性”的数学表达。

Cancel阶段:取消不伤和气

就在开席前十分钟,一位核心成员突然来电:临时出差赶不回来了!小王立刻通知餐厅撤掉他预占的座位。餐厅不损失什么,其他同事的座位不受影响,整个聚餐照常进行。这种“有人掉队就及时释放资源、不让其他人受牵连”的操作,就是 TCC 的第三阶段:Cancel(取消)。

Cancel 阶段的触发条件是:有任何一个服务的 Try 返回 FAIL。协调器收到失败信号后,会调用所有“已 Try 成功”的服务的 Cancel 接口,把预留的资源逐一释放——库存加回去、积分解冻、草稿订单标记作废。这样整个全局事务虽然没有成功,但所有服务都回到事务开始前的状态,这就是“补偿”二字的含义。

Cancel 阶段有一个隐蔽的坑叫空回滚:如果某个服务的 Try 请求超时了,协调器不确定它是否预留成功,只好直接对它发 Cancel。但这个服务其实根本没预留任何资源——此时 Cancel 操作必须能判断“无资源可释放”并优雅跳过,而不是报错。这就像小王给一个没报名接龙的同事打电话说“你不用来了”,对方一脸茫然,但也无伤大雅。

∃ i,Try⁡i=FAIL  ⇒  ∀j,Cancel⁡j=DONE\exists\, i, \operatorname{Try}_i = \text{FAIL} \;\Rightarrow\; \forall j, \operatorname{Cancel}_j = \text{DONE}
只要有一个人说“来不了”,那么所有人都要把占的位子退掉。Cancel 不是可选项,而是必选项——每一个预留过资源的服务都必须完成补偿。
  1. 存在失败$\exists\, i, \operatorname{Try}_i = \text{FAIL}$ 表示至少有一个服务的 Try 返回了失败,这是触发补偿的信号。
  2. 全量补偿$\forall j$ 表示要对事务涉及的所有服务执行 Cancel——不仅包括 Try 成功的,也包括那些“还没轮到执行 Try”的服务。
  3. 完成标志$\operatorname{Cancel}_j = \text{DONE}$ 表示每个 Cancel 操作必须真正执行完成,事务才能对外宣告失败结束,避免资源泄漏。

对比两阶段提交:TCC的取舍

2PC(两阶段提交)像一场现场表决大会:主持人喊“同意的举手”,所有参与者必须同时在场、当场表态,有人手机没信号,全场就得干等。这种方案的优点是强一致,缺点也显而易见——资源被锁住期间,其他事务全部阻塞,协调器更是单点瓶颈。一旦协调器宕机,所有参与者都会卡在“等指令”的状态。

TCC 则更像线上报名系统:每个人在不同时间点完成自己的动作,不用实时在线,随时可以取消。它将数据库层面的资源锁替换为业务层面的“预占+补偿”,虽然实现复杂度提高了,但换来了两个关键收益:一是无同步阻塞,预占资源不会锁住数据库行,其他事务可以照常操作;二是故障容错更强,即使某个服务暂时不可用,协调器也能通过重试和补偿把事务导向终态。

不过 TCC 也并非银弹:它需要业务方为每个服务手写 Try、Confirm、Cancel 三套逻辑,开发和维护成本高;它保证的是最终一致性而非实时强一致——在事务进行中的某一瞬间,各个服务的数据可能是不一致的。选择 2PC 还是 TCC,本质是在“强一致”和“高可用”之间做取舍。

T2PC=Tlock+Tcommit,TTCC=Ttry+TconfirmT_{\text{2PC}} = T_{\text{lock}} + T_{\text{commit}}, \qquad T_{\text{TCC}} = T_{\text{try}} + T_{\text{confirm}}
2PC 的耗时主要在“锁”上,锁越久阻塞越久;TCC 的耗时主要在“业务操作”上,没有数据库锁,各服务并行度更高。
  1. 2PC的耗时模型$T_{\text{lock}}$ 是数据库锁的持有时间,$T_{\text{commit}}$ 是提交耗时;在锁释放前,其他事务都只能排队等待。
  2. TCC耗时模型$T_{\text{try}}$ 是业务预占耗时,$T_{\text{confirm}}$ 是确认执行耗时;资源只在业务层被临时占用,底层数据库完全无锁。
  3. 结构差异TCC 把“锁”换成了“业务操作”,单个事务不再阻塞其他事务,系统整体的吞吐量和可用性都显著提升。

补偿机制设计:三个防守要点

管理团建报名,最怕三种意外:同事重复接龙报了两次名(重复请求);没报名的人被通知“请取消座位”(空回滚);人走了但餐厅还留着座位(悬挂)。这三件事,恰好对应 TCC 补偿设计的三大要点:幂等控制、空回滚、悬挂处理。

幂等控制是地基。协调器与各个服务之间通过消息通信,消息可能重复投递,服务必须保证同一个事务请求执行多次的效果等于执行一次。实现方案通常是为每个事务生成唯一的事务 ID,服务端维护一张“已处理事务”记录表。收到请求先查表:

空回滚要求 Cancel 操作对“从未 Try 过”的事务也能安全执行——不能因为找不到预留记录就报错中断。悬挂处理则要求:在 Cancel 已经执行完之后,如果迟到的 Try 结果才返回,协调器必须能识别当前事务已处于终态,不再触发 Confirm——否则就会出现“座位都退了,人又来了”的混乱局面。这三个要点,是 TCC 上线前必须逐一验证的防守底线。

result={stored,tx_id 已存在execute,tx_id 不存在\text{result} = \begin{cases} \text{stored}, & \text{tx\_id 已存在} \\ \text{execute}, & \text{tx\_id 不存在} \end{cases}
这张表就像小王的报名登记表:名字已经在表上,就告诉他“你已经报过了”;名字不在表上,才登记新记录。
  1. 查重收到请求后先拿 $\text{tx\_id}$ 去查询“已处理事务”记录表,判断这个请求是否处理过。
  2. 命中直接返回若 $\text{tx\_id}$ 已存在,则直接返回历史结果 $\text{stored}$,不再执行业务逻辑——这是防重复执行的关键。
  3. 未命中才执行若 $\text{tx\_id}$ 不存在,才真正执行业务并把结果记录保存下来 $\text{execute}$(事务 ID 作为唯一键),保证同一事务只执行一次。

落地实战要点:上线前必须做的事

组织一场百人团建,光靠小王脑子记肯定不行,必须有签到表、时间表和应急方案。落地 TCC 也同样不是定义三个接口就完事,而是需要配套三件基础设施:事务状态机、超时控制、事务日志。

事务状态机用来描述一次全局事务的完整生命周期。每一个服务都要维护自己的事务状态,协调器同样维护全局状态。所有状态迁移必须是有向且确定的,配合持久化记录,才能在任何节点宕机后恢复现场。

超时控制解决的是“请求发出去了但没回应”的问题。协调器给每个请求设置超时时间,超时后不能简单判定失败,而要通过查询接口主动确认服务端的真实状态——是没收到消息,还是执行了但响应丢了。事务日志则是保命的记录:每一次 Try、Confirm、Cancel 的发起和结果都要落库。宕机重启后,协调器扫描日志,找出所有未到达终态的事务,继续推进或补偿。小王之所以能把百人团建安排得明明白白,靠的正是那张永远实时更新的签到表——分布式事务的可靠性,同样建立在完备的工程配套之上,而不是靠运气。

S:INIT→TRYING→{CONFIRMING→DONECANCELING→DONES: \text{INIT} \to \text{TRYING} \to \begin{cases} \text{CONFIRMING} \to \text{DONE} \\ \text{CANCELING} \to \text{DONE} \end{cases}
状态机就是一张“团建进度表”:从“未开始”到“报名中”,要么走向“确认开席”,要么走向“取消收场”,最终都归于“办完”。
  1. 初始与尝试$\text{INIT}$ 是事务刚创建时的状态,随后进入 $\text{TRYING}$,协调器开始逐个调用各服务的 Try 接口。
  2. 分支决策所有 Try 全部成功则进入 $\text{CONFIRMING}$ 状态;只要有任一失败,就转入 $\text{CANCELING}$ 状态。
  3. 终态$\text{CONFIRMING}$ 和 $\text{CANCELING}$ 两个分支最终都会落到 $\text{DONE}$,表示事务结束,此时所有日志落盘、资源清理完毕。

AI 应用场景

电商下单链路:消灭“付款后没货”

阶段:跨服务数据一致性保障

解决的问题:传统下单流程中,库存服务、订单服务、优惠券服务各自独立处理数据。没有 TCC 时,一旦库存扣减成功但订单创建失败,用户既付了钱又拿不到货,客服投诉堆积如山。引入 TCC 后,库存服务先冻结商品数量、订单服务先创建待确认订单、优惠券服务先锁定优惠券;三者全部 Try 成功才统一 Confirm,任何一步失败则统一 Cancel。

具体案例:某电商平台大促期间订单量暴涨,使用 TCC 后,下单链路的超卖率降为零,因跨服务数据不一致导致的售后工单减少了 80% 以上。

跨境支付与账户冻结:资金安全双保险

阶段:资金类业务的分布式事务管理

解决的问题:跨境支付涉及支付服务、外汇服务、账务系统三个服务,资金操作必须万无一失。Try 阶段冻结付款方资金、锁定汇率;Confirm 阶段完成扣款和收款方入账;若汇率波动超限导致 Try 失败,Cancel 阶段解冻资金并按原汇率退回。相比 2PC 的全局锁,TCC 在资金类高并发场景下避免了账户行锁的竞争,大幅提升吞吐量。

具体案例:某跨境支付公司将银行卡转账从 2PC 迁移到 TCC 后,账务系统的单账户并发从每秒 200 笔提升到 2000 笔,且从未出现资金错账。

Python 演示

python108 行
# TCC 分布式事务模拟器class Service:    """服务基类:三个接口分别是 Try / Confirm / Cancel"""    name = "基础服务"     def try_op(self, tx_id):        raise NotImplementedError     def confirm_op(self, tx_id):        raise NotImplementedError     def cancel_op(self, tx_id):        raise NotImplementedError  class StockService(Service):    name = "库存服务"     def __init__(self):        self.free = 100      # 剩余可用库存        self.reserved = 0    # 已预占库存     def try_op(self, tx_id):        if self.free >= 1:            self.free -= 1            self.reserved += 1            return True        return False     def confirm_op(self, tx_id):        self.reserved -= 1     def cancel_op(self, tx_id):        if self.reserved > 0:            self.reserved -= 1            self.free += 1  class PointService(Service):    name = "积分服务"    fail_next = False     def __init__(self):        self.balance = 1000   # 积分余额        self.frozen = 0       # 已冻结积分     def try_op(self, tx_id):        if PointService.fail_next:            PointService.fail_next = False            return False        if self.balance >= 100:            self.balance -= 100            self.frozen += 100            return True        return False     def confirm_op(self, tx_id):        self.frozen -= 100     def cancel_op(self, tx_id):        if self.frozen >= 100:            self.frozen -= 100            self.balance += 100  class TCCTransaction:    def __init__(self, tx_id, services):        self.tx_id = tx_id        self.services = services        self.log = []     def execute(self):        # 阶段一:Try,逐个尝试预留资源        for svc in self.services:            if svc.try_op(self.tx_id):                self.log.append("Try OK    -> %s" % svc.name)            else:                self.log.append("Try FAIL  -> %s" % svc.name)                self._cancel_all()                return "CANCELED"         # 阶段二:Confirm,全部成功才真正执行        for svc in self.services:            svc.confirm_op(self.tx_id)            self.log.append("Confirm   -> %s" % svc.name)        return "CONFIRMED"     def _cancel_all(self):        for svc in self.services:            svc.cancel_op(self.tx_id)            self.log.append("Cancel    -> %s" % svc.name)  if __name__ == "__main__":    services = [StockService(), PointService()]     # 场景一:所有服务预留成功,事务正常确认    tx1 = TCCTransaction("T001", services)    print("事务1结果:", tx1.execute())     # 场景二:积分服务预留失败,触发库存服务取消补偿    PointService.fail_next = True    tx2 = TCCTransaction("T002", services)    print("\n事务2结果:", tx2.execute())     print("\n事务2日志:")    for line in tx2.log:        print("  " + line)
目的:实现一个最小可运行的 TCC 分布式事务模拟器,通过库存服务和积分服务两个参与者,演示“全部 Try 成功 → Confirm”和“Try 失败 → Cancel 补偿”两条完整路径。
  1. self.free -= 1` / `self.reserved += 1库存服务在 Try 阶段把“可用库存”转为“预占库存”,资源只是换了个状态,并没有真正扣减。
  2. if svc.try_op(self.tx_id)协调器逐个调用服务的 Try 接口,任何一个返回 False 就立刻进入取消分支。
  3. self._cancel_all()Try 失败时调用所有服务的 Cancel 接口,把之前预占的资源全部释放回可用状态。
  4. svc.confirm_op(self.tx_id)只有所有 Try 都成功后才会执行 Confirm,把预占资源真正落账。
  5. PointService.fail_next = True模拟积分服务在第二次事务时“临时故障”,用真实可运行的方式展示 Cancel 补偿路径。

常见误区

误区“TCC 这么厉害,事务肯定是强一致的吧?我在 Try 阶段预留了资源,之后数据不就是立刻统一的吗?”

正解TCC 保证的是最终一致性,而非强一致。在 Try 执行完、Confirm 未执行的窗口期,各个服务的数据是处于中间状态的——库存被冻结但订单还没生成。此时外部查询看到的数据是“暂时不一致”的。这就像团建报名期间,有人已经报了名但餐厅名单还没更新,中间存在时间差。强一致需要 2PC 这类同步协议,代价是性能和可用性下降。

为何常见:人们看到“失败自动补偿”很容易误以为系统是强一致的,因为从结果看最终状态总是对的,但忽略了过程中短暂的中间状态。

误区“Cancel 还不简单?Try 加了库存就减回去,Try 减了积分就加回来,对称操作就完事了。”

正解真实世界的 Cancel 没这么天真。你需要考虑:Try 超时没执行成功,Cancel 却到了(空回滚);Cancel 和 Try 的网络包乱序到达(悬挂);Cancel 本身被重复调用(幂等)。一个健壮的 Cancel 必须先查事务状态,确认自己真的预留过资源,才能执行释放逻辑,并且要能容忍空回滚场景。就像取消团建座位时,你得先查报名表确认这人确实占过位,不能闭着眼睛打电话。

为何常见:从数据结构上看,预占和释放确实是反向操作,初学者容易把分布式问题简化成“加减法”,忽略了网络时序带来的边界情况。

误区“TCC 性能好、可用性高,那我把所有分布式事务都改成 TCC 不就行了?2PC 直接淘汰。”

正解两者适用场景完全不同。如果业务对强一致有硬性要求——比如银行余额查询必须实时准确、跨行转账不能出现瞬时对不上——2PC 或 XA 协议依然不可替代。TCC 适合的是“允许短暂中间状态、只需最终一致”的业务,比如电商订单、积分累计。另外 TCC 需要为每个服务写三套接口,开发成本很高,对简单的一次性事务性价比反而低。就像百人团建可以线上报名,但一百人的升旗仪式就得现场点名——不同场合需要不同的方案。

为何常见:对比文章中通常强调 TCC 的种种优势,读者容易产生“新方案取代旧方案”的线性思维,忽略了工程选型从来都是条件判断而非技术迭代。

误区“接口调用成功了就完事了,写日志只会增加延迟,等出问题再说吧。”

正解事务日志是 TCC 可靠性的生命线。协调器宕机后,只有通过日志才能知道哪些服务 Try 成功了、哪些 Confirm 还没执行、哪些 Cancel 需要补发。没有日志,一个节点宕机就可能让整个事务卡在中间状态,永远无法恢复。日志的写入时机也很讲究:必须在发起调用之前记录“准备调用”,收到结果后再记录“调用结果”。就像小王每次打电话都要先在日程表记一笔“准备打给谁、什么事”,打完再勾掉——这才能保证中途断电也不会忘事。

为何常见:单机开发中日志只是排查手段,但在分布式系统里,日志从“辅助工具”变成了“恢复系统运行的核心数据”,这个角色转变常被忽视。

误区“请求发出去等了两秒没响应,那肯定是失败了,直接 Cancel 吧,干脆利落!”

正解超时≠失败。响应丢失的情况下,服务端可能已经执行成功了。此时直接 Cancel 会带来双重风险:服务端既执行了操作又要执行补偿,可能造成数据错乱。正确的做法是:超时后先通过查询接口确认真实状态,再决定是继续 Confirm 还是触发 Cancel。这就像小王打电话给餐厅确认人数,电话没接通不能直接取消预订——得让前台回个电话确认清楚再说。

为何常见:在单体应用中,请求超时基本等同失败;但在不可靠的网络上,“没收到响应”和“处理失败”是两种完全不同的情况,新手很容易沿用单体思维。

面试题

Q1. TCC 模式中,Try 阶段的核心任务是什么?

单选题 · easy

  • A.直接提交事务并释放资源
  • B.检查业务并预留资源← 正确答案
  • C.删除事务所有操作记录
  • D.通知所有服务立即执行

答案:B。Try 阶段的核心是“业务检查 + 资源预留”,即保证事务将来可执行但不立即生效。A 是 Confirm 的动作;C 是无意义操作;D 是跳过预留直接执行,完全违背 TCC 的设计初衷。就像团建报名阶段只登记意向、不真正订餐。

Q2. 以下哪种情况会触发 TCC 的 Cancel 阶段?

单选题 · easy

  • A.所有服务的 Try 全部成功
  • B.某个服务的 Try 返回失败← 正确答案
  • C.所有服务的 Confirm 都完成
  • D.协调器收到一个 Confirm 结果

答案:B。只要有一个服务的 Try 返回 FAIL,协调器就必须触发 Cancel,对所有已预留资源的服务进行补偿。A 是进入 Confirm 的条件;C 和 D 是事务已走向成功路径的标志,不需要补偿。这就像有人临时说“来不了”,就得取消他为预留的座位。

Q3. 关于 Confirm 阶段的说法,正确的是?

单选题 · mid

  • A.需要加分布式锁保证串行
  • B.使用预留资源真正执行业务← 正确答案
  • C.必须人工确认后才能执行
  • D.可以从业务表中删除数据

答案:B。Confirm 的作用是把 Try 阶段预留的资源正式转为实际业务操作——冻结变扣减、锁定变入账。A 错误:TCC 的价值之一就是不加锁;C 错误:Confirm 由协调器自动触发;D 错误:Confirm 是写正式数据,不是删数据。就像人到齐后通知餐厅真正开席。

Q4. TCC 相比 2PC 的优势有哪些?

多选题 · mid

  • A.事务粒度更细,支持并行← 正确答案
  • B.不在数据库层面加锁阻塞← 正确答案
  • C.可用业务逻辑实现补偿← 正确答案
  • D.完全避免最终一致性难题

答案:ABC。TCC 把锁从数据库层上移到业务层,以预占+补偿实现最终一致,支持更高的并行度。但 D 错误:TCC 并没有完全避免最终一致性,它本身就是靠“允许短暂不一致+最终补偿一致”来工作的。就像线上报名比现场表决更灵活,但不能说它解决了所有同步问题。

Q5. 什么是 TCC 中的“空回滚”?

单选题 · mid

  • A.Try 失败时不做任何补偿
  • B.对未执行过 Try 的业务执行 Cancel← 正确答案
  • C.回滚时删除数据库的所有记录
  • D.Confirm 阶段执行直接回滚

答案:B。空回滚指由于 Try 超时等原因,协调器不确定服务是否预留成功,直接对该服务执行 Cancel,而实际上该服务从未预留过任何资源。Cancel 操作必须能识别并安全跳过。其他选项描述的都不是空回滚的准确定义。就像给没报名接龙的人打电话说“你不用来了”。

Q6. 为避免 TCC 悬挂问题,以下哪些措施有效?

多选题 · hard

  • A.记录状态,Try 成功后才接受 Cancel← 正确答案
  • B.未完成 Confirm 则拒绝执行 Cancel← 正确答案
  • C.Cancel 前检查事务日志中的状态← 正确答案
  • D.用同步调用取代所有异步消息

答案:ABC。悬挂问题的本质是“Cancel 后的迟到 Try 结果又触发了 Confirm”。通过事务日志记录状态、在状态机中拦截非法迁移,可以确保 Cancel 之后的 Try 结果被丢弃。D 错误:同步调用只改变通信方式,不能解决网络乱序的本质问题。就像座位取消了,迟到的“报名成功”回复也不能再让人入席。

Q7. 幂等控制通常使用什么机制实现?

单选题 · easy

  • A.唯一事务 ID 加去重表← 正确答案
  • B.全局自增主键作为标识
  • C.每次调用都使用随机数
  • D.服务间共享一张业务表

答案:A。标准做法是生成唯一事务 ID,服务端用去重表记录已处理的事务 ID,重复请求直接查表返回历史结果。B 的自增主键无法跨服务唯一标识事务;C 的随机数无法去重;D 共享业务表破坏了服务自治。就像报名表上记了名字,重复接龙就不会再登记一次。

Q8. 如果一个服务的 Try 成功但 Confirm 阶段超时,正确做法是?

单选题 · mid

  • A.标记失败,直接调用 Cancel
  • B.查询事务状态并重试 Confirm← 正确答案
  • C.忽略超时等待下一次事务
  • D.立即回滚整个数据库

答案:B。Try 已成功说明资源已被预占,此时 Confirm 超时可能是网络抖动或响应丢失,服务端可能已执行成功。正确做法是查询真实状态,若未执行则重试 Confirm,直到成功。直接 Cancel 可能造成“既执行又补偿”的双重破坏。就像确认开席的电话没打通,不能直接取消座位,得先确认厨房到底有没有开炒。

Q9. 事务日志在 TCC 中的作用是什么?

多选题 · mid

  • A.记录每个阶段的执行状态← 正确答案
  • B.记录回滚所需的业务上下文← 正确答案
  • C.直接替代数据库的事务日志
  • D.用于恢复服务间的网络连接

答案:AB。事务日志记录事务在各个阶段的状态变化以及回滚所需的业务上下文,宕机后据此恢复未完成的事务。C 错误:业务级事务日志无法替代数据库底层日志;D 错误:日志不能恢复网络连接,只能辅助协调器做状态决策。就像团建的签到表,记录谁报名了、谁取消了,而不是用来修电话线。

Q10. 关于 TCC 最终一致性的实现,下列说法最准确的是?

单选题 · extreme

  • A.Try 阶段负责保证一致性
  • B.Confirm 阶段负责保证一致性
  • C.Cancel 阶段负责保证一致性
  • D.所有阶段共同保证一致性← 正确答案

答案:D。TCC 的最终一致性是三个阶段协作的结果:Try 确保可执行性,Confirm 确保正确提交,Cancel 确保失败补偿。缺少任何一环,事务要么悬挂、要么资源泄漏,都无法收敛到一致状态。三阶段各自输出的确定性加上状态机的正确流转,才共同构成了最终一致性的保证。就像一场团建的圆满结束,报名、确认、取消三个环节缺一不可。