Skip to content

模块导航: 总入口 · 总纲与方法 · 后端场景 · AI与算法工程 · 国企与金融 · 网络问答


第五章 分布式与一致性(第97-122题) ​

本章导览(元问题与表面主题逐字取自 《核心考点与元理论》§六)

  • 本章真正考的那一个问题: 跨节点无法同时完美时,按什么业务代价排序?
  • 表面主题: CAP、分布式事务、ID、锁
  • 题号与题量: 第 97–122 题,共 26 题;刷题时不要按知识点记,要按上面那一个元问题记。

97. 注册中心选型会上 CP 和 AP 吵起来了(CAP 理论) ​

【考察内容】CAP 是分布式第一理论题

【题目】注册中心选型会上两派吵起来了:一派坚持必须 CP(强一致),另一派说 AP(高可用)就行。请先讲清楚 CAP 到底是什么、为什么三者不可兼得,再给出结论:注册中心/配置中心/交易系统分别该牺牲哪一项?为什么?

【参考答案】

  1. 定义:分布式系统最多同时满足两个——C(一致性:所有节点同一时刻看到同一数据)、A(可用性:每个请求都能收到非错误响应)、P(分区容错性:网络分区时系统仍能运行);
  2. 核心论证:网络分区(P)在分布式系统中必然发生(不可选择放弃),所以实际是在 C 和 A 之间取舍:
    • 选 CP:分区时拒绝服务(等待/报错)保证一致——如 ZooKeeper、etcd(选主期间不可用);
    • 选 AP:分区时继续服务(返回可能过期的数据)——如 Redis 集群部分场景、Eureka、DNS;
  3. 注意:CAP 只刻画「分区期间 C 与 A 不可兼得」,对分区恢复后能否收敛不做任何保证——收敛是要额外实现的属性(Eureka/Nacos 靠续约与重新注册收敛,Dynamo 类靠 read-repair/反熵/CRDT;用 LWW 的 AP 存储并发写会静默丢数据,不会自动一致)。所以选 AP 必须显式写出收敛手段与不一致窗口 SLA,不能把“最终一致”当 CAP 的承诺(无分区时的延迟/一致性取舍见 PACELC,《核心考点与元理论》§6.6);
  4. 应用:交易/支付强一致场景偏 CP(或关键路径强一致+旁路最终一致);互联网读多写少场景通常 AP+最终一致;注册中心选 AP(Eureka)可容忍“短暂读到旧实例列表”,保可用性。配置中心要单独答一句(题干点了三个组件,不能只答两个):配置中心的写侧宜 CP 或「AP+版本号推送」——写错配置的影响面大于读到旧值,宁可写入失败也不能让各实例拿到不一致版本;读侧必须 AP(推送失败时要能用本地缓存快照继续启动与运行),否则配置中心抖动会拖挂全站。
  5. 容量/可用性数字锚点:Eureka 默认 30s 心跳、90s 摘除、自我保护 15 分钟成功率 <85% 触发;Nacos 临时实例 1.x 是 5s 心跳、15s 标不健康、30s 摘除,2.0 起临时实例改走 gRPC 长连接、服务端在连接断开时直接剔除该实例(下线感知取决于断连检测,实测在秒级,不再是 15s/30s 这套档位);ZooKeeper/etcd 选主期间不可写通常秒级~十几秒——这是选 CP 时要付的可用性代价。注册中心读 QPS 往往远高于写(服务发现),AP 系统可横向扩展读;写(注册/心跳)频率 ≈ 实例数/心跳间隔,例 1 万实例、Eureka 30s 心跳 ≈ 333 次/s(3s 心跳是反例:换成它心跳量涨到 3300 次/s、约 10 倍,只用来对比,不是推荐值),需要评估元数据存储与心跳通道容量。分区场景下的错误预算:调用方超时+重试次数要能覆盖注册中心短暂不一致窗口。
  6. 失败与降级:注册中心不可用时,消费端应使用本地缓存的实例列表继续调用(Eureka 客户端缓存、Nacos 本地 snapshot);调用方超时/重试/熔断消化坏实例;服务端优雅下线(先摘流量再停);关键交易链路可配置静态兜底地址。CP 系统选主失败期间:读可能过期/写拒绝,业务要么等待要么降级。原则:注册中心挂了,调用不能全断——靠客户端缓存与容错。

【原理溯源】

  • 为什么 P 不可放弃? 网络分区=机器之间网络断开或延迟剧增。分布式系统的前提是“多机+网络”,而网络本身不可靠(交换机故障、机房光纤被挖、GC 停顿导致心跳超时)。放弃 P 等价于假设“网络永远通”——这在真实环境不成立。所以 CAP 的真实命题是:分区发生时,保 C 还是保 A。

  • 为什么分区时 C 和 A 必然冲突? 分区后节点间无法同步写入。若保 C:一侧写入后另一侧必须拒绝读/写(否则读到旧数据=破坏 C),等于牺牲 A。若保 A:两侧都能继续服务,但一侧的写入暂时同步不到另一侧,读到旧数据=破坏 C。这不是工程取巧能绕开的,是信息无法跨分区传播的物理约束。

  • 为什么说“CA 系统”在分布式里不存在? 单机数据库(MySQL 单实例)在无网络时是 CA——它没有分区问题。一旦多副本,就必须面对分区:要么 CP(牺牲分区期可用性),要么 AP(牺牲分区期一致性)。所以正确表述是:分布式系统 = P 必选,C/A 二选一。

  • 为什么注册中心常选 AP? 注册中心的核心价值是“让调用方找到可用实例”。短暂读到旧实例列表(多一个已下线实例)的代价是“多试几次/重试其他实例”;而分区期不可用(CP 选主失败)的代价是“整个系统的服务发现瘫痪”。两者相比,可用性损失 >> 短暂不一致损失,所以 Eureka 设计成 AP + 最终一致。

  • 为什么交易系统偏 CP? 扣款场景下读到旧余额可能导致超卖/透支,资金错误的代价远大于“分区期暂时拒绝服务”。所以关键写路径宁可阻塞,也要保证读到的是最新已提交数据。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
这个组件/链路在分区时,"读到旧数据"和"暂时不可用"哪个更不可接受?
├─ 读到旧数据更不可接受(钱、库存、主键、强约束状态)
│   → CP:ZooKeeper / etcd / Raft 组 / 关键写路径同步阻塞
│   → 典型:交易扣款、分布式锁、配置的强一致下发
└─ 暂时不可用更不可接受(服务发现、DNS、读多写少展示)
    → AP + 最终一致:Eureka / Nacos 临时实例 / DNS / CDN
    → 典型:注册中心、用户信息展示、推荐列表

额外判断:是否可以"分层取舍"?
└─ 可以 → 关键路径 CP,旁路/读路径 AP(主流互联网架构)

判断口诀: 先承认 P 必在,再问“分区期宁可拒服务还是宁可读旧数据”。

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“CAP 讨论的是分区发生时 C 和 A 的取舍,P 不是可选项”
0:30–1:30讲清三字母C/A/P 各一句话定义 + 强调“分布式里 P 必然存在”
1:30–2:30论证冲突用“分区后无法同步写入”讲清为什么 C/A 不能兼得
2:30–3:30落地选型题干点了三个组件,逐个给:注册中心 AP(短暂旧列表可接受);配置中心 写侧 CP 或 AP+版本号推送、读侧 AP+本地快照;交易 CP(资金不能读旧)——并各给一句理由
3:30–4:30补充纵深提分区恢复后最终一致(BASE)、单机 CA vs 分布式二选一
4:30–5:00收尾“一句话:CAP 不是三选二的口号,而是分区时的业务代价排序”

【关键数字】

参数经验值说明
Eureka 心跳/续约默认 30s 心跳,90s 未续约摘除AP 设计下容忍短暂故障
Eureka 自我保护15 分钟内心跳成功率 <85% 触发宁可保留旧实例也不摘除
ZK 选举期间不可写通常秒级~十几秒CP 代价的直观感受
Nacos 临时实例心跳1.x:5s/15s/30s;2.x:gRPC 长连接,断连即剔除临时实例偏 AP
网络分区在生产大规模集群“一定会遇到”不是假设,是运维现实
ZK 选主不可写秒级~十几秒CP 代价
注册写压力实例数÷心跳间隔推荐口径 1万/30s≈333写/s;3s 心跳是反例:1万/3s≈3300写/s、约 10 倍,只用来对比

【追问链】(三层)

L1|“MySQL 主从属于 CAP 里的哪种?” → 严格说主从不是 CA:只要存在异步复制,没发生分区时副本间也有延迟,就不满足本题对 C 的定义(所有节点同一时刻同一数据)。出现主从延迟或主挂切换时:从库可读旧数据=AP 倾向;若强制只读主=偏 CP(牺牲从库可用性)。准确说法是:主从复制本身是最终一致的 AP 倾向方案,业务可选择“读主”来逼近强一致。

L2|“怎么做到既强一致又高可用?” → CAP 说的是分区场景。常态下 Raft/ZAB 多数派存活时可既一致又可用。跨地域时:单地域 Raft 强一致;跨地域异步复制+最终一致,或单元化把交易锁在单地域。不能在跨分区场景同时要 C 和 A,但可以在无分区时两者兼得。落地话术:我们关键写路径 CP(扣款),服务发现与读展示 AP+最终一致,用调用容错消化不一致窗口。

L3|“注册中心选了 AP,实例列表不一致导致调到已下线服务怎么办?” → 三层兜底:① 消费者侧重试/故障转移到其他实例;② 调用方超时+熔断,不因坏实例拖死;③ 服务端优雅下线(先摘流量再停进程)。AP 的不一致窗口被“调用容错”消化,这就是为什么注册中心敢选 AP——下游有重试,上游才敢读旧。

【评分标准】

档位答案特征
60 分能说出 C/A/P 三字母含义,知道“三选二”
80 分讲清 P 必然存在、实际是 C/A 取舍,并能用 ZK/Eureka 举例
95 分能从“信息无法跨分区传播”论证冲突;按业务代价给出注册中心 AP、配置中心(写侧强一致+读侧本地快照)、交易 CP 三项的理由;指出单机 CA 与分布式二选一的区别;能接“分层取舍”

【关联题】

  • 下游工程延伸: 第 98 题(BASE)——CAP 的工程化落地(98 侧把本题列为它的上游,方向以这里为准)
  • 同簇组件: 第 112 题(注册/配置中心)、第 113 题(注册中心故障)——AP 选择的直接后果
  • 对比 CP 组件: 第 104 题(ZK 锁)、第 105 题(RedLock)——强一致组件的代价
  • 下游应用: 第 99 题(分布式事务选型)——理论到方案

【自测】

  1. 判断对错:CAP 理论说分布式系统只能三选二,所以我们可以做一个不要 P 的系统。 参考答案:错。网络分区在分布式环境必然发生,放弃 P=假设网络永远可靠,不现实。真实选择是分区时保 C 还是保 A。
  2. 为什么注册中心通常选 AP 而不是 CP? 参考答案:短暂读到旧实例列表的代价(重试即可)远小于分区期服务发现整体不可用的代价。调用方有重试/熔断兜底,所以可以容忍短暂不一致。
  3. 交易扣款链路应该选 CP 还是 AP?为什么? 参考答案:偏 CP。读到旧余额可能导致超卖/透支,资金错误代价远大于分区期暂时拒绝服务。

98. 架构师说“我们走 BASE 不走 ACID”(BASE 理论) ​

【考察内容】BASE 是分布式理论必考题,常与 CAP 连问

【题目】订单系统要做最终一致方案,架构师拍板“我们走 BASE,不走 ACID”。BASE 是什么?它和 ACID 是什么关系?为什么互联网系统普遍接受 BASE 而不是 ACID?

【参考答案】

  1. BASE 是分布式系统的一致性模型(由 eBay 提出):Basically Available(基本可用)——系统允许部分功能降级(如高峰限流、非核心关闭),核心可用;Soft state(软状态)——允许数据存在中间状态(如订单“处理中”);Eventually consistent(最终一致)——不要求实时一致,允许延迟,但保证经过一段时间后数据一致(通过重试、对账、补偿);
  2. 与 ACID 关系:ACID 是单机事务的强一致模型(原子、一致、隔离、持久);BASE 是分布式场景对 ACID 的妥协——用“最终一致+基本可用”换“高可用+高性能”;
  3. 工程体现:本地消息表、事务消息、TCC、Saga、对账系统都是实现“最终一致”的手段;
  4. 选择:核心资金强一致链路用 ACID 特性(数据库事务/2PC/TCC);互联网绝大多数业务用 BASE(最终一致可接受)。
  5. 容量与 SLA 数字:最终一致窗口常见约定——通知/积分 秒级(MQ 实时消费);本地消息表轮询 秒~十秒级(扫描间隔);对账兜底 分钟级~T+1。若峰值业务 3000 TPS,补偿任务与对账要能处理同量级延迟事件,否则不一致窗口被拉长。消息重试常见 ≥16 次 + 死信;重试队列容量按失败率估算。监控“不一致延迟”:从业务时间戳到副作用完成时间的 P99,直接对应 SLA。没有数字的“最终一致”在面试与生产都站不住。
  6. 失败与降级:基本可用=高峰限流/关闭非核心;软状态=允许“处理中”,前端可查询进度;最终一致=重试+对账+补偿三层收敛。失败模式:消息丢→靠本地消息表/事务消息;重复→消费幂等;补偿失败→人工差错平台。禁止把 BASE 理解成“可以不对账”。金融场景:日终/准实时对账是硬要求。

【原理溯源】

  • 为什么分布式里 ACID 难以为继? ACID 的 I(隔离性)和 A(原子性)依赖“一个事务管理器能同时锁住所有相关数据”。单机时锁在本地内存/引擎里,代价可控;跨库跨服务时必须用 2PC 同步持锁,锁时间=全链路网络往返,高并发下锁冲突指数级上升。ACID 没有错,错的是把单机假设硬套到跨网络场景。

  • BASE 三个字母各在解决什么问题? 基本可用:高峰时可降级,避免“全有或全无”的雪崩;软状态:承认中间态存在(“支付中”“同步中”),不强行让所有节点时刻一致;最终一致:用时间换空间——不立刻一致,但通过重试/对账/补偿保证收敛。三者合起来=把一致性的成本从“同步时刻”摊到“异步过程”。

  • 为什么最终一致必须有“收敛机制”? 最终一致不是“放着不管”。若只靠“发个消息”,消息丢失/消费失败/进程宕机都会导致永不一致。所以工程上必须有:可靠投递(本地消息表/事务消息)+ 消费幂等 + 对账兜底。没有收敛机制的最终一致=不一致。

  • 为什么说 BASE 不是“不要正确性”? BASE 放弃的是“实时强一致”,不放弃“最终正确”。资金场景仍要对账到分毫不差,只是允许秒级/分钟级延迟。把 BASE 理解成“可以乱来”是最大误读。

  • 为什么互联网普遍接受 BASE? 因为大多数业务的“不一致窗口”人眼不可感:订单状态晚 2 秒同步、积分晚 5 秒到账、物流状态晚 1 分钟更新——用户无感。用这个可容忍的延迟换可用性和吞吐,是业务语义允许下的最优解。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
这条数据/链路,能否容忍"几秒~几分钟"的不一致窗口?
├─ 不能(资金扣减、库存超卖、主键冲突)
│   → 强一致:本地事务 / TCC / 2PC 类;仍要对账兜底
└─ 能(状态同步、通知、积分、日志、统计)
    → BASE:本地消息表 / 事务消息 / Saga + 幂等 + 对账
    └─ 进一步:窗口能多长?
        ├─ 秒级 → 同步 RPC 校验 + 异步补偿
        └─ 分钟级/T+1 → 纯异步 + 定时对账

判断口诀: 先问“不一致窗口业务能不能忍”,再选收敛手段(重试/对账),最后定 SLA。

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“BASE 是分布式对 ACID 的工程化妥协,用最终一致换可用与性能”
0:30–1:30三字母拆解基本可用/软状态/最终一致,各配一个业务例子
1:30–2:30对比 ACIDACID 单机强一致 vs BASE 分布式最终一致;为什么跨库 ACID 代价大
2:30–3:30工程落地本地消息表、事务消息、对账——强调“最终一致必须有收敛机制”
3:30–4:30场景划分资金走强一致,绝大多数业务走 BASE
4:30–5:00收尾“BASE 不是不要正确性,是把一致性的成本从同步时刻摊到异步过程”

【关键数字】

参数经验值说明
最终一致常见 SLA秒级~分钟级需与业务明确约定
本地消息表投递延迟秒~十秒(取决于扫描间隔;要 <1s 需事务提交后同步触发)依赖扫描频率
对账周期通知/状态:分钟级;资金:T+0 或 T+1资金建议准实时
eBay 提出 BASE2008 年左右分布式架构实践总结与 CAP 论文形成互补
消息重试常见 ≥16 次 + 死信 + 人工仅靠重试不够

【追问链】(三层)

L1|“BASE 是不是就不用事务了?” → 不是。本地库内仍用 ACID 事务保证单库正确;BASE 放弃的是“跨库/跨服务的同步强一致”。本地消息表本身就是“业务写入+消息记录在同一个本地事务里”——局部 ACID,全局 BASE。

L2|“最终一致要多久?业务问 SLA 怎么答?” → 取决于收敛手段:MQ 实时消费通常秒级;本地消息表轮询秒~十秒;对账兜底分钟到 T+1。必须与业务明确 SLA,并在监控埋“不一致延迟”指标(P99)。不能含糊说“很快”。面试加分:给出具体链路的例子——“支付成功后积分,我们约定 5s 内 99% 到账,超 30s 触发补偿对账”。

L3|“如果消息丢了,最终一致还成立吗?” → 仅靠 MQ 不成立。所以要有:① 本地消息表/事务消息保证“事件不丢”;② 消费幂等保证“重复不坏”;③ 定时对账保证“漏了能补”。三层缺一不可——最终一致是机制组合,不是单点组件。

【评分标准】

档位答案特征
60 分能说出 BASE 三档:基本可用(BA)、软状态(S)、最终一致(E)
80 分能对比 ACID 与 BASE,并举出本地消息表/对账等工程手段
95 分讲清“局部 ACID + 全局 BASE”;强调最终一致必须有收敛机制;能按业务划分强一致/最终一致场景;能定义 SLA

【关联题】

【自测】

  1. BASE 的三个字母分别指什么?各举一个工程例子。 参考答案:基本可用(高峰限流降级)、软状态(订单“处理中”中间态)、最终一致(积分异步发放,秒级到账)。
  2. 判断对错:走 BASE 就可以不做对账。 参考答案:错。最终一致必须有收敛机制(重试/对账/补偿),否则消息丢失或消费失败会导致永不一致。
  3. 单库内更新订单状态+写流水,应该用 ACID 还是 BASE? 参考答案:同库用本地 ACID 事务。BASE 针对的是跨库/跨服务场景;单库强事务成本低,没必要弱化。

99. 下单跨订单/库存/积分三个服务,事务方案怎么选(分布式事务选型) ​

【考察内容】分布式事务选型是资深岗必考

【题目】微服务化后,一次下单要同时操作订单服务、库存服务、积分服务,三个独立数据库。强一致(2PC)代价大、最终一致又怕出错。请对比 2PC/TCC/Saga/本地消息表/事务消息几类方案,并给出这个场景的推荐组合与理由。

【参考答案】

  1. 先分类:按一致性强度分——强一致(2PC/Seata AT/TCC)与最终一致(本地消息表/事务消息/Saga);按业务场景分——短事务(交易链路)与长事务(跨多系统);
  2. 方案矩阵:
    • 2PC/Seata AT:自动回滚、开发量小,适合并发不高的短事务(依赖数据库事务 + 全局锁,高并发下锁竞争严重);
    • TCC:Try 预留 / Confirm / Cancel,业务自实现,性能好、无全局锁,适合高并发核心交易(支付、扣减),但要处理空回滚 / 悬挂 / 幂等(实现复杂);
    • 本地消息表 / 事务消息(RocketMQ):最终一致,适合“下单成功后异步通知”类场景(发短信、积分、状态同步),主流电商做法;
    • Saga:正向 + 反向补偿,适合长事务(跨多系统、流程长),需要补偿逻辑设计;
  3. 本题推荐:订单创建与库存扣减(资金 / 库存强相关)用 TCC——按第 4 条假设的下单峰值 5000 TPS(题干未给数,这是该场景常见量级)已超出 AT 的适用区间(AT 的全局锁在数千 TPS 下会随并发放大重试、吞吐塌陷),只有并发确实落在数百 TPS 以内的内部系统才考虑 AT;发积分 / 通知走事务消息最终一致——混合使用,强一致链路保正确,非核心链路异步化。
  4. 容量估算(方案选型约束):下单峰值 5000 TPS 时——2PC/AT 全局锁竞争激烈,行锁持有时间=全链路 RT,通常只适合 数百 TPS 级短事务;TCC 无全局长事务锁,可支撑 数千~数万 TPS(取决于资源预留实现);本地消息表/事务消息对核心下单路径几乎只增加一次本地写,异步事件峰值与订单峰值同量级。Saga 适合长流程,补偿逻辑 RT 与频率要纳入容量。选型时同步估算:库存/账户热点行、事务状态表写入 QPS、消息投递 QPS、对账任务批处理量。
  5. 失败与降级:混合架构下——TCC 失败要 Cancel/空回滚/悬挂处理;事务消息/本地消息表失败要补偿;Saga 某一步失败触发反向补偿;对账发现差异走差错。降级顺序:非核心积分/通知降级为最终一致可延迟;库存超卖风险不可降级,必须预留成功才确认。演练故障注入:网络延迟、重复回调、MQ 丢失,验证收敛。

【原理溯源】

  • 为什么 2PC 高并发扛不住? 2PC 是同步阻塞协议:Prepare 阶段所有参与者就开始持有行锁,直到 Commit/Rollback 才释放。锁持有时间 = 整个事务时长(含协调者与所有参与者的网络往返),并发越高,锁冲突与等待越严重。此外协调者是单点,参与者阻塞期间不可用。

  • 为什么 TCC 性能好? 它把“锁”的粒度从数据库行锁下沉到业务层面的资源预占。Try 阶段只做额度冻结(如 UPDATE stock SET frozen = frozen + 1 WHERE ...),不持有数据库长事务,因此并发能力强。代价是业务必须自己实现三阶段,且要处理空回滚 / 悬挂。

  • TCC 为什么会有空回滚和悬挂? 根因是分布式环境下的网络乱序与超时重试:

    • 空回滚 = Try 请求因网络问题未到达,但超时后协调者发了 Cancel → Cancel 先执行,此时根本没有预留资源;
    • 悬挂 = Cancel 先于 Try 到达并执行完,之后延迟的 Try 才到达,预留的资源再也没人释放 → 资源被永久占用;
    • 解法:用事务状态表记录“Try 是否执行过”。Cancel 时先查状态(空回滚则直接标记为已回滚),Try 时先查是否已被 Cancel(悬挂则直接拒绝)。
  • 为什么最终一致必须配本地消息表? 因为 MQ 无法与本地数据库事务原子提交:“先写库后发消息”会在写库成功后、发消息前宕机而丢消息;“先发消息后写库”会在消息发出后、写库失败时产生脏消息。本地消息表把“业务写入”和“待发消息”放进同一个本地事务,再由后台任务扫描投递,从而消除这个窗口。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
事务是否真的跨多个库 / 服务?
├─ 否(同一个库)→ 用本地事务,不要引入分布式事务(最常见的过度设计)
└─ 是
   ├─ 并发低 且 事务短(<1s)→ 2PC / Seata AT(开发量最小)
   └─ 并发高 或 事务长
      ├─ 操作强约束资源(钱 / 库存,不允许超卖或透支)
      │   → TCC(资源预占 + 确认/取消),必要时叠加 Redis 预扣
      └─ 操作非强约束资源(积分 / 通知 / 日志 / 状态同步)
          ├─ 流程短(1–2 步)→ 本地消息表 / RocketMQ 事务消息
          └─ 流程长(3+ 步,跨多系统)→ Saga(正向 + 补偿)

判断口诀:先问“是否真跨库”,再问“并发与事务长度”,最后问“资源是不是强约束”。

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“这是一道分布式事务选型题,核心是先分清强一致和最终一致两条线,再按业务约束选”
0:30–2:30展开矩阵按“一致性强度 × 并发量 × 事务长度”三维讲 5 类方案,每类一句机制 + 一句代价
2:30–3:30落地推荐“回到本题:订单和库存走 TCC,积分走事务消息——强一致链路保正确,非核心链路异步化”
3:30–4:30风险兜底主动提 TCC 三问题及解法、对账补偿机制(主动提风险是加分项)
4:30–5:00收尾“一句话:分布式事务没有银弹,选型本质是在一致性、性能、复杂度三者间按业务分级取舍”

【关键数字】

参数经验值说明
TCC Try 预留超时30s – 5min超过则自动 Cancel,防资源长期占用
RocketMQ 事务消息回查默认每 60s 检查一轮,最多 15 次(transactionCheckMax)确认“半消息”状态;次数耗尽默认回滚。递增档位「1s…10s 30s 1m…2h」是消费重试用的,不是回查节奏
本地消息表重试≥16 次 + 定时补偿兜底仅靠重试不够,必须有补偿任务
库存 Redis 预扣超时与订单超时一致(15–30 分钟)未支付自动回滚库存
2PC 高并发劣化并发 1000+ 时 RT 通常劣化 5–10 倍锁等待放大的量级感
对账兜底周期T+0(准实时)或 T+1资金类建议 T+0
2PC/AT 适用 TPS常数百级(短事务)全局锁竞争限制
TCC 适用 TPS数千~数万业务预留,无长事务锁
异步事件峰值≈ 订单峰值本地消息表/事务消息容量

【追问链】(三层)

L1|“为什么不全部用最终一致?省事又快。” → 因为库存和资金是强约束资源,异步扣减存在超卖窗口。钱的场景不能容忍“暂时不一致”,哪怕只有几百毫秒。所以强一致链路必须保留。

L2|“TCC 的 Try 成功了,但 Confirm 一直没来怎么办?” → Confirm 不能因为失败就改判回滚——Try 已预留成功,这笔分支只能走向 Confirm 或超时兜底。落地四步:① 事务状态表记下该分支已 Try 成功,重投 Confirm 时靠它保证只生效一次;② 补偿任务定期扫「已 Try 未 Confirm」的记录持续补投,按第 90 题给的重试口径(RocketMQ 默认重试 16 次)加定时兜底;③ 先判终态:TC 已判 Commit 的分只能继续投 Confirm,不能 Cancel;只有全局终态未定的悬挂预留才按关键数字的超时(30s–5min)自动解冻,绝不能让库存和额度长期悬着;④ 对账兜底(资金类 T+0)发现「预留已扣但下游没到账」走差错。本参考答案第 5 条的降级顺序里,库存超卖风险不可降级。

L3|“如果 Cancel 本身也失败了,预留的资源是不是就永远锁死了?” → 所以 Cancel 必须幂等 + 可重试;同时对关键资源(额度冻结)设置超时自动解冻作为最后一道防线,保证不会永久占用。这也是 TCC 在资金场景比 Saga 更合适的原因——Saga 的补偿是“反向操作”,而 TCC 的预占天然可超时释放。

【评分标准】

档位答案特征
60 分能列出 2PC / TCC / Saga / 消息表 4 类方案,大致说清机制
80 分能按“一致性强度 × 并发量 × 事务长度”三维选型,并给出本场景的推荐组合与理由
95 分主动指出 TCC 的空回滚 / 悬挂 / 幂等三问题及解法;说清“为什么库存不能用最终一致”;补充对账兜底;指出“同库场景不该引入分布式事务”这一过度设计陷阱

【关联题】

【自测】

  1. 一个“用户注册成功后送 100 积分”的场景,跨用户服务与积分服务,该选哪种方案?为什么? 参考答案: 最终一致(事务消息或本地消息表)。积分是非强约束资源,注册成功但积分延迟几秒发放业务可接受,不值得为它引入 TCC 的复杂度。
  2. TCC 的 Try 阶段做的关键动作是什么?(不是“扣减”,而是什么?) 参考答案: 资源预占 / 额度冻结。Try 不真正扣减,只锁定资源;真正扣减发生在 Confirm。
  3. 判断对错:用了本地消息表就保证了消息不丢,所以不需要对账。 参考答案: 错。本地消息表保证“消息最终会被投递”,但下游消费处理失败同样会导致业务不一致,因此仍需对账兜底。

100. 跨服务扣款要同时成功或同时失败(2PC/3PC) ​

【考察内容】分布式事务协议原理

【题目】业务要求“扣用户余额”和“扣商家账户”要么都成功要么都失败,跨两个服务。用两阶段提交(2PC)怎么实现?它有哪些致命缺陷(阻塞、单点、脑裂)?3PC 改进了什么、为什么生产上还是少用?

【参考答案】

  1. 2PC:
    • 阶段一(准备):协调者向所有参与者发 prepare,参与者执行本地事务但不提交,返回 yes/no;
    • 阶段二(提交):协调者根据结果发 commit(全部 yes)或 abort(任一 no),参与者执行提交/回滚;
    • 缺陷:①同步阻塞(prepare 后参与者资源被锁住,等待协调者);②单点故障(协调者挂了,参与者无法决策,一直阻塞);③脑裂风险(协调者与部分参与者网络分区,可能提交不一致);④没有容错恢复机制;
  2. 3PC:引入超时机制和第三阶段(canCommit 询问 → preCommit 预提交 → doCommit 最终提交),参与者在超时后可自行决定提交(解决协调者挂了的阻塞);但仍未完全解决分区下的不一致,且多一轮通信开销;
  3. 工程启示:强一致方案(2PC)在跨服务场景代价高,所以生产多用 TCC/最终一致;数据库内部的"两阶段提交"指的是 redo log prepare → 写 binlog → redo log commit(崩溃时按 binlog 是否完整决定提交或回滚),以及 XA 这类机制(半同步属复制持久化,不是两阶段提交:它只等从库收到 binlog,不提供跨引擎原子提交),不是 redo log 自己分两阶段。
  4. 容量估算:2PC 同步阻塞,锁持有时间 ≈ Prepare 往返 + 协调者决策 + Commit 往返,假设全链路 20–50ms,则同一行锁冲突概率随并发上升。经验上高并发交易 不宜用 2PC 做主路径(数百 TPS 以上锁竞争明显);3PC 在 2PC 上加超时与 precommit,减少阻塞但不能消除网络分区下的不一致,生产少见。参与者数量 N 增加时,协调者消息复杂度 O(N),失败面扩大。替代:TCC/本地消息表按业务拆事务边界。
  5. 失败与降级:协调者宕机 → 参与者阻塞(2PC 经典问题),需协调者高可用/备份日志恢复;参与者 prepare 后超时 → 按协议回滚或询问协调者;网络分区 → 多数表现为阻塞(CP 取向,宁可等待不盲提交);但 2PC 没有多数派机制,决议一旦作出就无法再复核——协调者发出部分 commit 后失联、或同时有两个协调者实例发起同一事务时,仍会提交不一致(见缺陷③)——「阻塞」与「提交不一致」是两种并存的失效,不是「而非」。生产预案:协调者集群化、事务日志持久化、超时后人工/自动恢复流程;核心资金路径宁可失败也不盲提交。

【原理溯源】

  • 为什么 2PC 必然同步阻塞? Prepare 阶段参与者“执行但不提交”,本质是持有行锁/写隔离,等待协调者指令。锁必须持有到第二阶段,否则别人一改就破坏了“准备态”。锁持有时间 ≥ 一次网络往返 + 协调者决策时间,高并发下锁等待队列爆炸。

  • 为什么协调者是单点致命问题? 2PC 没有内置的协调者选举/复制协议。协调者在发完部分 commit 后宕机,参与者既不能自行提交(可能有人收到 abort)也不能回滚(可能有人已 commit)——卡在不确定态,只能等人工或协调者恢复。

  • 提交不一致是怎么发生的? 2PC 里参与者看不到彼此的 vote,只有协调者能判提交,所以「一侧收到 abort、另一侧自行 commit」在协议内不会发生。真正的危害有两条:① 协调者已判 commit、只把 commit 送达部分参与者后失联——已收到的提交、没收到的卡在不确定态只能阻塞,恢复后若按超时默认回滚就与前者不一致;② 具体实现允许参与者在不确定态自行判定,或同时跑着两个协调者实例(部署层面的脑裂),才会两边各自推进。根因是没有多数派机制;Raft/Paxos 用多数派把「决议唯一化」解决的正是这条(注意它只消除协调者单点与决议分叉,参与者持锁与阻塞不变)。

  • 3PC 的超时为什么不够? 3PC 让参与者在超时后“默认提交”,减少了阻塞,但默认提交在分区下可能错:一侧超时提交、另一侧实际应 abort。它把“阻塞”换成了“可能不一致”,没有消除分区下的根本矛盾。多一轮 RPC 还增加了延迟。

  • 为什么数据库内部还用类 2PC? 因为数据库内部(redo log prepare → commit)协调者与参与者在同一实例/强绑定的存储引擎内,没有跨机网络分区问题,或由存储层的共识协议保证。跨服务 2PC 才是灾难现场。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
是否真的需要"跨库/跨服务同时成功"?
├─ 否 → 本地事务,别上 2PC
└─ 是
   ├─ 并发低 + 事务短 + 团队不想改业务代码
   │   → Seata AT / XA(类 2PC,自动补偿)
   └─ 并发高 或 不能接受长持锁
       → TCC(业务预留)或 最终一致(消息/对账)
       → 2PC/3PC 直接在生产跨服务场景基本不用

判断口诀: 2PC 是教材协议,生产用它的“自动补偿变体”(AT)或换 TCC/最终一致。

【口述骨架】(5 分钟;框架 A,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“2PC 是强一致协议的原型,核心问题是同步阻塞与协调者单点”
0:30–1:30讲流程Prepare/Commit 两阶段,用扣款例子走一遍
1:30–2:30讲缺陷阻塞、单点、脑裂,每条一句因果
2:30–3:30讲 3PC加超时+第三阶段,改进了什么、为什么仍不够
3:30–4:30工程结论生产跨服务不用裸 2PC;AT/TCC/最终一致;DB 内部类 2PC 可行
4:30–5:00收尾“2PC 的价值是把问题说清楚,不是拿来直接上线”

【关键数字】

参数经验值说明
2PC 往返次数2 轮(4 条消息,最简)3PC 至少 3 轮
高并发下 2PC RT锁等待可放大 5–10 倍并发 1000+ 时明显
协调者恢复依赖事务日志重放无内置选举
XA 事务在 MySQL性能约为本地事务的 1/3~1/10视锁冲突而定
Seata AT 全局锁行级,冲突则重试高并发仍成瓶颈
锁持有时间≈ 事务全链路 RT20–50ms×并发=冲突上升
适用并发常数百 TPS 以下短事务高并发改用 TCC/消息表
协调者消息O(参与者数)N 越大失败面越大

【追问链】(三层)

L1|“数据库内部为什么可以做两阶段,跨服务就不行?” → DB 内部 prepare/commit 时协调者与参与者在同一存储引擎/实例内,或由存储共识协议保证,没有跨机分区脑裂问题。跨服务时网络成为变量,才暴露单点与阻塞。

L2|“参与者 prepare 成功后协调者挂了,资源会一直锁着吗?” → 2PC 各参与者在 Prepare 阶段已开始持有本地锁/资源,直到协调者下达 Commit/Rollback 才释放。协调者单点或网络中断时,参与者进入阻塞态(不确定态),线程与锁被占住。3PC 引入超时与 PreCommit,参与者超时可按策略提交/回滚以降低阻塞,但网络分区下仍可能不一致,且实现复杂,互联网主链路很少用;更常见是 TCC/Saga/本地消息表。

L3|“如果用 Raft 把协调者做成多副本,2PC 就完美了吗?” → 能解决协调者单点,但解决不了参与者长时间持锁。而且多副本协调者增加延迟与复杂度。高并发场景仍应把“锁”下沉到业务预留(TCC)或异步化(最终一致),而不是把 2PC 包得更重。

【评分标准】

档位答案特征
60 分能说出 2PC 两阶段流程和“阻塞、单点”
80 分完整讲阻塞/单点/脑裂,并说明 3PC 加了超时
95 分从锁持有时间解释阻塞根因;指出 3PC 用“可能不一致”换“少阻塞”;说清 DB 内部可用、跨服务不用;引出 AT/TCC 替代

【关联题】

【自测】

  1. 2PC 的“同步阻塞”指的是什么被阻塞? 参考答案:参与者在 Prepare 后持有数据库锁/写隔离,必须等 Commit/Rollback 才能释放,期间其他事务被挡。
  2. 3PC 相对 2PC 核心改了什么?为什么生产仍少用? 参考答案:引入超时与第三阶段,减少协调者宕机导致的无限阻塞;但分区下仍可能不一致,且多一轮通信,故跨服务生产很少直接用。
  3. 判断对错:2PC 能保证跨服务强一致,所以高并发交易也该用 2PC。 参考答案:错。2PC 长持锁+协调者单点,高并发下锁竞争严重、可用性差,应改用 TCC 或最终一致。

101. 余额+库存+积分三件事要“可预留、可确认、可回滚”(TCC 模式) ​

【考察内容】TCC 是高并发强一致场景的核心考点

【题目】业务要求强一致性:用户余额扣减、库存扣减、积分增加三件事要么全成要么全回滚,且并发高。你们评估用 TCC 模式。请讲清 Try/Confirm/Cancel 三阶段各自做什么(怎么预留资源),以及 TCC 的三个经典坑:空回滚、悬挂、幂等怎么处理?

【参考答案】

  1. 三阶段:
    • Try:预留资源/检查条件(如扣款冻结 100 元、库存预占 1 件),不实际生效;
    • Confirm:确认执行(冻结转扣款、预占转扣减),Try 全部成功才执行;
    • Cancel:取消回滚(解冻金额、释放库存);
  2. 三个经典坑及解法:
    • 空回滚:Try 没执行(网络问题/服务挂了),Cancel 却到了——Cancel 里要能“空操作成功”:通过事务记录表判断 Try 是否执行过,没执行过直接标记回滚成功;
    • 悬挂:Cancel 先于 Try 到达并成功,之后 Try 才到——Try 执行前检查是否已 Cancel(幂等记录),已取消则拒绝执行(否则资源被永久占用);
    • 幂等:Confirm/Cancel 可能被重试多次——用事务控制表(全局事务 ID+分支状态)保证同阶段只生效一次;
  3. 工程组件:事务协调器(Seata TC)调度 Try→Confirm/Cancel,业务实现三个接口+事务状态表;
  4. 优缺点:无全局锁、性能好、灵活;侵入性强(每服务三接口)、开发量大。
  5. 容量估算:TCC 性能瓶颈通常在 Try 的资源预留写(冻结字段)与状态表。假设下单峰值 5000 TPS,每个订单扣减库存+冻结余额 2 次 Try(题干三件事里的积分已移出 TCC 走事务消息,见第 6 条,所以这里只有 2 个分支),则 Try 阶段的业务预留写约 1 万行/秒(5000×2);但 TCC 框架自己的状态表要另算:每笔 2 个分支=1 万行 insert/s,每个分支还要 Confirm 或 Cancel 各更新一次=再 1 万行 update/s,合计 ≈3 万行/秒。两类写要分开估、分开落分库分表;只按 1 万规划会低配 3 倍,状态表要分库分表或按账户/商品拆分。热点账户/热点 SKU 行更新可能成单行瓶颈,需要合并扣减或排队。Confirm/Cancel 失败重试频率 ≈ 峰值×失败率(1% 即 50/s)。空回滚/悬挂检查都走状态表,索引设计决定回查 RT。
  6. 失败与降级:Try 成功 Confirm 失败 → 重试 Confirm(幂等);Try 失败/未达 → Cancel(空回滚直接成功);Cancel 后迟到的 Try → 拒绝(悬挂防护);事务管理器不可用 → 依赖超时策略与状态表恢复任务。降级:非核心积分可从 TCC 降级为消息最终一致;库存/余额不可降级为“先确认后扣减”。监控:Try/Confirm/Cancel 成功率、悬挂次数、空回滚次数、状态表积压。

【原理溯源】

  • 为什么 Try 不能直接扣减? 直接扣减后若其他分支失败需要回滚,但“扣减”可能已被下游感知(余额变了、触发了消息)。预留/冻结是把资源标记为“不可用但未最终归属”,Confirm 才真正转移,Cancel 只解冻。这是“可确认、可回滚”的语义基础。

  • 为什么 TCC 比 2PC 快? 2PC 锁的是数据库行锁,持有整轮网络往返;TCC 把互斥下沉到业务字段(frozen),不持有 DB 长事务。并发冲突从“锁等待”变成“frozen 条件更新”,粒度可控、时间极短。

  • 空回滚的根因是什么? 网络超时后协调者重试/发 Cancel,而 Try 可能从未到达。分布式里“超时≠失败”,只能按最坏情况补偿——于是 Cancel 可能“先于”Try。解法必须让 Cancel 容忍 Try 不存在。

  • 悬挂的根因是什么? 消息/请求乱序:Cancel 已执行完毕,延迟的 Try 才到。若 Try 不检查“是否已取消”,会冻结一笔再也没人释放的资源。所以 Try 入口要有悬挂检查。

  • 为什么必须有事务状态表? 空回滚、悬挂、幂等三问题的共同解法都是“记住分支执行到哪一步了”。没有持久化状态,重试与乱序无法被正确识别。状态表是 TCC 的正确性核心,不是可选优化。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
是否高并发 + 强约束资源(钱/库存)+ 跨服务?
├─ 否,并发低 → Seata AT / 2PC 自动补偿(开发量小)
├─ 否,可最终一致 → 消息/Saga
└─ 是 → TCC
    └─ 实现检查清单:
        ├─ Try=冻结/预占,不是扣减
        ├─ Confirm/Cancel 幂等
        ├─ 事务状态表
        ├─ 空回滚、悬挂处理
        └─ 预留资源超时自动释放(最后一道防线)

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“TCC 把强一致从 DB 锁下沉到业务预留,换高并发”
0:30–1:30三阶段冻结→确认/释放,用余额+库存走一遍
1:30–3:00三大坑空回滚、悬挂、幂等,各一句成因+一句解法(状态表)
3:00–4:00与 2PC 对比无长持锁、侵入性强、开发量大
4:00–4:30工程组件TC + 三接口 + 状态表
4:30–5:00收尾“TCC 的本质是业务层资源预占协议,状态表是正确性核心”

【关键数字】

参数经验值说明
Try 预留超时30s – 5min超时自动 Cancel 只适用于全局终态未定的预留;已判 Commit 的分只能重投 Confirm
空回滚/悬挂处理状态表一次查询必须持久化
Confirm 重试可多次,必须幂等用全局事务 ID 去重
TCC vs 2PC 性能高并发下 TCC 明显更优无全局行锁
库存冻结字段frozen / available 分离典型表结构
Try 写压力业务预留:峰值×资源数;框架状态表另算5000×2≈1 万行/s(预留)+状态表 1 万 insert/s+Confirm/Cancel 1 万 update/s ≈3 万行/s,只按 1 万规划会低配 3 倍
热点行单行更新上限需合并/拆分/排队

【追问链】(三层)

L1|“TCC 和 2PC 本质区别是什么?” → 2PC 在数据库层锁资源(DB 控制,自动);TCC 在业务层预留资源(应用控制,需手写三接口)。TCC 无长持锁、更灵活,代价是侵入性与三大坑。

L2|“Try 成功了,Confirm 一直失败怎么办?” → 只能重试 Confirm,不能改判——第 6 条给的处置就是「重试 Confirm(幂等)」。要点四条:① 用事务控制表按 全局事务 ID + 分支状态 去重,重复执行只生效一次,这正是关键数字里「Confirm 重试可多次,必须幂等」的含义;② 重试要有退避与上限,第 5 条算过 5000 TPS、失败率 1% 就是约 50 次/秒的重投,不设上限等于自己再造一波洪峰;③ 先判终态:TC 已记 Commit 决议的分支不能再 Cancel(否则「全局认为扣减成功、资源却已解冻」是资损),只能幂等重投 Confirm 并告警转人工,必要时做可审计的业务冲正、纳入对账(资金类 T+0);只有全局终态未定(决议丢失、悬挂预留)才按 Try 预留超时(30s–5min)自动 Cancel 释放冻结;④ 监控 Confirm 成功率与状态表积压。

L3|“Confirm/Cancel 能不能合并成一个反向 SQL,像 Saga 那样?” → 不建议在资金场景。Saga 补偿是“再做一笔反向业务”,中间态已对外可见;TCC 的冻结/解冻是资源状态机,天然可超时释放,审计清晰。资金场景更需要 TCC 这种“未最终归属”语义。

【评分标准】

档位答案特征
60 分能说出 Try/ Confirm/ Cancel 三阶段大致含义
80 分讲清“预留不是扣减”,并说出空回滚/悬挂/幂等及状态表解法
95 分从锁粒度解释 TCC 为何快;三大坑根因是网络乱序/超时;补充超时释放兜底;能对比 2PC/Saga

【关联题】

【自测】

  1. TCC 的 Try 阶段关键动作是什么? 参考答案:资源预留/冻结,不是真正扣减。真正扣减在 Confirm。
  2. 什么是悬挂?怎么防? 参考答案:Cancel 先执行完,延迟的 Try 才到,资源被永久冻结。Try 入口检查是否已 Cancel,已取消则拒绝。
  3. 判断对错:TCC 不需要事务状态表,靠重试就能正确。 参考答案:错。空回滚、悬挂、幂等都依赖状态表识别分支执行情况,没有状态表无法正确处理乱序与重试。

102. 两个库要同时提交或回滚,但业务代码不想大改(Seata AT) ​

【考察内容】Seata AT 是主流分布式事务框架,面试高频

【题目】订单服务调用库存服务,跨两个库要同时提交或回滚。团队希望业务代码改动最小(不想像 TCC 那样写预留逻辑)。Seata AT 模式是怎么做到的?它和 TCC 比有什么优缺点?高并发下有什么瓶颈?

【参考答案】

  1. 角色:TC(事务协调器)、TM(事务管理者,开启全局事务)、RM(资源管理器,注册分支事务);
  2. 原理(自动补偿):
    • 一阶段:业务 SQL 执行前,RM 生成 undo_log(记录修改前后的镜像),与业务 SQL 在同一本地事务提交;
    • 二阶段:全局事务提交——各分支异步删除 undo_log;回滚——根据 undo_log 生成反向 SQL 恢复数据;
    • 全局锁:写操作前获取全局锁(防止其他全局事务并发修改同一数据,TC 管理);
  3. 优点:无侵入(@GlobalTransactional 注解即可,业务代码不用改)、开发量小;
  4. 缺点:依赖数据库事务(undo_log 表);全局锁在高并发下成为瓶颈(锁冲突重试);适合并发不高、业务逻辑复杂的场景;
  5. 与 TCC 对比:AT 自动补偿(透明、开发少、全局锁瓶颈);TCC 业务预留(灵活、性能好、开发多);
  6. 补充:读操作默认不加全局锁(可能读到中间态)——用 @GlobalLock + SELECT FOR UPDATE 感知全局锁。
  7. 容量估算:Seata AT 基于数据库全局锁与 undo log,全局锁竞争在热点行上明显。经验:非热点、短事务、并发 数百~一两千 TPS 较合适;更高并发核心交易改 TCC 或消息表。undo log 表会随事务量增长,日订单 50 万×平均 2 分支 ≈ 100 万 undo 行/日,要归档。全局锁与 TC 服务本身要高可用;分支事务越多,协调 RT 越长。
  8. 失败与降级:TC 宕机 → 事务悬挂/需恢复(TC 要集群化);undo 应用失败 → 重试与人工介入;热点行全局锁等待超时 → 业务失败重试或改方案。降级:非核心服务从 AT 模块拆出,改为最终一致;核心扣减若 AT 撑不住峰值,切换 TCC。监控:全局锁等待、分支事务失败率、undo 积压、TC 负载。

【原理溯源】

  • AT 为什么能做到“无侵入”? 它在 SQL 执行前后由数据源代理自动拦截:先读旧值/推算新值写入 undo_log,再执行业务 SQL,两者同一本地事务。回滚时用 undo_log 生成反向 SQL。业务只写普通 SQL,框架在底下补了“镜像+补偿”。

  • 为什么还需要全局锁? 若两个全局事务同时改同一行,各自记录的 undo_log 会在时间上交错,回滚时可能把别人的修改一起冲掉。全局锁保证同一行同一时刻只被一个全局事务写,让镜像可回滚。

  • 为什么高并发下全局锁成瓶颈? 全局锁是跨事务的逻辑锁,冲突时 RM 要重试等待。热点行(如库存、账户)上锁冲突随并发上升,吞吐下降,行为接近“分布式版的热点行锁”。

  • 为什么 AT 适合“不想改代码”的团队? 把复杂度从业务挪到框架:开发只加注解,运维管 TC。代价是框架假设较强(SQL 可解析、有 undo 表、锁行为可接受)。用开发简单换运行时约束。

  • 为什么读可能脏? 全局锁防的是“脏写”,不自动让读等待。一阶段分支已本地提交,其他事务若直接读,可能看到“已改但全局未提交”的中间态。需要读侧显式加全局锁感知。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
跨库事务 + 团队不想写预留逻辑?
├─ 并发不高、SQL 常规、可接受全局锁
│   → Seata AT
└─ 高并发热点行 / 要极致性能
    → TCC 或 预扣+最终一致
其他分支:
├─ 想完全自动且可补偿 → AT
├─ 要业务可控预留 → TCC
└─ 长链路跨多系统 → Saga

【口述骨架】(5 分钟;框架 A,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“AT 是无侵入的自动补偿分布式事务”
0:30–1:30角色与流程TC/TM/RM + 一阶段 undo_log + 二阶段提交/回滚
1:30–2:30全局锁为什么需要、防什么
2:30–3:30与 TCC 对比无侵入 vs 性能;开发量 vs 灵活性
3:30–4:30瓶颈与补充高并发锁冲突;脏读与 @GlobalLock
4:30–5:00收尾“AT 用框架约束换开发简单,高并发热点仍要 TCC”

【关键数字】

参数经验值说明
undo_log必须建在业务库一阶段同事务写入
全局锁粒度行级(PK)热点行冲突大
AT 适用并发中低并发、非极端热点高并发转 TCC
二阶段提交异步删 undo_log提交路径轻
二阶段回滚生成反向 SQL依赖镜像质量
AT 适用 TPS数百~一两千(非热点短事务)热点行更敏感
undo 增长日事务×分支数50万×2≈100万行/日
高可用TC 集群 + 恢复任务防协调者单点

【追问链】(三层)

L1|“AT 会不会读到脏数据?” → 全局锁防脏写不防脏读。一阶段本地已提交,普通 SELECT 可能看到中间态。需要时用 @GlobalLock + SELECT FOR UPDATE 让读也等待全局锁。

L2|“undo_log 和业务 SQL 为什么必须同一本地事务?” → 因为 undo_log 唯一的用处是在「数据已经改了、但全局事务要回滚」时把数据改回来,镜像必须与这次修改同生共死。分两步提交会留两个窗口:业务 SQL 提交成功而 undo_log 丢了,二阶段回滚时无镜像可用,自动补偿当场失效,只能人工修数;反过来只写了 undo_log 而业务 SQL 失败,就留下一条对不上真实数据的镜像,误用会写坏行。同一本地事务让「改了什么」和「怎么改回来」原子落地,这也是关键数字把 undo_log 定为「必须建在业务库、一阶段同事务写入」的原因——它不是审计流水,而是这次回滚的唯一依据。

L3|“超高并发能不能硬上 AT?” → 热点行全局锁冲突会让吞吐塌陷,重试放大。应改为 TCC(业务冻结)或 Redis 预扣+异步最终一致。AT 的定位是“开发简单的中并发方案”,不是高并发银弹。

【评分标准】

档位答案特征
60 分知道 Seata AT、加注解就能用
80 分讲清 undo_log 一阶段+二阶段补偿、TC/TM/RM
95 分解释全局锁必要性与热点瓶颈;区分脏写/脏读;能与 TCC 按并发和侵入性选型

【关联题】

【自测】

  1. Seata AT 一阶段除了业务 SQL 还写了什么? 参考答案:undo_log(前后镜像),与业务 SQL 同一本地事务提交。
  2. AT 的全局锁主要防什么? 参考答案:防多个全局事务交错修改同一行导致 undo 镜像不可回滚。
  3. 什么情况下不该用 AT? 参考答案:热点行高并发、需要极致性能、或 SQL/数据源无法满足框架假设时,应改 TCC 或最终一致。

103. 下单-支付-出库跨 5 个服务,长链路怎么保证最终一致(Saga 模式) ​

【考察内容】Saga 是分布式事务全景里的高频补充题

【题目】一个下单链路串了 5 个服务(下单→支付→出库→发货→积分),中间某一步失败前面的都要补偿。不想用 2PC(全局锁太重),Saga 模式怎么做?正向操作和反向补偿怎么编排?什么场景适合 Saga、什么场景不适合?

【参考答案】

  1. 定义:把一个长事务拆成一系列本地事务(T1→T2→...→Tn),每个本地事务有对应的补偿操作(C1→C2→...→Cn);
  2. 执行:
    • 正向执行:T1、T2、...、Tn 依次执行;
    • 失败回滚:某一步失败,逆序执行已成功步骤的补偿(如 T3 失败 → C2、C1);
  3. 编排方式:
    • 编排(Orchestration):中央协调器(状态机/流程引擎)驱动各服务,简单直观;
    • 协同(Choreography):各服务通过事件驱动互相触发,去中心化但流程隐式;
  4. 优缺点:适合长事务/跨多系统(如旅游预订:机票+酒店+租车)、无全局锁;缺点:补偿逻辑复杂、无隔离性(中间态对外可见)、补偿失败需人工介入;
  5. 与 TCC 区别:TCC 是“预留-确认-取消”三阶段(短事务、资源预留);Saga 是“正向+补偿”(长事务、无预留)。
  6. 容量估算:长链路 5 个服务,每步 RT 50–200ms,则同步走完 0.25–1s,尚可;若含人工审核/外部机构,可能小时级,必须异步 Saga。补偿流量 ≈ 正向峰值 TPS×失败率;失败率 2% 时补偿量不是峰值×2%:一条 5 步链在中间失败时,前面已成功的 1–4 步都要逆序补偿,平均已成功步数≈2.5,故补偿 ops ≈ 峰值×失败率×平均已执行步数 ≈ 峰值×5%;只按 2% 预留容量,批量故障时补偿链路自己先堵死(正是 L2 警告的那个后果)。状态机存储:每个 saga 实例一行状态,日峰值订单 5 万则状态表 5 万行/日 起,含重试历史会更多。编排器(协调者)QPS ≈ 峰值×步骤通知次数,要水平扩展。
  7. 失败与降级:某一步失败 → 执行已完成步骤的补偿;补偿也失败 → 重试 + 死信 + 人工;超时未收到回调 → 主动查询或超时补偿;编排器宕机 → 状态持久化 + 恢复扫描继续。业务允许时,非关键步骤可标记为“可跳过”不阻塞主流程。监控:saga 完成率、平均完成时间、补偿率、卡单数(超时未终态)。

【原理溯源】

  • 为什么长事务不适合 2PC/TCC? 2PC 全程持锁,链路一长锁时间爆炸;TCC 要求每一步都可预留,跨外部系统(物流、第三方支付)往往做不到。Saga 用事后补偿替代事前预留,才能覆盖长链路。

  • 为什么补偿是逆序? 后续操作可能依赖前序结果(出库依赖支付成功)。失败时先撤销更近的副作用,再撤销更早的,避免“撤销了前提却留下依赖它的结果”。

  • 为什么 Saga 没有隔离性? 正向步骤本地已提交,中间态对外可见(“已扣款但未发货”)。补偿只能事后修复,无法像 2PC 那样隐藏中间态。所以业务要设计状态展示(“处理中”)。

  • 编排 vs 协同怎么选? 编排有中央状态机,流程清晰、易监控、易改;协同靠事件驱动,服务间更解耦但链路隐式、排障难。链路长且关键时,编排更可控。

  • 为什么补偿失败必须有兜底? 补偿本身也可能失败(网络、下游故障)。没有重试+对账+人工,Saga 只是“尽力而为”,资金类绝不能停在这一层。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
链路是否长(3+ 步)且跨多系统/含外部依赖?
├─ 否 → TCC / AT / 本地消息表
└─ 是 → Saga
    └─ 编排方式:
        ├─ 链路关键、要可视化、要易改 → Orchestration(状态机)
        └─ 高度自治、事件驱动、链路简单 → Choreography
    └─ 必配:每步补偿 + 补偿重试 + 对账 + 状态展示
不适合:要求中间态不可见、强隔离、补偿语义无法定义的场景

【口述骨架】(5 分钟;框架 A,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“Saga 用正向+逆序补偿做长事务最终一致”
0:30–1:30执行模型T1…Tn 与 C1…Cn,失败逆序补偿
1:30–2:30编排两种Orchestration vs Choreography
2:30–3:30与 TCC 对比无预留、中间态可见、适合长链路
3:30–4:30风险兜底补偿失败、对账、状态机
4:30–5:00收尾“Saga 换来了覆盖长链路的能力,代价是隔离性与补偿复杂度”

【关键数字】

参数经验值说明
链路长度3+ 步常见,5–10 步也有长事务典型
补偿延迟秒~分钟级看下游能力
补偿重试有限次 + 死信/人工不能只重试一次
中间态窗口秒~分钟业务需可展示
编排引擎状态机/流程引擎可观测性好
链路 RT步骤数×单步RT5×50–200ms=0.25–1s(同步可)
补偿流量≈ 峰值×失败率×平均已执行步数2% 失败、5 步链 → 约 5% 的处理能力,别只按 2% 预留
状态存储实例数/日 + 重试历史需终态可查询

【追问链】(三层)

L1|“Saga 和 TCC 最本质的区别?” → TCC 事前预留资源,Cancel 释放预留;Saga 事后反向业务补偿,无预留。TCC 适合短事务强约束资源;Saga 适合长链路跨系统。

L2|“补偿失败怎么办?” → 补偿失败是 Saga 最危险的状态:正向副作用已经对外可见(已扣款未发货),撤销又没成功,既非完成也非回滚。第 7 条给的处置是有限次重试 + 死信 + 人工,绝不能只重试一次就丢掉;同时业务状态机要能表达「补偿中/失败待人工」,并把卡单数(超时未终态)单独告警。补偿本身也要幂等,重试多次只撤销一次。容量上补偿流量不等于峰值×失败率:一条 5 步链中间失败时要逆序补偿前面已成功的 1–4 步(平均≈2.5 步),故补偿 ops ≈ 峰值×失败率×平均已执行步数 ≈ 峰值×5%;按 2% 预留,真出批量故障时补偿链路自己先堵死。兜底是超时主动查询与超时补偿,再加对账——原理溯源说得很直白:没有这些,Saga 只是「尽力而为」,资金类不能停在这一层。

L3|“Saga 中间态被用户看到了怎么办?” → 业务状态机展示“处理中/补偿中”,不把中间态当最终态;关键读可用只读快照或以最终态为准。

【评分标准】

档位答案特征
60 分知道正向+补偿
80 分能讲逆序补偿与编排/协同
95 分对比 TCC 的预留差异;指出无隔离性;补偿失败与对账兜底

【关联题】

【自测】

  1. Saga 失败时如何回滚? 参考答案:逆序执行已成功步骤的补偿操作。
  2. Saga 与 TCC 的核心差异? 参考答案:Saga 无预留、事后补偿、中间态可见、适合长事务;TCC 有预留、适合短事务强约束资源。
  3. 为什么 Saga 要有编排器? 参考答案:集中管理流程状态、补偿顺序与失败处理,可观测、易排障;比纯事件协同更可控。

104. 任务调度要抢锁,Redis 还是 ZooKeeper(分布式锁选型) ​

【考察内容】分布式锁选型是必考题

【题目】分布式定时任务要抢唯一执行权(同一时刻只能一个实例跑),有人建议用 Redis 锁,有人建议用 ZooKeeper 锁。两者机制分别是什么?各自的优劣(性能、强一致、丢锁风险、羊群效应)?什么业务场景选哪个?

【参考答案】

  1. Redis 锁:SET NX EX + Lua 解锁 + 看门狗续期(Redisson)。优点:性能高(毫秒级)、实现简单、无额外组件;缺点:主从切换可能丢锁、RedLock 争议大、可靠性依赖 Redis 集群;羊群效应这一格也要答(这四个维度缺一不可):Redis 锁没有前驱队列,等锁只能 SETNX 轮询,持锁者一释放就有 N 个等待者同时重试、把 Redis 打成热点;Redisson 用 pub/sub 订阅释放通知规避,但订阅消息本身可能丢,仍要保留兜底轮询(否则死等)
  2. ZooKeeper 锁:临时顺序节点 + watch(只监听前一个节点,前一个删除则获得锁)。优点:强一致(ZAB)、已提交的锁节点不会因选主/主从切换而丢、会话断开自动释放、无羊群效应(标准实现);缺点:性能较低、运维复杂;
  3. 选型:
    • 大多数业务(秒杀、任务调度防重)→ Redis 锁(性能+简单,容忍极小概率锁丢失或配合 DB 兜底);
    • 严格场景(金融主键、分布式任务强互斥、ZK 已存在)→ ZooKeeper/etcd 锁;
  4. 兜底哲学:锁是并发控制手段,关键正确性还需业务幂等/DB 约束兜底。
  5. 容量估算:分布式锁本身不是高并发热点解决方案,而是低频协调原语。定时任务抢锁:每分钟 1 次,锁操作 QPS 极低,Redis 单实例 10 万+ ops 绰绰有余;真正要估的是“持锁期间要做的事”是否超过锁 TTL,以及抢锁节点数(几十~几百实例的心跳/重试不会压垮 Redis,但 ZK 在频繁创建删除节点时 watch/节点操作有上限,一般任务调度够用)。若把 Redis 锁当每秒数万次业务互斥,会把 Redis 打成热点,应改为本地队列/分段锁。
  6. 失败与降级:Redis 锁在主从切换时可能丢锁(见 105 题)——业务必须幂等,锁只作优化而非唯一正确性来源;ZK 会话超时锁释放,客户端要处理 SessionExpired;锁服务不可用 → 定时任务的协调环节可降级为数据库唯一行锁/条件更新(UPDATE job_lock SET holder=?, expire=? WHERE job=? AND (holder IS NULL OR expire<NOW()),靠 DB 原子性选主)。「降级为单节点主备」这句要慎用:要降级的正是「谁是主」这个协调问题,主备本身仍需外置选举(DB/etcd)或写死+人工切换,否则等于把同一个问题挪了个位置。监控:锁获取失败率、持锁超时、任务重复执行次数。

【原理溯源】

  • 为什么 Redis 主从可能丢锁? 主从异步复制:客户端在主节点 SET 成功后主节点宕机,从节点未同步到锁就升主,锁记录消失。异步复制=不保证已确认写入在故障转移后仍在。

  • 为什么 ZK 不容易丢锁? ZAB 写入需过半节点持久化后才返回成功。主挂后新主一定包含已提交的锁节点。临时节点还绑定会话,会话断则自动删除,不会出现“持锁者死了锁还在”。

  • 标准 ZK 锁为什么无羊群效应? 竞争者创建临时顺序节点后只 watch 前驱。锁释放只唤醒下一个,而不是所有等待者。若所有人都 watch 同一节点才会惊群——那是错误实现。

  • 为什么 Redis 更快? 内存操作、协议轻、无需过半持久化。ZK 每次写要日志+过半 ACK,延迟更高。性能差一个数量级感。

  • 为什么“锁+幂等”是标配? 无论 Redis 还是 ZK,锁都可能因超时、会话抖动、GC 停顿失效。锁降低并发冲突,幂等保证即使双进也不会产生错误结果。

  • 元理论对应: 原理 4「时间差、多副本视图与一致性窗口」+原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§4.7+§6.7)

【选型判断树】

text
互斥的严格程度?
├─ 容忍极小概率重复,性能优先 → Redis 锁(Redisson)+ 幂等/DB 兜底
├─ 不能重复(金融、主键、强互斥任务)→ ZK / etcd
├─ 已有 ZK/etcd 集群 → 优先复用,少加组件
└─ 超高并发短临界区 → Redis;低频但严格 → ZK
通用:任何分布式锁都要业务幂等双保险

【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“Redis 锁性能好但可能丢锁;ZK 强一致但重”
0:30–1:30Redis 机制SET NX EX + Lua + 看门狗
1:30–2:30ZK 机制临时顺序节点 + 前驱 watch
2:30–3:30对比性能、丢锁、羊群
3:30–4:30选型与兜底按严格程度;锁+幂等
4:30–5:00收尾“锁是手段,正确性靠幂等与约束”

【关键数字】

参数经验值说明
Redis 锁延迟通常毫秒级远快于 ZK
ZK 锁延迟毫秒~十毫秒级含过半提交
Redisson 看门狗默认 30s,每 1/3 续期防提前过期
ZK 临时节点绑定会话,断开即删自动释放
任务抢锁抢不到直接跳过避免堆积
调度抢锁频率通常每分钟/每秒低频非高并发组件
Redis 锁 QPS需求≪Redis 能力勿把锁当业务互斥热点
TTL> 任务最长耗时+续期防执行中锁过期

【追问链】(三层)

L1|“Redis 主从切换一定会丢锁吗?” → 不一定,但异步复制下存在窗口。主写成功未同步就宕机时会丢。可用多副本/RedLock(有争议)/接受风险+幂等。

L2|“ZK 锁有羊群效应吗?” → 标准实现没有,错误实现才有。惊群的成因是所有等待者 watch 同一个节点,锁释放时 ZK 回调全部 watcher,几百个客户端同时惊起、同时去创建节点。正确写法是每个竞争者创建自己的临时顺序节点,只 watch 前驱那一个:锁释放只唤醒「下一个」,一次一个,天然形成排队——这就是参考答案第 2 条把「无羊群效应(标准实现)」列为 ZK 优点的原因。真正要付的代价不在惊群上:一是每次写要过半 ACK,锁延迟是毫秒到十几毫秒级,比 Redis 慢一个数量级;二是第 5 条提醒的,频繁创建删除节点会撞上 watch 与节点操作上限,别把它当每秒几万次的业务互斥用。

L3|“etcd 和 ZK 怎么比?” → 都是强一致(Raft/ZAB)+ 租约/临时节点思想。etcd 更云原生、运维与 API 更现代;ZK 生态老但成熟。锁语义相近。

【评分标准】

档位答案特征
60 分知道 Redis 能做锁
80 分能对比 Redis 与 ZK 机制和优缺点
95 分讲清异步复制丢锁根因、临时节点自动释放、羊群效应两侧(ZK 前驱 watch 无惊群;Redis 侧 SETNX 轮询会放大等待方 QPS、Redisson 订阅通知规避但丢消息要留兜底轮询)、锁+幂等

【关联题】

【自测】

  1. Redis 锁为什么可能丢? 参考答案:主从异步复制,主宕时未同步的锁在新主上不存在。
  2. ZK 锁如何避免惊群? 参考答案:临时顺序节点只 watch 前驱,锁释放只通知下一个竞争者。
  3. 任务调度抢锁你会推荐哪种? 参考答案:多数业务 Redis+幂等即可;强互斥或已有 ZK/etcd 则用后者。

105. 主从切换后锁丢了,有人提议 RedLock,靠谱吗(RedLock 争议) ​

【考察内容】RedLock 机制与争议(分布式锁正确性边界)

【题目】你们用 Redis 锁做分布式互斥,遇到主从切换锁丢失的问题。有同事提议用 Redis 官方提出的 RedLock。RedLock 是怎么工作的?它真的能解决锁丢失吗?业界为什么对它争议很大?更稳的做法是什么?

【参考答案】

  1. 目的:解决单 Redis 主从切换丢锁——在 N 个独立 Redis 节点(如 5 个,非主从)上加锁;
  2. 流程:客户端依次向 5 个节点执行 SET NX EX(带唯一 value)——过半(≥3)成功且总耗时小于锁过期时间才算加锁成功;锁的过期时间要减去获取锁的耗时;释放时向所有节点执行 Lua 解锁;
  3. 优点:不依赖主从复制,单节点故障不影响(过半即可);
  4. 争议(Martin Kleppmann 观点):
    • 时钟跳跃:依赖物理时钟,时钟回拨会导致锁提前失效;
    • GC/暂停:客户端加锁成功后发生长时间 GC/暂停,锁过期,其他客户端拿到锁——RedLock 无法解决“锁持有者暂停”的根本问题;
    • 认为“用 fencing token(单调递增令牌)配合存储校验”才是正确做法;
  5. 业界结论:RedLock 实现复杂、收益存疑,多数生产环境用“单 Redis 主从+Redisson 续期+业务幂等兜底”或换 ZK/etcd。
  6. 容量与时间数字:RedLock 要求向 N 个独立 Redis 主节点(通常 5)申请锁,总耗时 > 锁 TTL 的可能性随节点 RT 上升;客户端要等待多数派成功。高并发下每把锁 5 次网络往返会放大延迟——不适合超高 QPS 临界区。时钟假设:依赖各节点时钟大致同步,NTP 偏差大会导致锁提前过期。多数互联网业务用“单 Redis 锁+DB 唯一约束+业务幂等”即可,RedLock 在理论争议与运维复杂度下性价比低。
  7. 失败与降级:主从切换丢锁 → 业务幂等兜底;RedLock 部分节点不可用 → 可能拿不到多数派,锁获取失败率上升,需要降级策略;GC 停顿导致锁过期但客户端仍在执行 → fencing token/业务版本号防止旧持有者写入。原则:锁是效率工具,正确性靠幂等与约束。

【原理溯源】

  • RedLock 想解决什么? 单实例/主从 Redis 锁在故障转移时可能丢失。用多个独立节点过半成功,模拟共识写入,降低“单点丢锁”概率。

  • 为什么时钟是软肋? 锁的有效性依赖各节点过期时间对物理时钟的信任。时钟跳变、漂移会让“客户端以为还持有、服务器已认为过期”。

  • 为什么 GC 停顿致命? 进程停顿期间锁在服务端过期,另一客户端获得锁并写入;原客户端恢复后仍以为自己持锁继续写——双写。这是任何“基于过期时间的锁”的通病,不只是 RedLock。

  • Fencing token 怎么防旧锁写入? 每次加锁获得单调递增 token,写存储时携带 token,存储拒绝更小 token 的写。这样即使旧锁持有者恢复,也无法覆盖新持有者的写。正确性下沉到存储校验。

  • 为什么很多人最终不用 RedLock? 运维 5 个独立 Redis、网络假设严格、争议未消;而业务幂等+唯一约束已能兜住大多数场景。工程上要选“够用且简单”的正确性。

  • 元理论对应: 原理 4「时间差、多副本视图与一致性窗口」+原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§4.7+§6.7)

【选型判断树】

text
锁失效后果?
├─ 可幂等/可对账/可重试 → 单 Redis 主从 + Redisson + 幂等
├─ 要求严格互斥 → ZK/etcd(共识),或存储层 fencing token
├─ 想上 RedLock → 先确认:
│   ├─ 能接受复杂运维与争议
│   ├─ 时钟与 GC 风险仍有
│   └─ 仍要业务兜底
└─ 金融级 → 不单靠分布式锁,用 DB 约束/状态机/对账

【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“RedLock 试图用多独立节点过半解决丢锁,但有边界”
0:30–1:30机制5 节点、过半、耗时限制
1:30–2:30争议时钟跳跃、GC 停顿
2:30–3:30Fencing token令牌校验防旧写
3:30–4:30业界做法Redisson+幂等 或 ZK/etcd
4:30–5:00收尾“锁的正确性最终靠业务与存储约束”

【关键数字】

参数经验值说明
节点数常见 5(奇数,过半=3)独立实例非主从
加锁耗时必须 < 锁 TTL否则视为失败
客户端 GC任意长度都可能锁过期风险
Fencing token单调递增存储侧校验
生产主流Redisson 单实例 + 幂等简单够用
RedLock 节点通常 N=5,多数派=3独立主节点,非主从
性能每锁多次 RT,放大延迟高 QPS 临界区不适合
时钟依赖NTP 偏差影响 TTL争议点之一

【追问链】(三层)

L1|“RedLock 能 100% 防止两客户端同时持锁吗?” → 不能。时钟问题与进程停顿下仍可能出现。它是降低概率,不是绝对互斥证明。

L2|“不用 RedLock 怎么办?” → Martin Kleppmann 指出:依赖时钟的 RedLock 在 GC/网络延迟下仍可能不安全;Antirez 认为在工程假设下可用。务实立场:① 低频协调可试 RedLock,但必须幂等;② 高并发核心互斥用 DB 条件更新/唯一约束/版本号;③ 任何分布式锁都要回答“锁过期后原持有者还在写怎么办”。面试答争议题要给工程结论,不要只站队。

L3|“Fencing token 现实里怎么落地?” → 锁服务发令牌,业务写入时带上;存储(DB/对象存储)维护最大令牌并拒绝旧令牌。需要存储支持,不是纯 Redis 内能完成。

【评分标准】

档位答案特征
60 分知道 RedLock 是多节点加锁
80 分能讲过半机制与时钟/GC 争议
95 分讲 fencing token;指出基于过期的锁通病;给出工程务实结论

【关联题】

【自测】

  1. RedLock 加锁成功条件是什么? 参考答案:向多个独立节点 SET NX EX,过半成功且总耗时小于锁过期时间。
  2. Kleppmann 对 RedLock 的两大攻击点? 参考答案:物理时钟跳跃;客户端 GC/停顿导致锁过期后仍以为持有。
  3. 更稳的替代方案? 参考答案:ZK/etcd 强一致锁,或 Redis 锁+fencing token+业务幂等/存储约束。

106. 订单分库分表后主键要全局唯一、趋势递增(分布式 ID) ​

【考察内容】分布式 ID 是分布式基础必考题

【题目】订单表分库分表后,自增主键会重复。主键要求:全局唯一、趋势递增(利于索引)、不能依赖单点。请对比 UUID、数据库自增、号段模式、雪花算法的机制与取舍,并说明你们会选哪种、为什么?

【参考答案】

  1. UUID:本地生成、简单;缺点:无序、过长(36 字符)、非递增——不适合作为数据库主键(页分裂/索引性能差),适合无需排序的场景(如消息 ID);
  2. 数据库自增/号段:单库自增有瓶颈;号段模式(Leaf segment)——数据库维护“当前号段”(如每次取 1000 个 ID),应用内存发放,用完再取。优点:简单、趋势递增、性能好;缺点:依赖 DB、号段浪费;
  3. 雪花算法(Snowflake):64 位(1 符号 + 41 时间戳 + 10 机器 ID + 12 序列号),本地生成、趋势递增、高性能;缺点:时钟回拨、机器 ID 管理;
  4. Leaf-snowflake(美团):雪花+ZK 管理机器 ID+时钟回拨处理;
  5. 选型:纯数字、趋势递增、高性能、无中心依赖→雪花(本地生成);可接受中心 DB→号段;严格全局有序→只能单点发号(单库自增 / 单发号节点串行)——号段是多实例各持一段并发发放,全局会交错,只保证唯一+趋势递增;无顺序要求→UUID;
  6. 附加需求:业务可解析(时间/机器)、前端 JS 精度(雪花 64 位超 JS Number 安全整数,需转字符串)。
  7. 容量估算:雪花单机 4096/ms(12 位序列)≈ 409 万/s/机器,理论极高,实际受发号线程与打包限制;号段模式步长 1000 时,发号速率高则 DB 取号频率 = 速率/步长,例 10 万 ID/s → 每秒取号 100 次,DB 完全可扛;步长过大宕机浪费号段,过小压力大。UUID 存 36 字节 vs 雪花 8 字节:只算主键列本身,1 亿行差 1e8×(36−8)B ≈ 2.8GB;要差到「数十 GB」得把二级索引都算上(每个二级索引叶子都要存一份主键,约 10 个索引 → 28GB)。引用「数十 GB」必须带上索引个数这个前提。雪花 41 位时间约 69 年;10 位机器 ID=1024 实例,多机房要规划。
  8. 失败与降级:时钟回拨→等待/拒绝/降级号段(见 107);机器 ID 冲突→启动校验失败拒绝服务;号段 DB 挂→内存双缓冲可撑一段,用完失败,可降级雪花或备用 ID;前端精度→统一字符串传输。监控:发号 QPS、回拨次数、号段剩余水位、发号失败率。

【原理溯源】

  • 为什么趋势递增利于索引? B+ 树索引按序追加,页分裂少、缓存局部性好。随机 UUID 会导致频繁页分裂与随机 IO,写入与索引维护成本显著上升。
  • 号段模式为什么快? 一次 DB 取一批,应用内存发放,把“每 ID 一次 DB”降为“每批一次 DB”。瓶颈从 DB 读写变成号段获取频率。
  • 为什么雪花能本地生成? 时间戳+机器 ID+序列号在本地可保证全局唯一(前提机器 ID 唯一、时钟不回拨)。无中心依赖,吞吐高。
  • 为什么 UUID 不适合做主键? 随机性破坏索引顺序,存储更长,排序与范围查询差。可做业务号,但作聚簇主键性价比低(有序 UUID 变体可缓解)。
  • 前端精度坑是什么? 雪花是 64 位 long,超过 JS Number 安全整数(2⁵³-1),前端会精度丢失。传输要用字符串。

【选型判断树】

text
需要全局唯一 + 趋势递增?
├─ 否,只要唯一 → UUID(注意主键索引)
└─ 是
   ├─ 严格趋势递增、可依赖 DB 号段 → 号段(Leaf segment)
   ├─ 高性能、本地生成、能管机器 ID → 雪花 / Leaf-snowflake
   └─ 对外单号还要防遍历 → 加密/随机位后处理
分库分表主键:号段或雪花最常见

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“分库分表后自增失效,需要全局唯一且常要趋势递增”
0:30–1:30UUID简单但无序
1:30–2:30号段批量取号
2:30–3:30雪花64 位结构
3:30–4:30取舍与推荐按递增/中心依赖/性能
4:30–5:00收尾“索引友好与生成性能要一起看”

【关键数字】

参数经验值说明
UUID36 字符(标准)存储大、随机
号段步长100/1000/10000按发号速率
雪花 41 位时间约 69 年从自定义纪元起
序列号12 位 = 4096/ms/机器单机上限
JS 安全整数2⁵³-164 位 ID 要转字符串
雪花序列上限4096/ms/机器12 位
号段取号频率发号速率÷步长10万/s÷1000=100次/s
机器 ID 空间1024(10 位)多机房需规划
时间戳跨度41 位约 69 年自定义纪元

【追问链】(三层)

L1|“为什么号段模式性能好?” → 一次 DB 取一批,内存发放,显著降低 DB 访问频率;趋势递增对索引友好。

L2|“雪花机器 ID 冲突怎么办?” → 这一问只问「冲突」,要解决的是:① 机器 ID 唯一——启动/注册时向 ZK/etcd/配置中心抢号并校验,查到重复就拒绝启动(见第 8 条),运行期靠租约续约发现被占用;② 位宽规划——多机房要拆分那 10 位(如机房 3 位+机器 7 位),各机房各自从 0 编号必然撞号;③ 时间同步与监控(发号 QPS、冲突告警)。时钟回拨是第 107 题、中心化高可用是号段话题,别混进这一问。

L3|“号段 DB 挂了还能发号吗?” → 已取到的内存号段可继续发一段;用完则失败。可双缓冲预取下一段缓解,但最终仍依赖 DB 恢复或降级方案。

【评分标准】

档位答案特征
60 分知道 UUID 和自增
80 分能对比号段与雪花
95 分讲清趋势递增与索引;64 位结构与前端精度;机器 ID/回拨风险

【关联题】

【自测】

  1. UUID 作为数据库主键的主要问题? 参考答案:无序导致页分裂与索引性能差,且存储长。
  2. 号段模式如何降低 DB 压力? 参考答案:批量取号,应用内存发放,用完再取。
  3. 雪花算法 64 位如何划分? 参考答案:1 符号 + 41 时间戳 + 10 机器 ID + 12 序列号。

107. 服务器时钟回拨,生成的 ID 重复了(雪花算法回拨) ​

【考察内容】时钟回拨是雪花算法必考衍生题

【题目】线上服务器时钟发生回拨,用雪花算法生成的 ID 出现重复,业务数据被覆盖。时钟回拨为什么会造成重复?按回拨大小(毫秒级 vs 秒级以上)分别怎么处理?美团 Leaf 的方案是什么?

【参考答案】

  1. 问题本质:雪花 ID 高 41 位是时间戳,若本机时钟回退到上次生成 ID 的时间之前,时间戳变小,可能生成与历史重复的 ID;
  2. 处理策略(按回拨大小分档):
    • 小回拨(毫秒~秒级):等待——循环等时钟追平到上次时间戳再继续;或扩展序列号消化回拨区间(实现复杂,少用);
    • 大回拨(秒级以上):拒绝服务/报错,上层走兜底(号段/DB ID),同时告警排查 NTP;
    • 兜底切换:运行时报错拒绝生成,由业务决定是否降级;Leaf-snowflake 默认校验时间戳、回拨超过阈值直接抛异常,是否降级由业务侧决定;
  3. 预防:配置 NTP 同步、禁用人工改时间、容器环境注意时钟漂移;
  4. 最终防线:数据库主键冲突/唯一索引兜底报错——但该防线只在 INSERT 路径成立:题干已发生「业务数据被覆盖」,说明写路径多为 UPDATE/upsert,而唯一索引对 UPDATE 不报错,只会把别人的行改掉。所以要么把写入改成显式 INSERT(冲突即失败、拒绝重复),要么 INSERT ... ON DUPLICATE KEY UPDATE 且限定可改列;再配发号侧「时间戳单调+机器位校验」,才谈得上最终防线。只有 insert-only 的表(流水表、幂等记录表)才是唯一索引天然兜底。
  5. 容量/阈值数字:小回拨等待成本=回拨毫秒数,一般业务可接受 <1s;Leaf 等实现常设阈值(如 5ms~可配秒级)。若回拨 10s,等待 10s 会让下单线程阻塞超时,必须拒绝。序列号 12 位=4096/ms,无法“消化”秒级回拨。发号 QPS 回拨期间降为 0(等待)或直接失败——容量规划要把“回拨故障下的失败预算”纳入可用性目标,而不是假设永远 100% 可发号。
  6. 失败与降级:小回拨等待追平;大回拨抛错+告警,业务切换号段/DB ID 或快速失败;NTP 偏差监控前置;DB 唯一索引作最后防线防覆盖。容器环境禁止随意 date 改时间;宿主机与容器时钟源要统一。演练:模拟回拨验证业务是否走降级。

【原理溯源】

  • 为什么会重复? 同一机器 ID + 序列号重置后,若时间戳回到过去某个已用过的值,就可能生成完全相同的 64 位 ID。时间戳是单调性假设,回拨打破假设。
  • 为什么小回拨可以等待? 回拨几十毫秒~几秒,等待代价可接受;追平后时间戳继续前进,不会撞历史。阻塞生成线程即可。
  • 为什么大回拨不能死等? 等待时间过长会拖垮业务线程,甚至超时。此时应快速失败,把决策权交给上层(降级发号或报错),而不是无限阻塞。
  • Leaf 为什么默认抛异常而不是静默切换号段? 静默切换会隐藏故障、改变 ID 形态,可能影响依赖方。显式失败+告警更符合运维可控原则;降级策略应由业务明确选择。
  • 为什么唯一索引是最后防线? 即使算法层漏判,DB 主键冲突会阻止覆盖。正确性不能只靠发号器。

【选型判断树】

text
检测到时钟回拨:
├─ 回拨 ≤ 小阈值(如 5ms~几秒)→ 等待追平
├─ 回拨 > 阈值 → 抛错/拒绝生成 + 告警
│   └─ 业务侧:降级号段/DB ID 或快速失败
└─ 预防:NTP、监控偏差、唯一索引兜底

【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“雪花依赖时钟单调,回拨会撞历史 ID”
0:30–1:30成因时间戳变小 + 机器 ID/序列重复
1:30–2:30分档处理小回拨等待,大回拨拒绝
2:30–3:30Leaf校验+抛异常,业务决定降级
3:30–4:30预防与兜底NTP、唯一索引
4:30–5:00收尾“发号器也要按故障设计,不能假设时钟完美”

【关键数字】

参数经验值说明
小回拨阈值毫秒~秒级可配Leaf 有阈值
等待成本线程阻塞到追平只适合很短
大回拨直接失败防无限等
NTP 偏差应监控容器更敏感
序列空间4096/ms,无法消化秒级回拨不要迷信序列号方案
最后防线DB 唯一索引仅 INSERT/insert-only 表成立:UPDATE·upsert 路径下唯一索引不报错,只会改掉别人的行

【追问链】(三层)

L1|“回拨期间业务下单失败怎么办?” → 可降级到号段/DB ID,或快速失败重试;要有预案与告警,不能静默生成重复 ID。

L2|“能不能用序列号空间消化任意回拨?” → 只能消化很小回拨且实现复杂;大回拨时序列号不够,且时间戳单调假设已被打破,继续发号有重复风险。通用策略仍是小回拨等待、大回拨快速失败并切换备用发号,同时修时钟。面试加分:解释 Leaf 默认抛异常是“可观测、可控降级”优于“静默切换”。

L3|“多机房机器 ID 不够怎么办?” → 调整位分配(扩大机器 ID,牺牲时间或序列号),或用 ZK 动态分配,或改用号段。

【评分标准】

档位答案特征
60 分知道时钟回拨会重复
80 分能按回拨大小分档处理
95 分讲清时间戳单调假设;Leaf 抛异常策略;NTP 预防与唯一索引兜底

【关联题】

【自测】

  1. 时钟回拨为何导致 ID 重复? 参考答案:时间戳回到已使用过的值,同机器 ID 与序列可能生成相同 64 位 ID。
  2. 小回拨与大回拨分别怎么处理? 参考答案:小回拨等待追平;大回拨拒绝生成并告警,可降级号段。
  3. 发号器的最后一道防线是什么? 参考答案:数据库主键/唯一索引冲突即报错——但这条只在 INSERT 路径成立;题干那种「被覆盖」的场景写路径是 UPDATE/upsert,唯一索引不会报错,得改成显式 INSERT 冲突即失败(或限定 ON DUPLICATE 可改列)+发号侧时间戳单调校验。

108. 缓存集群扩容,一半缓存突然全失效(一致性哈希) ​

【考察内容】一致性哈希是分布式基础必考题

【题目】缓存集群从 8 台扩到 12 台,如果按“key 取模节点数”做路由,扩容后大部分 key 会路由到新节点,缓存瞬间大量失效、数据库被打穿。怎么解决?一致性哈希的原理是什么?它引入了哪些新问题(数据倾斜、虚拟节点)?

【参考答案】

  1. 背景:普通哈希取模 hash(key) % N,节点数 N 一变,很大比例 key 的映射同时改变(比例=1−gcd(N_old,N_new)/N_new,见第 5 条),需要大量数据迁移——不可接受;
  2. 原理:
    • 把哈希值空间组织成环(0~2³²-1);
    • 节点哈希后落在环上;
    • key 哈希后顺时针找第一个节点作为归属;
    • 扩容/宕机时,只影响该节点沿环方向相邻的一小段数据(顺时针/逆时针取决于实现约定,讲清自己用的是哪种即可),其他节点不动;
  3. 问题与解决:
    • 数据倾斜——引入虚拟节点(每个物理节点映射 N 个虚拟节点),打散分布;
  4. 应用:Redis Cluster 实际用 slot(16384 个哈希槽)方案(迁移以 slot 为单位,更可控);一致性哈希常用于负载均衡、分布式缓存中间件、分库分表扩容。
  5. 容量估算:取模扩容时期望迁移比例 = 1 − gcd(N_old, N_new)/N_new(8→12 为 1−4/12 ≈ 67%;只有新旧节点数互质时才接近 1−1/N_new 的“几乎全部”);一致性哈希每增/删 1 个节点只迁约 1/N 的数据(N=节点数);8→12 是一次加 4 个节点,期望迁移量约 4/12 ≈ 1/3,而不是 1/12、更不是取模那种接近全量。虚拟节点常见每物理节点 100~200 个,分布更均匀。Redis Cluster 16384 slot,扩节点时迁移 slot 数量可预估。缓存失效导致的 DB 回源 QPS:若缓存读 10 万 QPS、失效比例 50%,DB 瞬间可能被打到数万——必须限流与本地缓存兜底。
  6. 失败与降级:扩容/缩容用灰度迁移 slot/虚拟节点,避免一次性全量;迁移期双读/双写或短暂容忍 miss;DB 前加限流与热点本地缓存;预热新节点;监控缓存命中率、DB QPS、迁移进度。故障节点下线同样只影响局部,但仍要在业务侧容忍 miss。

【原理溯源】

  • 为什么取模扩容会雪崩? hash % N,N 一变映射关系就大面积改变,迁移比例=1−gcd(N_old,N_new)/N_new:8→9(互质)=1−1/9 ≈ 89%,8→12 是 2/3,成倍扩容 8→16 也要搬一半——无论哪种都远超缓存能承受的失效面,请求集中打回 DB。根因是取模把节点数写进了哈希函数。

  • 一致性哈希为何只影响局部? 节点与 key 都映射到同一环;增删节点只改变相邻弧段的归属,大部分 key 仍指向原节点。迁移量从“几乎全部”降到“局部”。

  • 为什么会有数据倾斜? 节点少时,环上分布不均,某些节点管很大弧段。虚拟节点让一个物理节点在环上出现多次,统计上更均匀。

  • 为什么 Redis Cluster 用 slot 而不是纯一致性哈希? slot 固定 16384 个,迁移与管理粒度可控、可显式指派,运维更直观;一致性哈希更“自动”,但迁移控制与可观测性弱一些。

  • 和“缓存穿透/雪崩”什么关系? 本题的“一半缓存突然失效”是路由层雪崩,不是单 key 过期雪崩。治理要在分片策略层,而不只是随机 TTL。

  • 元理论对应: 原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§6.7)

【选型判断树】

text
节点数会变化吗?
├─ 永远不变 → 取模也可(少见)
└─ 会扩缩容/故障
    ├─ Redis Cluster → 16384 slot(生产主流)
    ├─ 客户端分片/代理 → 一致性哈希 + 虚拟节点
    └─ 分库分表路由 → 一致性哈希或 slot 思想
同时:热点 key、大 key 要另外治理

【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
0:00–0:30定性“取模让节点数进入哈希,扩容即大面积失效”
0:30–1:30环与顺时针最小化迁移
1:30–2:30虚拟节点解倾斜
2:30–3:30Redis slot16384 槽
3:30–4:30场景缓存、负载均衡、分库分表
4:30–5:00收尾“分片键与迁移策略决定扩容是否安全”

【关键数字】

参数经验值说明
哈希环空间常 2³²经典论文设定
Redis slot16384固定
虚拟节点每物理节点数十~数百看倾斜程度
扩容迁移每增删 1 台约 1/N 量级N=节点数;一次加 k 台约 k/N_new
取模扩容1−gcd(N_old,N_new)/N_new8→12 约 67%;应避免
一致性哈希迁移增/删 k 台迁约 k/N_new8→12 是加 4 台:4/12≈1/3(1/N 只适用 8→9 增删 1 台)

【追问链】(三层)

L1|“虚拟节点是不是越多越好?” → 不是。太多会增加元数据与计算开销。够用即可,以分布均匀为目标。

L2|“一致性哈希能完全避免缓存失效吗?” → 不能,它只把失效从「几乎全量」压到「局部」。增删节点时只有一小段弧上的 key 换归属,但这些 key 在新节点上本来就没数据,照样回源。量级按第 5 条算:增删 1 台迁约 1/N;8 台扩到 12 台是加 4 台,期望迁移 4/12≈1/3,不是 1/12,更不是取模那种接近全量;虚拟节点重分布时也会再挪动一部分 key。所以治理不能停在「建环」上:迁移期要预热新节点、DB 前限流、热点走本地缓存兜底——第 5 条的算例就是缓存读 10 万 QPS、失效 50% 时 DB 会被打到约 5 万 QPS,这正是「局部失效」没兜住的后果。

L3|“Redis Cluster 为什么是 16384 个槽?” → 官方在心跳包大小、迁移粒度、集群规模之间的工程折中。槽少则心跳小,16384 对常见集群足够。

【评分标准】

档位答案特征
60 分知道取模扩容有问题
80 分能讲一致性哈希环与虚拟节点
95 分对比 Redis slot;迁移量级;预热/双写等扩容实践

【关联题】

【自测】

  1. hash % N 扩容的主要问题? 参考答案:N 一变,比例为 1−gcd(N_old,N_new)/N_new 的 key 要重新映射(8→12 约 67%,只有互质时才接近全量),缓存集中失效打穿 DB。
  2. 虚拟节点解决什么? 参考答案:节点少时数据倾斜,让分布更均匀。
  3. Redis Cluster 用什么替代纯一致性哈希? 参考答案:16384 个哈希槽(slot),以槽为迁移与管理单元。

持续学习,持续积累。