你打开演唱会购票 App,抢到一张心仪已久的门票。表面上看,你只是点了几下屏幕,可背后,三个独立的业务系统正在你追我赶:票务系统要锁住一个座位,支付系统要从你的银行卡里扣钱,会员系统要给你的账户加积分——它们各自维护着自己的数据库,谁也没法替别人做主。 如果这三个动作必须“永不反悔”地一起成功,可就难了:网络抖动一下、服务器卡顿一秒,都可能出现“座位锁了、钱没扣成”的尴尬局面。聪明人想出一个办法:把“买票”拆成三步——先占住座位但不出票(Try),确认无误后再出票扣款(Confirm),任何一步不顺就把座位放回去(Cancel)。这三步合起来,就是今天要讲的 TCC 补偿事务。它不追求所有系统“同时”成功,而是保证最终大家能对得上账——这就是最终一致性。

岗位关联

一句话价值:掌握 TCC,你就掌握了微服务架构下跨库数据最终一致性的核心武器,是胜任资深后端与 AI 平台工程师的必备技能。

目录
  1. 可视化演示
  2. 分布式困境
  3. 三阶段原理
  4. 幂等设计
  5. 异常处理
  6. 落地实践
  7. 场景与权衡
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

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

TCC补偿事务三阶段流程 Try阶段预留资源(冻结余额、预扣库存),不真正执行业务操作 Confirm阶段确认执行消费预留资源;Cancel阶段释放预留回滚 空回滚处理Try未到达异常,悬挂防护避免Cancel后Try迟到 TCC通过业务层补偿实现最终一致性

分布式困境

演唱会门票销售从线下搬到线上后,事情变得复杂了。你在官方网站上买票,看似一次点击,背后其实是三个“收银台”在协作:票务柜台负责锁定座位,支付柜台负责从卡里扣钱,账户柜台负责给会员积分加分。更麻烦的是,这三个柜台各自拥有一本完全独立的账本,谁也管不了谁。一旦网络抽风,就会出现“票务柜台划掉了座位,支付柜台却说钱没扣成”的扯皮场面。

在传统单体系统里,一个客户买票,减库存、扣余额、加积分都在同一个数据库里完成。数据库提供的 ACID 事务——原子性、一致性、隔离性、持久性——可以保证这组操作要么全部成功、要么全部失败。可一旦拆成微服务,三个操作落在三个不同的数据库里,本地事务就“手伸不到别人家”了。这就是分布式事务要解决的难题:如何让多个独立数据库的操作最终保持一致。

为什么跨库这么难?因为网络会超时、服务会宕机、消息会丢失,任何一个环节出错,其他环节已经执行的操作都无法自动撤回。假设每个独立环节的成功概率都是 p,一个需要 n 个环节的事务,整体成功率会快速衰减:环节越多,失败概率越高,数据不一致的风险就越大。这正是分布式环境下“越拆分越脆弱”的数学真相。

这就像三个柜台各记各的账:票务柜台已经划掉了座位,支付柜台却说“网络断了,钱没扣成”,两边账本对不上,你说该信谁?TCC 的出现,就是给这三个柜台立一套统一的“对账规矩”,让它们在同一套规则下协作而不是互相甩锅。

Psuccess=∏i=1npi=pn\displaystyle P_{\text{success}} = \prod_{i=1}^{n} p_i = p^n
这个公式告诉我们:跨库事务的成功率等于所有环节成功率的连乘。环节越多,整体成功率越低,所以必须有一套专门机制来兜底,而不是指望运气。
  1. P_success(整体成功率)整个跨库事务从第一笔操作到最后全部成功的概率,相当于三个柜台全部不出错的概率。
    PsuccessP_{\text{success}}
  2. 连乘符号 ∏把 n 个环节的成功率逐一相乘,表示“每个环节都必须成功,缺一不可”。
    ∏i=1npi\prod_{i=1}^{n} p_i
  3. p_i(单环节成功率)第 i 个独立环节(如库存锁定、余额扣减)的成功概率,通常由单机稳定性决定。
    pip_i
  4. p^n(指数衰减)当所有环节成功率同为 p 时,整体成功率等于 p 的 n 次方。n 越大数字越小,说明环节越多越脆弱。
    pnp^n

三阶段原理

这套规矩的核心,是把一次“买票”拆成三步:先把座位“占住”但不出票(Try),占住了再正式“出票”(Confirm),占不住或你自己放弃,就“放回去”(Cancel)。注意,这三步不是数据库自动完成的,而是每个业务系统都要亲手实现的三段代码。Try 是试探,Confirm 是拍板,Cancel 是变卦。

对应到技术上,TCC 要求每个参与方都实现 Try、Confirm、Cancel 三个方法。Try 阶段做资源预留——库存服务冻结 1 件商品、钱包服务预扣 99 元,但商品还没真的卖出去、钱还没真的划走。Confirm 阶段把预留变成现实——库存减一、钱包减钱。Cancel 阶段把预留归还——库存加回、钱包退款。整个过程由事务管理器(协调者)统一编排。

TCC 的关键在于 Try 这道“保险闸门”:无论后续走 Confirm 还是 Cancel,都在 Try 的预留基础上操作,绝不会出现“超卖”或“重复扣款”。只有当所有参与方的 Try 都成功,协调者才统一发起 Confirm;只要有一个 Try 失败,或业务中途取消,协调者就对已成功的参与方逐个发起 Cancel。这可以用一个条件决策式表达:

三兄弟里,Try 是最稳重的那个——它从不真正“搞定”任何事,只负责把位置占住;Confirm 和 Cancel 则像一枚硬币的两面,只能抛出一面,绝不会同时出现。正是这种“先占位、再决定”的设计,让 TCC 能在不锁数据库的情况下保证最终一致。

Result={Confirm(Try(s))if Try(s) succeedsCancel(Try(s))if Try(s) fails or aborts\displaystyle Result = \begin{cases} Confirm(Try(s)) & \text{if } Try(s) \text{ succeeds} \\ Cancel(Try(s)) & \text{if } Try(s) \text{ fails or aborts} \end{cases}
这个公式是 TCC 的总开关:Try 成功则走 Confirm,Try 失败或业务中止则走 Cancel,两条路互斥且必选其一。
  1. Try(s)用业务参数 s 调用每个参与方的预留方法,比如“锁定座位 2 张”,此时数据处于冻结态而非生效态。
    Try(s)Try(s)
  2. if Try(s) succeeds(Try 成功)所有参与方都预留成功,协调者拍板走 Confirm 分支。
    if Try(s) succeeds\text{if } Try(s) \text{ succeeds}
  3. Confirm(Try(s))在预留的资源上执行确认,把“冻结 2 张”变成“已售 2 张”,业务真正生效。
    Confirm(Try(s))Confirm(Try(s))
  4. Cancel(Try(s))当任一 Try 失败或业务中途取消时,对已预留资源执行释放,退回到初始状态。
    Cancel(Try(s))Cancel(Try(s))

幂等设计

假设你在付款页面网络卡了一下,手指一急连点了三次“确认支付”。你是该被扣三笔钱,还是只扣一笔?显然,无论点多少次,同一笔订单只能出一张票、扣一次钱。这个“多点几次和点一次结果一样”的性质,就叫幂等。它是 TCC 能在不可靠网络上生存的基石。

分布式环境下,消息重发是家常便饭:Confirm 请求可能在网络上走丢了,协调者自动重试;Cancel 请求也可能重复投递。如果参与方不做好幂等,就会出现“同一张票被出两次”或“同一笔退款退两遍”的灾难。所以 TCC 的每个方法都必须能识别“这单我是不是已经处理过了”。

实现幂等要靠两件东西:一是全局唯一的事务 ID,相当于每笔交易的“身份证号”;二是状态机校验——参与方在执行业务前,先查本地事务记录:如果该事务 ID 已经处于终态(已提交或已取消),直接返回成功,不再重复执行。用数学语言说,业务操作 f 满足执行两次等同执行一次:

这就像自助售货机:你投了硬币,多拍几下“出货”按钮,它也只会掉一罐饮料——因为机器内部早就给这笔交易盖了“已出货”的章。TCC 中的事务控制表就是这个“章”,一盖下去,重复请求统统放行,但绝不重复执行。

f(f(x))=f(x)\displaystyle f(f(x)) = f(x)
幂等的数学定义:同一个操作做一次和做无数次,结果完全一样。有了它,网络重试、消息重复投递都不会破坏数据。
  1. f(x)(一次业务操作)对数据 x 执行一次完整业务动作,例如对订单 1001 扣款 99 元。
    f(x)f(x)
  2. f(f(x))(重复执行)用同样的参数再执行一次,模拟网络重试、消息重复投递的真实场景。
    f(f(x))f(f(x))
  3. 等式成立(幂等)两次执行的结果与一次执行完全一致,说明该操作满足幂等性,不会重复生效。
    f(f(x))=f(x)f(f(x)) = f(x)

异常处理

分布式世界里总有“幽灵事件”。你明明点了取消,系统却回你一句“取消失败,因为没找到你的订单”。其实不是系统傻了,而是 Cancel 请求到达时,Try 可能还没来得及执行——这在 TCC 中叫“空回滚”:没有预留可释放,取消却已经来了。这是合法路径,不是 BUG。

与空回滚对应的还有“悬挂”:Confirm 已经成功完成,但因为响应超时,发起方以为它失败了,于是发出 Cancel。如果 Cancel 真的去“补偿”一笔已经确认的交易,就会造成账目错乱。此外还有 Try 卡死、网络分区等超时场景。这些异常的本质,都是消息到达顺序与真实执行顺序不一致。

处理这些异常的核心手段是“记账 + 拦路”。每个参与方用事务控制表记录该事务 ID 的完整状态流转:收到 Cancel 时先查表,如果根本没有 Try 成功的记录,就静默返回成功(空回滚);如果状态已是终态,就拦截迟到的 Cancel 消息(防悬挂)。竞态窗口的大小可以用下面这个式子刻画,它决定了悬挂风险的时间跨度:

对付幽灵最好的办法,是让每一个操作都留下记录。柜台翻一翻账本,发现“你这单根本没占座”,直接把你的取消请求当作“已办结”处理——不纠缠、不报错,事情就平息了。这就是异常处理的艺术:不是消灭所有异常,而是让异常在规则下安全落地。

Δthang=max⁡(0,tcancel−tconfirm)\displaystyle \Delta t_{\text{hang}} = \max(0, t_{\text{cancel}} - t_{\text{confirm}})
这个公式刻画了“悬挂窗口”:Cancel 发出时刻减去 Confirm 完成时刻。差为正,说明 Cancel 晚到,存在悬挂风险;差为负,说明 Cancel 反而早到,风险为零。
  1. t_confirm(确认完成时刻)Confirm 操作真正落库并记为终态的那一刻,是数据生效的分水岭。
    tconfirmt_{\text{confirm}}
  2. t_cancel(取消发出时刻)发起方因为超时等原因发出 Cancel 请求的那一刻。
    tcancelt_{\text{cancel}}
  3. 差值(竞态窗口)Cancel 发出时间减去 Confirm 完成时间。正数意味着 Cancel 晚于 Confirm 到达,存在悬挂风险。
    tcancel−tconfirmt_{\text{cancel}} - t_{\text{confirm}}
  4. max(0, ·)(取非负)时间差为负数时取 0,表示 Cancel 到来之前 Confirm 已确认,悬挂窗口闭合。
    max⁡(0,Δt)\max(0, \Delta t)

落地实践

实操中,TCC 需要一本“总台账”来协调所有参与方,这就是事务控制表。每笔交易一行记录:事务 ID、参与方列表、当前状态(初始、已 Try、已 Confirm、已 Cancel)、操作时间。任何一个参与方想动数据,先查台账,再按状态机规则流转——先 Try 再 Confirm 或 Cancel,绝不能凭空起飞。

状态机规定了合法流转路径:初始 → 已 Try → 已 Confirm,以及初始 → 已 Try → 已 Cancel。任何偏离路径的请求都被视为非法,直接拒绝。比如收到 Confirm 但状态还是“初始”,说明消息乱序或伪造,必须拦截。这种“查表 + 校验”的模式,让消息即使乱飞,数据也走不歪。

此外还要做并发控制。同一件商品可能被两个用户同时 Try,必须用唯一索引加状态字段或乐观锁(版本号)保证同一资源只能被一个事务预留。对长时间停留在“已 Try”中间态的事务,要配置监控告警,由定时任务自动补偿或人工介入。整套机制可以用一个状态转移函数抽象描述:给定当前状态和到达事件,得到合法的新状态。

有了总台账和状态机,整个系统就像装上了红绿灯和监控探头:每一辆“事务列车”都按轨道行驶,一旦有车在中间路段滞留,系统立刻报警,调度员马上人工处理。这样,TCC 就不再是纸面理论,而是可以稳定运行的工程系统。

Snext=g(Scurrent,E)\displaystyle S_{\text{next}} = g(S_{\text{current}}, E)
状态转移函数:TCC 的每个参与方都是一个有限状态机,事件只有通过状态机的合法校验,才能推动事务进入下一状态。
  1. S_current(当前状态)事务在控制表中的当前状态,如初始、已 Try 等。
    ScurrentS_{\text{current}}
  2. E(触发事件)到达的消息或超时信号,如 Try 成功、Confirm 请求、Cancel 请求。
    EE
  3. g(状态转移函数)根据当前状态与事件查状态机表,得到合法的下一状态,非法事件直接拒绝。
    g(Scurrent,E)g(S_{\text{current}}, E)
  4. S_next(下一状态)状态机校验通过后事务进入的新状态,如已 Confirm、已 Cancel。
    SnextS_{\text{next}}

场景与权衡

TCC 虽好,却不是万能药。买一瓶矿泉水扫码支付,一个数据库本地事务就够了,硬要拆成 Try-Confirm-Cancel,反而是杀鸡用牛刀。TCC 的真正主场,是那些要跨库修改“稀缺资源”的业务:库存会不够、余额会不足、票数会耗尽——这些资源一旦超卖或透支,后果很严重。

适合 TCC 的业务有三个特征:资源型(库存、余额、配额)、强约束(不能超卖、不能透支)、可补偿(失败后能释放资源)。纯查询、只读报表、日志收集这类操作没有“资源锁”的概念,就完全不需要 TCC。识别这个边界,才能避免过度设计。

TCC 与 2PC 的取舍也很清晰:2PC 让协调者在 prepare 阶段持有数据库锁直到 commit,性能差、可用性低;TCC 把 try 阶段的锁换成业务层面的“冻结”,长事务期间不占数据库锁,性能更好,但代价是每个参与方都要手写三套业务方法,开发成本高。整体耗时大致等于 Try 阶段耗时,加上 Confirm 与 Cancel 二者中较长的一条路,再加上网络往返开销:

所以记住这句口诀:能用账本自己解决的事,别惊动三兄弟;只有当多个独立账本必须保持步调一致时,才请 Try、Confirm、Cancel 出山。选型没有绝对的好,只有合不合适——TCC 为“强资源约束 + 高并发”而生,它追求的不是万无一失的强一致,而是实用的最终一致。

TTCC=Ttry+max⁡(Tconfirm,Tcancel)+Tnetwork\displaystyle T_{\text{TCC}} = T_{\text{try}} + \max(T_{\text{confirm}}, T_{\text{cancel}}) + T_{\text{network}}
TCC 的耗时模型:Try 只做预留很快,Confirm 和 Cancel 二选一走一条,真正的开销主要在多次网络往返上。比起 2PC 的锁库等待,这个成本要低得多。
  1. T_try(预留阶段耗时)所有参与方 Try 串行或并行预留资源的总耗时,只做冻结不落死账。
    TtryT_{\text{try}}
  2. max(T_confirm, T_cancel)(二选一耗时)Confirm 与 Cancel 只会走其中一条,取两者中较大者代表最终完成路径的耗时。
    max⁡(Tconfirm,Tcancel)\max(T_{\text{confirm}}, T_{\text{cancel}})
  3. T_network(网络往返开销)三次调用之间网络传输与协调者的调度开销,在分布式环境下不可忽略。
    TnetworkT_{\text{network}}

AI 应用场景

电商订单履约:下单、锁库存、扣款、加积分一把梭

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

解决的问题:下单涉及库存、钱包、积分三个微服务各写各的库。同一笔订单,如果库存扣了但扣款失败,或扣款成功但积分没加,都会造成资损或用户体验下降。传统无分布式事务的方案只能靠售后对账人工修复;用 TCC 后,三个服务先各自预留,全部成功才真正生效,任一失败自动补偿回初始态,账实始终对得上。

具体案例:某电商大促 1 秒售出 3 万单。用户买一台 6999 元的手机:库存服务 Try 冻结 1 台,钱包服务 Try 预扣 6999 元,积分服务 Try 预加 699 积分。三个 Try 全部成功,协调器统一发起 Confirm;若钱包余额不足导致 Try 失败,协调器自动对已成功的库存服务发 Cancel,释放库存。

AI 平台多租户资源计费与配额结算

阶段:分布式任务结算

解决的问题:AI 训练平台里,一次任务同时消耗 GPU 机时、对象存储容量和账号余额。任务跑完要同时扣三本账,任何一个扣费服务超时都会导致记账不平,影响对账与成本核算。TCC 让每个计费参与方先预留再确认,任务失败时统一回滚,保证平台账实相符。

具体案例:某 MLOps 平台每天调度 5000 个训练任务。任务结束时计费系统 Try 冻结费用、配额系统 Try 扣减 GPU 配额、存储系统 Try 记录用量;全部 Try 成功则 Confirm,任务失败重跑则全部 Cancel 回滚配额与费用,月末对账零差异。

Python 演示

python104 行
# -*- coding: utf-8 -*-"""TCC 补偿事务简易模拟器: 用演唱会抢票演示三阶段流程"""import uuid  class TicketService:    """票务服务: 管理库存, 提供 Try/Confirm/Cancel 三阶段能力"""     def __init__(self, total=100):        self.total = total        self.frozen = 0        self.sold = 0        self.tx_records = {}  # 事务ID -> 状态     def try_op(self, tx_id, num):        if self.tx_records.get(tx_id) == "TRIED":            return True  # 幂等: 同一事务的重复 Try 直接返回成功        if self.total - self.sold - self.frozen < num:            return False  # 库存不足, 预留失败        self.frozen += num        self.tx_records[tx_id] = "TRIED"        return True     def confirm(self, tx_id, num):        if self.tx_records.get(tx_id) == "CONFIRMED":            return True  # 幂等: 已确认过的订单不重复处理        if self.tx_records.get(tx_id) != "TRIED":            return False  # 状态校验: 只有 Try 成功后才能 Confirm        self.frozen -= num        self.sold += num        self.tx_records[tx_id] = "CONFIRMED"        return True     def cancel(self, tx_id, num):        if self.tx_records.get(tx_id) != "TRIED":            return True  # 空回滚: Try 未执行就收到取消, 视为成功        self.frozen -= num        self.tx_records[tx_id] = "CANCELED"        return True  class PaymentService:    """支付服务: 模拟预扣款与退款"""     def __init__(self, balance=100000):        self.balance = balance        self.pay_records = {}     def try_op(self, tx_id, amount):        if self.pay_records.get(tx_id) == "TRIED":            return True        if self.balance < amount:            return False        self.balance -= amount  # 预扣到虚拟冻结账户        self.pay_records[tx_id] = "TRIED"        return True     def confirm(self, tx_id, amount):        if self.pay_records.get(tx_id) == "CONFIRMED":            return True        if self.pay_records.get(tx_id) != "TRIED":            return False        self.pay_records[tx_id] = "CONFIRMED"  # 预扣已生效, 只更新状态        return True     def cancel(self, tx_id, amount):        if self.pay_records.get(tx_id) != "TRIED":            return True  # 空回滚: 未预扣成功, 无需退款        self.balance += amount  # 退还预扣款        self.pay_records[tx_id] = "CANCELED"        return True  def run_tcc(services, params, fail_step=None):    """编排 TCC 流程: 全部 Try 成功则 Confirm, 否则逆序 Cancel"""    tx_id = uuid.uuid4().hex[:8]    tried = []    for i, (service, param) in enumerate(zip(services, params)):        if fail_step is not None and i == fail_step:            break  # 模拟第 i 步 Try 阶段网络异常        if not service.try_op(tx_id, param):            break        tried.append((service, param))    else:        for service, param in tried:            service.confirm(tx_id, param)        return tx_id, "CONFIRMED"    for service, param in reversed(tried):        service.cancel(tx_id, param)  # 补偿已成功的参与方    return tx_id, "CANCELED"  if __name__ == "__main__":    ticket = TicketService(total=100)    payment = PaymentService(balance=100000)    print("== 场景1: 正常购票 ==")    _, st = run_tcc([ticket, payment], [2, 1000])    print("状态:", st, "| 已售:", ticket.sold, "| 冻结:", ticket.frozen, "| 余额:", payment.balance)    print("== 场景2: 库存不足 ==")    _, st = run_tcc([ticket, payment], [200, 1000])    print("状态:", st, "| 已售:", ticket.sold, "| 冻结:", ticket.frozen, "| 余额:", payment.balance)    print("== 场景3: 支付环节 Try 失败 ==")    _, st = run_tcc([ticket, payment], [2, 1000], fail_step=1)    print("状态:", st, "| 已售:", ticket.sold, "| 冻结:", ticket.frozen, "| 余额:", payment.balance)
目的:用两个带 Try/Confirm/Cancel 方法的服务类,模拟 TCC 协调器在正常、库存不足、支付失败三种场景下的完整处理流程。
  1. if self.total - self.sold - self.frozen < num:检查当前可售余量,只有真正有余票才允许冻结。
  2. self.frozen += numTry 阶段只增加冻结数,不减少总量,这就是“预留而非消耗”。
  3. if self.tx_records.get(tx_id) == "CONFIRMED": return True已确认过的订单直接返回,实现 Confirm 幂等。
  4. if self.tx_records.get(tx_id) != "TRIED": return True空回滚处理:Try 从未成功就收到取消,静默成功。
  5. self.balance -= amount # 预扣到虚拟冻结账户支付 Try 先把钱从可用余额划走,相当于“冻结资金”。
  6. tried.append((service, param))记录所有 Try 成功的参与方,为后续统一 Confirm 或补偿 Cancel 做准备。
  7. for service, param in reversed(tried): service.cancel(tx_id, param)逆序补偿:先回滚后成功的参与方,避免依赖顺序问题。

常见误区

误区“2PC 就是 prepare/commit 两个阶段,TCC 就是 Try/Confirm 两个阶段,不就是换个名字嘛?”初学者常把两者混为一谈。

正解两者本质不同。2PC 是数据库层面的 XA 协议,协调者在 prepare 阶段让所有库持有锁并同步等待,任何一台机器失联整个事务就会卡死;TCC 的三个阶段全部在业务代码里实现,Try 阶段只用“冻结”这类业务动作预留资源,不持有数据库锁,参与方之间也无需同步阻塞,灵活性和性能都更好。就像 2PC 是“全场拉闸停电、统一宣布结果”,TCC 是“各家先占座、后单独办理”。

为何常见:两者都是“先尝试、后决定”的两段式流程,阶段数量相同,概念上容易先入为主。

误区“Try 失败了直接 rollback 不就完了吗?为什么还要自己写 Cancel?”很多入门开发者把 TCC 的 Cancel 等同于数据库回滚。

正解本地事务的 rollback 是数据库自动撤销未提交的修改;而 TCC 中 Try 阶段的“预留”是一个独立的、已提交的业务操作,数据库并不理解“冻结”和“扣减”之间的业务关系。Cancel 必须由开发人员编写反向业务逻辑,比如把冻结的库存加回去、把预扣的钱退回来,属于业务补偿而非机械回滚。

为何常见:习惯了单体数据库事务的自动回滚语义,忽略了分布式环境下没有全局 undo 机制。

误区“三个服务的 Try 都成功了,这笔交易肯定成了,Confirm 随便出个票就行。”很多初学设计者把 Confirm 当成可有可无的摆设。

正解Confirm 同样会因网络超时、服务宕机、消息丢失而失败。因此 Confirm 也必须具备幂等性,配合消息队列与重试机制确保最终完成;如果重试后仍失败,还要触发补偿流程(Cancel 部分已确认的资源)或人工介入,绝不能默认“Try 成功即提交成功”。

为何常见:Try 是资源能否预留的关键,容易让人误以为后面只是走个过场。

误区“收到 Cancel 却查不到 Try 记录,这不是系统 BUG 吗?应该抛异常让上层知道!”——把空回滚当作程序故障。

正解空回滚是 TCC 的合法语义。Try 请求可能因网络超时从未到达参与方,但协调器超时后已发起 Cancel。此时参与方应当把取消当作“该事务从未发生过”来静默成功,确保事务快速终结;如果抛异常,协调器会反复重试,最终造成悬挂或死锁。

为何常见:本地事务中“操作不存在就是异常”,但分布式环境下“消息乱序”本身就是常态,需要换一套思路。

面试题

Q1. TCC 的 Try 阶段核心目标是什么?

单选题 · easy

  • A.直接完成业务扣减
  • B.预留资源但不生效← 正确答案
  • C.释放所有已锁定资源
  • D.提交整个分布式事务

答案:B。Try 阶段的核心是“冻结”而非“扣减”,比如锁住座位但不真正出票。选 B。A 是 Confirm 的职责,C 是 Cancel 的职责,D 是协调者的整体职责。

Q2. TCC 的 Confirm 阶段执行条件是什么?

单选题 · easy

  • A.所有参与方 Try 全部成功← 正确答案
  • B.任意一个 Try 成功即可
  • C.由数据库自动触发
  • D.提交时可绕过 Try 直接执行

答案:A。只有所有参与方的 Try 都成功,协调者才会统一发起 Confirm。就像三个柜台都完成了“占座”,才能正式出票。B 缺一不可,C 是 2PC 的误区,D 违背状态机规则。

Q3. 以下哪个场景最适合使用 TCC?

单选题 · easy

  • A.大数据平台离线报表聚合
  • B.订单库存扣减和账户余额扣款← 正确答案
  • C.搜索引擎索引的批量重建任务
  • D.静态前端资源的 CDN 缓存刷新

答案:B。TCC 适合“资源型、强约束、可补偿”的业务,库存和余额正是典型。A、C、D 都是无资源锁定的计算或缓存任务,用本地操作即可,不需要三阶段。

Q4. “空回滚”指的是什么场景?

单选题 · mid

  • A.Try 成功后 Confirm 失败
  • B.取消时发现 Try 从未成功← 正确答案
  • C.Confirm 被重复执行两次
  • D.Cancel 释放了过多资源

答案:B。空回滚是 Cancel 到达时,事务控制表中没有 Try 成功的记录。此时应静默成功而非报错。A 是 Confirm 失败,C 是幂等问题,D 是补偿过度。

Q5. 幂等操作的数学性质是?

单选题 · mid

  • A.两次执行效果等同一次← 正确答案
  • B.两次执行效果互相抵消
  • C.多次执行结果趋于零
  • D.执行次数越多结果越大

答案:A。幂等要求 f(f(x)) = f(x),无论执行多少次结果都一样。B 是互逆操作,C 和 D 都违背了“结果稳定”的本质。

Q6. 为什么 TCC 的 Cancel 不能依赖数据库 rollback?

单选题 · mid

  • A.数据库引擎不支持回滚操作
  • B.Try 的改动已独立提交到业务库← 正确答案
  • C.Cancel 必须额外释放数据库锁
  • D.回滚会造成全局数据库死锁

答案:B。Try 阶段的“冻结”是已提交的业务操作,数据库不知道它是预留,无法自动撤销。Cancel 要靠开发人员写反向业务逻辑,把冻结释放。A、C、D 都是对数据库机制的误解。

Q7. 解决 TCC “悬挂”问题的关键手段是什么?

单选题 · hard

  • A.让 Cancel 消息无限次重试发送
  • B.用控制表和幂等键拦截迟到请求← 正确答案
  • C.完全禁止使用任何超时重试机制
  • D.提高 Try 成功率到百分之一百

答案:B。悬挂是迟到的 Cancel 与已完成的 Confirm 竞争。核心手段是事务控制表加状态机拦截:一旦状态是终态,迟到的 Cancel 直接忽略。A 会让问题更严重,C 和 D 不现实。

Q8. TCC 相比 2PC 的优势包括哪些?

多选题 · mid

  • A.业务层预留资源不长期锁库← 正确答案
  • B.协调者无需做任何超时处理
  • C.不依赖数据库级分布式锁← 正确答案
  • D.完全不考虑网络异常的情况

答案:AC。TCC 在业务层做冻结,不占数据库长锁(A 对);同样不依赖数据库 XA 锁(C 对)。但超时处理依然必要(B 错),网络异常也需要重试和补偿机制(D 错)。

Q9. 以下哪些属于 TCC 的典型异常场景?

多选题 · hard

  • A.空回滚:Try 从未成功却收到取消请求← 正确答案
  • B.悬挂:迟到的取消与已完成确认竞争← 正确答案
  • C.重复投递:同一条确认消息被重试多次← 正确答案
  • D.所有数据必须在同一个数据库内提交

答案:ABC。A 是空回滚,B 是悬挂,C 是消息重复,都是 TCC 必须处理的典型异常。D 描述的是单体数据库方案,与 TCC 无关。

Q10. 落地 TCC 时通常需要哪些配套机制?

多选题 · extreme

  • A.引入事务控制表记录每个事务状态← 正确答案
  • B.用状态机校验非法状态流转← 正确答案
  • C.对长期中间态事务配置监控告警← 正确答案
  • D.用全局分布式锁串行化全部操作

答案:ABC。事务控制表(A)、状态机校验(B)、监控告警(C)是 TCC 落地的三件套。D 用全局锁串行化所有操作会杀掉并发性能,违背 TCC 的设计初衷。