Skip to content

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

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 的“不一致”只在分区发生时出现;分区恢复后系统会通过补偿达成最终一致(BASE 的最终一致性);
  4. 应用:交易/支付强一致场景偏 CP(或关键路径强一致+旁路最终一致);互联网读多写少场景通常 AP+最终一致;注册中心选 AP(Eureka)可容忍“短暂读到旧实例列表”,保可用性。配置中心要单独答一句(题干点了三个组件,不能只答两个):配置中心的写侧宜 CP 或「AP+版本号推送」——写错配置的影响面大于读到旧值,宁可写入失败也不能让各实例拿到不一致版本;读侧必须 AP(推送失败时要能用本地缓存快照继续启动与运行),否则配置中心抖动会拖挂全站。
  5. 容量/可用性数字锚点:Eureka 默认 30s 心跳、90s 摘除、自我保护 15 分钟成功率 <85% 触发;Nacos 临时实例 5s 心跳、15s 不健康、30s 摘除;ZooKeeper/etcd 选主期间不可写通常秒级~十几秒——这是选 CP 时要付的可用性代价。注册中心读 QPS 往往远高于写(服务发现),AP 系统可横向扩展读;写(注册/心跳)频率 ≈ 实例数/心跳间隔,例 1 万实例、Eureka 30s 心跳 ≈ 333 次/s(换成自建 3s 心跳就是 3300 次/s,两者别混),需要评估元数据存储与心跳通道容量。分区场景下的错误预算:调用方超时+重试次数要能覆盖注册中心短暂不一致窗口。
  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? 扣款场景下读到旧余额可能导致超卖/透支,资金错误的代价远大于“分区期暂时拒绝服务”。所以关键写路径宁可阻塞,也要保证读到的是最新已提交数据。

【选型判断树】

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

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

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

【口述骨架】(5 分钟)

时间任务内容
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 临时实例心跳默认 5s,15s 不健康,30s 摘除临时实例偏 AP
网络分区在生产大规模集群“一定会遇到”不是假设,是运维现实
ZK 选主不可写秒级~十几秒CP 代价
注册写压力实例数÷心跳间隔1万/3s≈3300写/s

【追问链】(三层)

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 的工程化落地
  • 同簇组件: 第 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 分钟更新——用户无感。用这个可容忍的延迟换可用性和吞吐,是业务语义允许下的最优解。

【选型判断树】

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

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

【口述骨架】(5 分钟)

时间任务内容
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 分能说出 B/A/S 三字母,知道“最终一致”
80 分能对比 ACID 与 BASE,并举出本地消息表/对账等工程手段
95 分讲清“局部 ACID + 全局 BASE”;强调最终一致必须有收敛机制;能按业务划分强一致/最终一致场景;能定义 SLA

【关联题】

  • 上游理论: 第 97 题(CAP)——BASE 是 CAP 中选 AP 后的一致性补救
  • 工程落地: 第 99 题(事务选型)、第 93 题(本地消息表)、第 92 题(事务消息)
  • 下游保障: 第 110 题(幂等)、第 219 题(对账)
  • 同簇场景: 第 109 题(微服务数据一致性)——BASE 的典型应用面

【自测】

  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 判给 TCC:别再写「或 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 无法与本地数据库事务原子提交:“先写库后发消息”会在写库成功后、发消息前宕机而丢消息;“先发消息后写库”会在消息发出后、写库失败时产生脏消息。本地消息表把“业务写入”和“待发消息”放进同一个本地事务,再由后台任务扫描投递,从而消除这个窗口。

【选型判断树】

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

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

【口述骨架】(5 分钟)

时间任务内容
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 事务消息回查默认 15 次,间隔递增(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 次)加定时兜底;③ 超过本题关键数字的预留超时(30s–5min)仍未收敛,就自动 Cancel 解冻,绝不能让库存和额度长期悬着;④ 对账兜底(资金类 T+0)发现「预留已扣但下游没到账」走差错。本参考答案第 5 条的降级顺序里,库存超卖风险不可降级。

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

【评分标准】

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

【关联题】

  • 同一知识簇(建议连读): 第 58 题(跨库一致性)→ 第 100 题(2PC/3PC)→ 第 101 题(TCC)→ 第 102 题(Seata AT)→ 第 103 题(Saga)→ 第 92 题(RocketMQ 事务消息)→ 第 93 题(本地消息表)
  • 上游理论: 第 97 题(CAP)、第 98 题(BASE)——选型的理论基础
  • 下游落地: 第 110 题(接口幂等)、第 219 题(日终对账)——一致性的保障手段
  • 金融场景版: 第 218 题(跨行转账跨 5 个系统)

【自测】

  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 没有多数派机制,协调者只与部分参与者连通时仍会脑裂不一致(见缺陷③)——「阻塞」与「脑裂」是两种并存的失效,不是「而非」。生产预案:协调者集群化、事务日志持久化、超时后人工/自动恢复流程;核心资金路径宁可失败也不盲提交。

【原理溯源】

  • 为什么 2PC 必然同步阻塞? Prepare 阶段参与者“执行但不提交”,本质是持有行锁/写隔离,等待协调者指令。锁必须持有到第二阶段,否则别人一改就破坏了“准备态”。锁持有时间 ≥ 一次网络往返 + 协调者决策时间,高并发下锁等待队列爆炸。
  • 为什么协调者是单点致命问题? 2PC 没有内置的协调者选举/复制协议。协调者在发完部分 commit 后宕机,参与者既不能自行提交(可能有人收到 abort)也不能回滚(可能有人已 commit)——卡在不确定态,只能等人工或协调者恢复。
  • 脑裂是怎么发生的? 协调者与部分参与者网络分区:协调者以为全失败发 abort,另一部分参与者以为全成功已 commit。没有多数派投票机制,两边各自推进,数据不一致。Raft/Paxos 用多数派解决的正是这类问题。
  • 3PC 的超时为什么不够? 3PC 让参与者在超时后“默认提交”,减少了阻塞,但默认提交在分区下可能错:一侧超时提交、另一侧实际应 abort。它把“阻塞”换成了“可能不一致”,没有消除分区下的根本矛盾。多一轮 RPC 还增加了延迟。
  • 为什么数据库内部还用类 2PC? 因为数据库内部(redo log prepare → commit)协调者与参与者在同一实例/强绑定的存储引擎内,没有跨机网络分区问题,或由存储层的共识协议保证。跨服务 2PC 才是灾难现场。

【选型判断树】

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

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

【口述骨架】(5 分钟)

时间任务内容
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 替代

【关联题】

  • 同簇协议: 第 101 题(TCC)、第 102 题(Seata AT)、第 103 题(Saga)、第 99 题(选型)
  • 上游理论: 第 97 题(CAP)、第 98 题(BASE)
  • 对比共识: 注册中心 CP 组件(ZK/etcd)用多数派解决脑裂,2PC 没有
  • 落地替代: 第 92/93 题(消息最终一致)

【自测】

  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,则 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 的正确性核心,不是可选优化。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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,防长期占用
空回滚/悬挂处理状态表一次查询必须持久化
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 次/秒的重投,不设上限等于自己再造一波洪峰;③ 长时间进不去 Confirm,就靠 Try 预留超时(30s–5min)自动 Cancel 释放冻结,避免资金与库存悬空,同时告警转人工;④ 监控 Confirm 成功率与状态表积压。

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

【评分标准】

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

【关联题】

  • 同簇: 第 99 题(选型)、第 100 题(2PC)、第 102 题(Seata AT)、第 103 题(Saga)
  • 上游理论: 第 97/98 题(CAP/BASE)
  • 落地保障: 第 110 题(幂等)、第 104 题(分布式锁,有时叠加)
  • 场景版: 第 218 题(跨行转账)

【自测】

  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 表、锁行为可接受)。用开发简单换运行时约束。
  • 为什么读可能脏? 全局锁防的是“脏写”,不自动让读等待。一阶段分支已本地提交,其他事务若直接读,可能看到“已改但全局未提交”的中间态。需要读侧显式加全局锁感知。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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 按并发和侵入性选型

【关联题】

  • 同簇: 第 99 题(选型)、第 100 题(2PC 原理)、第 101 题(TCC)、第 103 题(Saga)
  • 对比: TCC 更适合高并发资金/库存;AT 更适合快速接入
  • 落地: 第 110 题(幂等)、第 118 题(并发控制)

【自测】

  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 只是“尽力而为”,资金类绝不能停在这一层。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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 的预留差异;指出无隔离性;补偿失败与对账兜底

【关联题】

  • 同簇: 第 99 题(选型)、第 100 题(2PC)、第 101 题(TCC)、第 102 题(AT)
  • 落地: 第 109 题(事件一致性)、第 110 题(幂等)

【自测】

  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 停顿失效。锁降低并发冲突,幂等保证即使双进也不会产生错误结果。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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 订阅通知规避但丢消息要留兜底轮询)、锁+幂等

【关联题】

  • 进阶: 第 105 题(RedLock)
  • 同簇: 第 114 题(任务调度)、第 118 题(并发控制)、第 110 题(幂等)

【自测】

  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、网络假设严格、争议未消;而业务幂等+唯一约束已能兜住大多数场景。工程上要选“够用且简单”的正确性。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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;指出基于过期的锁通病;给出工程务实结论

【关联题】

  • 上游: 第 104 题(锁选型)
  • 相关: 第 116 题(时钟)、第 107 题(回拨)、第 110 题(幂等)

【自测】

  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→号段;严格有序→号段/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^53-1),前端会精度丢失。传输要用字符串。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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^53-164 位 ID 要转字符串
雪花序列上限4096/ms/机器12 位
号段取号频率发号速率÷步长10万/s÷1000=100次/s
机器 ID 空间1024(10 位)多机房需规划
时间戳跨度41 位约 69 年自定义纪元

【追问链】(三层)

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

L2|“雪花机器 ID 冲突怎么办?” → 必须解决:① 机器 ID 唯一(ZK/etcd/配置中心注册,或启动时抢号);② 时钟回拨检测(小回拨等待,大回拨拒绝并降级);③ 发号服务高可用(双缓冲号段或雪花本地生成);④ 监控告警。若多机房,还要考虑机器 ID 位宽与网络时间同步策略。选型对比:要中心化可控用号段;要本地高性能用雪花。

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

【评分标准】

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

【关联题】

  • 衍生: 第 107 题(时钟回拨)、第 121 题(为何要分布式 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 主键冲突会阻止覆盖。正确性不能只靠发号器。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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 偏差应监控容器更敏感
最终防线DB 唯一索引防覆盖
序列空间4096/ms,无法消化秒级回拨不要迷信序列号方案
最后防线DB 唯一索引仅 INSERT/insert-only 表成立:UPDATE·upsert 路径下唯一索引不报错,只会改掉别人的行

【追问链】(三层)

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

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

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

【评分标准】

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

【关联题】

  • 上游: 第 106 题(雪花)、第 121 题(ID 方案)
  • 相关: 第 116 题(时钟不一致)、第 105 题(锁与时钟)

【自测】

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

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

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

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

【参考答案】

  1. 背景:普通哈希取模 hash(key) % N,节点数 N 变化时,绝大多数 key 的映射都变了,需要大量数据迁移——不可接受;
  2. 原理:
    • 把哈希值空间组织成环(0~2^32-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 一变,几乎所有 key 重新取模,映射关系大面积改变。缓存同时失效,请求打到 DB。取模把节点数写进了哈希函数。
  • 一致性哈希为何只影响局部? 节点与 key 都映射到同一环;增删节点只改变相邻弧段的归属,大部分 key 仍指向原节点。迁移量从“几乎全部”降到“局部”。
  • 为什么会有数据倾斜? 节点少时,环上分布不均,某些节点管很大弧段。虚拟节点让一个物理节点在环上出现多次,统计上更均匀。
  • 为什么 Redis Cluster 用 slot 而不是纯一致性哈希? slot 固定 16384 个,迁移与管理粒度可控、可显式指派,运维更直观;一致性哈希更“自动”,但迁移控制与可观测性弱一些。
  • 和“缓存穿透/雪崩”什么关系? 本题的“一半缓存突然失效”是路由层雪崩,不是单 key 过期雪崩。治理要在分片策略层,而不只是随机 TTL。

【选型判断树】

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

【口述骨架】(5 分钟)

时间任务内容
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^32经典论文设定
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;迁移量级;预热/双写等扩容实践

【关联题】

  • 相关: 第 114 题(分片任务)、缓存雪崩/穿透题、分库分表题
  • ID/路由: 第 106 题

【自测】

  1. hash % N 扩容的主要问题? 参考答案:N 变化导致绝大多数 key 重新映射,缓存同时失效打穿 DB。
  2. 虚拟节点解决什么? 参考答案:节点少时数据倾斜,让分布更均匀。
  3. Redis Cluster 用什么替代纯一致性哈希? 参考答案:16384 个哈希槽(slot),以槽为迁移与管理单元。

109. 服务 A 改了数据,B/C/D 都要跟着变(微服务数据一致性) ​

【考察内容】微服务数据一致性是架构岗必考

【题目】微服务 A 修改了核心数据(如用户信息),下游 B(订单)、C(消息)、D(统计)都要基于新数据。A 不能直接改 B/C/D 的库,怎么保证数据最终一致?请给出完整方案(事件驱动、本地消息表、事务消息、订阅消费)与失败兜底。

【参考答案】

  1. 核心思想:事件驱动 + 最终一致,避免跨服务强事务;
  2. 方案组合:
    • 可靠事件(本地消息表/事务消息):A 的业务与事件同事务落库,事件异步投递到 MQ,B/C 订阅消费——保证“事件不丢”;
    • 消费幂等:B/C 用业务唯一键去重,保证重复事件只生效一次;
    • Outbox 模式:业务表旁建 outbox 表,同事务写入,relay(如 Debezium 监听 binlog)把 outbox 记录转发 MQ——比本地消息表更解耦;
    • 对账兜底:定时任务比对 A 与 B/C 的数据,发现不一致自动补偿(重发事件/直接修复);
    • 状态机推进:B/C 各自维护状态(如“待同步→已同步”),对账按状态处理;
  3. 什么时候不能最终一致:资金强一致(扣款+入账)用 TCC/Seata;大部分业务最终一致足够;
  4. 设计原则:明确每个数据的所有者(单一写入方),其他服务只读或事件同步。
  5. 容量/延迟估算:A 服务写峰值 2000 TPS,若每条变更需通知 3 个下游,则事件峰值 6000 msg/s;B/C/D 消费能力必须覆盖。不一致窗口:MQ 正常秒级;本地消息表秒~十秒;下游批量同步可到分钟级。对账任务按数据量:日变更要先从峰值折回均值(取峰值的 1/10 ≈ 200 TPS)再乘全天,200×86400 ≈ 1700 万事件/日;若按峰值 2000 TPS 直乘会得出 1.7 亿,那是几乎不可能的上限口径,别拿它做容量。对账不能每晚全量扫,应增量+抽样+关键账户全量。缓存失效风暴:热点 key 更新时多个下游同时失效,可用延迟双删/版本号。
  6. 失败与降级:消息丢失→本地消息表/事务消息;下游消费失败→重试+死信+补偿;缓存与 DB 不一致→延迟双删/订阅 binlog;对账发现差异→差错工单自动补偿。非核心下游可降级关闭;核心扣减不可最终一致了事,要强一致或预留。

【原理溯源】

  • 为什么不让 A 直接改 B/C/D 的库? 破坏服务边界与自治:耦合表结构、部署与故障相互拖累,且形成事实上的分布式事务。单一数据所有者才能让演进与故障隔离。
  • 为什么“先写库再发 MQ”不够? 写库成功、发消息前宕机,消息丢失。本地消息表/Outbox 把“业务变更”和“待发事件”放进同一本地事务,消除丢消息窗口。
  • Outbox 与本地消息表差别? 思路相同;Outbox 常配合 CDC(Debezium 读 binlog)转发,应用不必写投递任务,业务与发布管道解耦。
  • 为什么消费必须幂等? MQ 至少一次投递 + 网络重试 + 生产者重发,重复是常态。没有幂等,重复事件会把下游状态打乱。
  • 为什么还要对账? 消息可能进死信、消费者可能 bug、人工可能改数。对账是最终一致的最后收敛器,按所有者数据修复下游。

【选型判断树】

数据被多个服务使用?
├─ 是否资金/强约束?
│   ├─ 是 → TCC / Seata 强一致链路
│   └─ 否 → 事件最终一致
└─ 最终一致选型:
    ├─ 应用要简单、已有 MQ → 本地消息表 / 事务消息
    ├─ 要与业务代码解耦、有 CDC → Outbox + Debezium
    └─ 必配:消费幂等 + 对账 + 状态机

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“单一数据所有者 + 事件驱动最终一致”
0:30–1:30为什么不能直改库耦合与分布式事务代价
1:30–2:30可靠事件本地消息表 / Outbox
2:30–3:30消费与收敛幂等、状态机、对账
3:30–4:30强一致边界资金场景例外
4:30–5:00收尾“事件不丢 + 消费幂等 + 对账兜底 三件套”

【关键数字】

参数经验值说明
事件延迟秒级看 MQ 与消费能力
对账周期分钟~小时;资金 T+0/T+1按业务
重试与死信有限重试后进 DLQ必须可人工重投
Outbox relayDebezium/CanalCDC 转发
幂等键业务单号/版本号不要用随机 UUID 作业务幂等
事件峰值写TPS×下游数2000×3=6000 msg/s
不一致窗口秒~分钟级按业务 SLA
对账策略增量+抽样+关键全量避免全表扫

【追问链】(三层)

L1|“同步 RPC 调下游改数据不行吗?” → 行但差:形成调用耦合与分布式事务,下游慢/挂会拖垮 A,扩下游要改 A。事件异步化更稳。

L2|“消费失败进死信后业务就一直不一致?” → 不会「一直」,但前提是死信不是终点。第 2 条的方案组合里,死信之后还有两件事要做:① 有限重试后进 DLQ,而 DLQ 必须可人工或自动重投(关键数字写死了这条),重投时带上原业务单号或版本号,配合消费幂等保证只生效一次;② 对账作为最后收敛器——原理溯源说得很清楚,消息可能进死信、消费者可能有 bug、人工可能改数,对账按数据所有者 A 的数据修复下游,并落差错工单(第 6 条)。下游还要用状态机(待同步→已同步)表达中间态,补投才敢做。窗口按第 5 条是秒到分钟级:死信一旦持续积压并越过这个窗口就该告警,否则「最终一致」会悄悄变成「永远不一致」。

L3|“以谁的数据为准修不一致?” → 以数据所有者(A)为准。下游用事件重放或拉取全量修复,并记录审计。禁止下游反向覆盖所有者。

【评分标准】

档位答案特征
60 分知道发 MQ 异步通知下游
80 分能讲本地消息表/Outbox+幂等
95 分强调单一数据所有者;对账与状态机闭环;指出资金场景不能最终一致

【关联题】

  • 同簇: 第 99 题(事务选型)、第 98 题(BASE)、第 110 题(幂等)
  • 组件: 第 92 题(事务消息)、第 93 题(本地消息表)
  • 相关: 第 119 题(缓存一致性)

【自测】

  1. 为什么 A 不能直接改 B 的数据库? 参考答案:破坏服务自治,引入耦合与分布式事务,故障相互拖累。
  2. Outbox 模式如何保证事件不丢? 参考答案:业务变更与 outbox 记录同一本地事务提交,再由 relay/CDC 转发 MQ。
  3. 下游消费乱序/重复怎么办? 参考答案:幂等键去重;必要时按版本号/状态机拒绝旧事件;对账兜底修复。

110. 扣款接口被重复调用就重复扣钱(接口幂等) ​

【考察内容】接口幂等是后端最高频场景题之一

【题目】扣款接口因为网络重试、用户双击、消息重投被调用了两次,用户被扣了两笔钱。接口幂等怎么设计?幂等 key 怎么生成与存储?数据库唯一约束、Redis 标记、状态机三种方案各适用什么场景?

【参考答案】

  1. 幂等定义:同一操作执行多次与执行一次效果相同;
  2. 实现层次:
    • 前端/网关:按钮防重复提交、Token 机制——业务前先申请唯一 token,请求携带,服务端校验并删除(一次性消费必须原子:Redis≥6.2 用 GETDEL,更早版本用 Lua 的 EXISTS+DEL;SETNX 只能表达“首次写入成功”的占位语义);
    • 应用层:幂等键(Idempotency-Key)——客户端传业务唯一键(订单号/支付流水号),服务端以“唯一键+处理结果”存 Redis,重复请求直接返回上次结果;
    • 数据库层(最终防线):业务唯一键唯一索引,重复插入报错捕获;状态机幂等——处理前检查状态;乐观锁 version 字段;
    • 消息消费幂等:唯一键+去重表/Redis SETNX;
  3. 关键设计:
    • 幂等键要选业务唯一维度(订单号、支付单号),不能每次请求随机生成;
    • 查询接口天然幂等;扣款/发券/下单必须幂等;
    • 返回语义:重复请求返回与首次一致的结果(查已有结果而非报错);
  4. 验证:用相同请求重复调用测试。
  5. 容量估算:幂等层要按峰值接口 QPS 设计,而不是平均。扣款峰值 3000 QPS 时:唯一键写 DB 需要该表扛 3000 插入/s(应分表或热点优化);Redis SETNX 预检可挡住重复,但最终仍要 DB 唯一约束。幂等记录保留时间 ≥ 业务重试窗口(支付常见 24h~7 天)。存储:3000/s×86400×7 天若全存会巨大,需按天分区/定期清理成功记录。条件更新 UPDATE ... WHERE status=待支付 的行锁热点要评估。
  6. 失败与降级:Redis 幂等层挂→降级只靠 DB 唯一索引(限流保护);重复请求视为成功返回(查询真实状态)而不是报错,避免客户端无限重试;分布式锁过期导致并发重复→仍靠 DB 约束兜底。监控:幂等冲突次数(异常高可能是攻击或客户端 bug)、扣款重复拦截数。

【原理溯源】

  • 为什么会重复? 网络超时重试、用户双击、MQ 至少一次投递、网关重发——分布式里“调用一次”不保证“到达一次”。幂等是对不可避免的重复的防御。
  • 为什么前端防重不够? 可被脚本绕过、多端不同步、网关/客户端仍会重试。前端只能降低概率,正确性必须在服务端。
  • 为什么幂等键不能随机生成? 随机键每次请求都不同,服务端无法识别“这是同一次业务操作”。必须用业务唯一标识才能把重复请求映射到同一操作。
  • 为什么重复请求应返回首次结果而不是报错? 客户端超时重试时,往往不知道首次是否成功。返回首次结果符合“效果与一次相同”的幂等语义,也避免把成功误判为失败。
  • 为什么 DB 唯一索引是最终防线? Redis 可能丢标记、应用可能并发双写。唯一索引在存储引擎层保证“同一业务键只插一次”,冲突即失败,不会静默双写。

【选型判断树】

接口是否会产生副作用(写/扣减/发券)?
├─ 否(查询)→ 天然幂等
└─ 是
   ├─ 请求前可获取一次性凭证 → Token 机制(防进入)
   ├─ 有明确业务唯一键 → 幂等键 + 结果缓存(Redis)
   ├─ 落库可建唯一约束 → DB 唯一索引(最终防线)
   └─ 有明确状态流转 → 状态机 + 乐观锁
生产建议:Token/幂等键 + DB 唯一索引 多层叠加

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“分布式重复不可避免,幂等是写接口标配”
0:30–1:30重复来源重试、双击、消息重投
1:30–3:00三层设计Token / 幂等键 / 唯一索引与状态机
3:00–4:00关键细节业务唯一键、返回首次结果
4:00–4:30验证相同请求压两次
4:30–5:00收尾“锁防互斥,幂等防重复,二者互补”

【关键数字】

参数经验值说明
幂等记录 TTL大于业务重试窗口(如 24h)过短会漏
Redis 标记SETNX + 过期注意与 DB 对齐
消息重复至少一次语义下常态必须消费幂等
重试间隔指数退避避免重试风暴
唯一索引(user_id, biz_no) 类复合键按业务设计
幂等层容量= 峰值接口 QPS3000 QPS 峰值
记录保留≥ 重试窗口支付 24h–7d
最后防线DB 唯一键/条件更新缓存过期也能防重

【追问链】(三层)

L1|“只有 Redis 幂等标记够吗?” → 不够最终。Redis 可能丢数据或标记过期。关键扣减必须有 DB 唯一索引/状态机兜底。

L2|“幂等键存什么?” → 三选一或组合:① DB 唯一索引(最可靠,注意热点);② 状态机条件更新(仅允许合法迁移一次);③ Redis SETNX+TTL(快,但是缓存,过期后失效,必须 DB 兜底)。扣款场景推荐:请求唯一键+DB 唯一流水+账户余额条件更新,三重防护。返回策略:重复请求返回首次结果(或查询态),保证幂等语义。

L3|“并发两个相同请求同时到达怎么办?” → SETNX/唯一索引做互斥:一个成功占位,另一个失败或等待后读结果。幂等也要考虑并发竞态,不只是串行重试。

【评分标准】

档位答案特征
60 分知道要做防重复,会提前端防抖
80 分讲清 Token + 幂等键 + 唯一索引
95 分强调业务唯一键与返回首次结果;并发竞态;Redis 过期与 DB 兜底关系

【关联题】

  • 同簇: 第 117 题(防重复下单)、第 118 题(并发控制)、第 114 题(任务幂等)
  • 上游: 第 98 题(BASE)、第 101 题(TCC 幂等)
  • 消息: MQ 重复消费相关题

【自测】

  1. 幂等键为什么不能用每次随机生成的 UUID? 参考答案:无法标识同一次业务操作,重复请求会被当成新请求。应使用订单号等业务唯一键。
  2. 重复扣款请求应报错还是返回首次结果? 参考答案:返回与首次一致的结果,符合幂等语义,避免客户端把成功当失败。
  3. 幂等的最终防线通常是什么? 参考答案:数据库唯一索引或状态机/乐观锁,在存储层挡住重复写入。

111. 用户登录后换台机器就掉登录(分布式 Session) ​

【考察内容】分布式会话设计

【题目】服务多实例部署后出现怪现象:用户这次请求被路由到 A 机器登录了,下次请求被路由到 B 机器,B 不认识他,又要重新登录。怎么解决登录态共享?Session 集中存储、JWT、Cookie 方案各自怎么做、怎么选?

【参考答案】

  1. 方案一:Session 集中存储——Session 放 Redis(spring-session),所有实例共享;登出/踢人可主动失效;适合传统 Web;
  2. 方案二:无状态 Token(JWT)——用户信息+签名放进 Token,服务端不存 Session,任意实例验签即可;适合移动端/开放 API/跨域;注意:JWT 无法主动失效(需黑名单/短有效期+refresh);
  3. 方案三:Cookie 加密(老式)——会话数据加密后放客户端;安全风险高,不推荐;
  4. 选型建议:PC 端管理系统(需要会话管理/强制下线)→Redis Session;开放平台/移动端→JWT;两者可结合(JWT 认证 + Redis 存可撤销状态);
  5. 安全要点:Token 存 HttpOnly Cookie 防 XSS、HTTPS、过期时间合理、防 CSRF(SameSite/CSRF Token)。
  6. 容量估算:日活 100 万、平均会话 2h、登录态更新频率低,则 Session 存储约 100 万级 key;若每 session 2KB,约 2GB,Redis 单实例可容。QPS:登录/鉴权校验是高频读,峰值可能 数万 QPS——Session 读要走 Redis,可再加本地缓存(短 TTL)——但这一条会直接打掉第 1 条卖点「登出/踢人可主动失效」:TTL>0 就存在窗口,被踢/被封用户在窗口内仍被本地缓存判为有效。两种写法二选一:① 本地只缓存「JWT 验签结果+会话版本号」,踢人时版本号自增,本地比对不一致立即失效(可撤销态不落本地);② 对安全等级高的会话明确声明「可撤销态不走本地缓存」,只接受 Redis 的读放大与扩容成本。过期清理:惰性+定期。JWT 无服务端存储则 Redis 压力小,但吊销难,需要黑名单(登出/封禁时写 blacklist,其容量按需吊销比例估算)。
  7. 失败与降级:Redis Session 挂→本地无法共享登录态,可降级 JWT 验签模式或要求重新登录;多机房 Session 复制/双活;滚动发布要保证 Session 兼容。监控:Session 命中率、Redis 内存、登录失败率。安全:cookie httpOnly/secure,Session 固定攻击防护。

【原理溯源】

  • 为什么会掉登录? 默认 Session 在单机进程内存。负载均衡把后续请求打到其他实例,该实例没有这份 Session。根因是会话状态未共享。
  • 为什么 Redis Session 能解决? 把 Session 读写集中到共享存储,所有实例同一来源。代价是每次请求多一次 Redis 访问,Redis 成为可用性依赖。
  • 为什么 JWT 是无状态的? 用户信息与过期时间写在 Token 内并签名,服务端只验签不查库/缓存。水平扩展容易,但无法简单“删除”已签发 Token。
  • 为什么 JWT 主动失效难? 签名有效期内 Token 一直被接受。要么短有效期+refresh 轮换,要么维护黑名单/版本号,部分回到有状态。
  • 为什么两者可结合? JWT 负责可扩展认证,Redis 存“会话版本/黑名单”做可控失效。无状态验签 + 有状态撤销兼顾扩展与安全。

【选型判断树】

是否需要强制下线/会话管理/设备管理?
├─ 是(管理后台、安全要求高)→ Redis Session
└─ 否,要跨端/开放 API/高扩展 → JWT
    └─ 仍要可撤销?→ JWT + 短有效期 + refresh + 黑名单/版本
不推荐:把大段会话明文/弱加密放 Cookie

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“多实例掉登录本质是会话未共享”
0:30–1:30Redis Session集中存储、可踢人
1:30–2:30JWT无状态验签、难撤销
2:30–3:30选型管理端 vs 开放端
3:30–4:30安全HttpOnly、HTTPS、CSRF、过期
4:30–5:00收尾“可撤销性与无状态性要做取舍或混合”

【关键数字】

参数经验值说明
Session 默认过期30 分钟闲置~数小时按安全策略
JWT Access Token5 分钟~1 小时宜短
Refresh Token数天~数周用于续期
Redis Session 延迟1ms 级每请求一次
Cookie 属性HttpOnly + Secure + SameSite防 XSS/CSRF
Session 存储日活×平均体积100万×2KB≈2GB
鉴权读 QPS峰值可达数万Redis+本地缓存(只缓存验签结果+会话版本号;或明确声明可撤销态不缓存,否则吊销有 TTL 窗口)
JWT 吊销黑名单/版本号无状态的代价

【追问链】(三层)

L1|“JWT 能直接踢掉某个用户吗?” → 默认不能。需要黑名单、会话版本号,或缩短 access 有效期并使 refresh 失效。

L2|“Redis 挂了会怎样?” → 共享登录态当场失效:各实例本地都没有这份 Session,用户表现为集体掉线、要重新登录;混合方案里那份黑名单/会话版本号也读不到,吊销能力同时没了,所以必须提前定策略(读不到是拒绝还是放行,按业务选)。第 7 条给的降级是「切 JWT 本地验签或要求重新登录」——注意两者代价不对等:失去的是吊销能力,不是认证能力。缓解要按第 6 条的量级来做:日活 100 万、每 session 2KB 约 2GB,Redis 放得下,但鉴权峰值可达数万 QPS,等于把它放在核心链路上,因此要集群/双活;本地缓存只能放「验签结果+会话版本号」(踢人时版本号自增即失效)——直接缓存整个 Session 会打掉「登出/踢人可主动失效」这个卖点,安全等级高的会话应声明可撤销态不落本地。一句话结论:这里的 Redis 不是缓存,是认证依赖。

L3|“为什么不用粘性 Session(IP Hash)?” → 可临时缓解,但实例重启/扩缩容仍掉线,故障转移也破坏会话。粘性是优化不是方案,正解仍是共享存储或无状态 Token。

【评分标准】

档位答案特征
60 分知道 Session 要放 Redis
80 分能对比 Redis Session 与 JWT
95 分讲清 JWT 难撤销及混合方案;安全属性;指出粘性会话局限

【关联题】

  • 相关: 第 112 题(基础设施)、第 122 题(调用容错,Redis 依赖)
  • 安全向: 网关鉴权相关题
  • 架构: 多实例无状态化原则

【自测】

  1. 多实例掉登录的根因是什么? 参考答案:Session 存在单机内存,请求路由到其他实例后读不到。
  2. JWT 最大的工程缺点是什么? 参考答案:默认无法主动失效/踢人,需黑名单或短有效期+refresh。
  3. 管理后台强制下线你会选什么? 参考答案:Redis Session 或 JWT+Redis 会话版本/黑名单,保证可撤销。

112. 几十个微服务互相调用,怎么知道对方在哪、配置怎么统一改(注册/配置中心) ​

【考察内容】微服务基础设施

【题目】公司微服务拆了 30 个,A 调用 B 时怎么知道 B 的地址?B 的 IP 变了怎么办?另一个问题:限流阈值、功能开关散落在各服务配置文件里,改一次要重新发版。请说明注册中心和配置中心分别解决什么问题、怎么工作。

【参考答案】

  1. 注册中心(服务发现):服务启动时注册自己的地址(IP:port),消费者从注册中心获取服务实例列表并负载均衡调用;核心功能:服务注册、心跳续约、健康检查、摘除故障节点、服务订阅/变更通知。解决“服务地址动态变化”问题;
    • 选型:Nacos(功能全、AP/CP 可切换)、Eureka(AP)、ZooKeeper(CP)、Consul(多数据中心);
  2. 配置中心(动态配置):配置集中管理,修改后动态刷新到运行中的服务(@RefreshScope/长轮询),无需重启;解决“改配置要发版重启”问题;
    • 选型:Apollo、Nacos、Spring Cloud Config;
  3. 两者关系:Nacos 可同时提供;注册中心侧重“实例地址”,配置中心侧重“运行参数”。
  4. 容量估算:1000 个服务实例、心跳 5s,则注册中心写 ≈ 200 写/s,读(服务发现+配置拉取)可能 数千~数万 QPS(每个调用方缓存实例列表,本地缓存后打到注册中心的实际读会低很多)。配置变更频率低,但变更推送要快:长轮询/WebSocket,秒级生效。元数据存储容量小,重点在可用性与推送而不是吞吐。多机房部署时注册中心本身要跨机房同步策略。
  5. 失败与降级:注册中心宕机→客户端本地缓存实例列表继续调用(关键);配置中心宕机→应用使用本地快照启动与运行;推送失败→客户端定时全量拉取兜底。核心原则:注册/配置中心是控制面,挂了不应导致数据面全灭。监控:心跳丢失率、推送延迟、客户端缓存命中。

【原理溯源】

  • 为什么硬编码 IP 不行? 扩缩容、容器重建、故障迁移都会改地址。硬编码导致发布耦合与频繁改配置。注册中心把“地址发现”从静态配置变成动态数据。
  • 注册中心四步闭环? 注册(上线声明)→ 心跳(证明活着)→ 健康检查/摘除(去掉死实例)→ 订阅通知(消费者更新列表)。缺一环就会调到死实例或发现不了新实例。
  • 为什么配置要动态刷新? 限流阈值、开关、超时若绑定发布,响应慢且风险大。配置中心把“变更”从“发版”解耦,支持灰度与快速回滚。
  • 为什么两者常一起谈却要分清? 都是“运行时元数据”,但生命周期与一致性要求不同:实例列表要快速摘除故障;配置变更要审计、灰度、可回滚。混用容易把配置当发现、把发现当配置。
  • 为什么 Nacos 能两者通吃? 统一元数据与控制面,降低组件数;但仍要在使用上区分领域模型(服务 vs 配置项)。

【选型判断树】

要解决什么问题?
├─ 服务在哪、实例变了 → 注册中心
│   ├─ 偏可用、可容忍短暂旧列表 → Nacos 临时 / Eureka
│   └─ 偏强一致 → Nacos CP / ZK / etcd
└─ 参数/开关/阈值变更 → 配置中心
    ├─ 要灰度/审计/权限强 → Apollo
    └─ 已用 Nacos、需求标准 → Nacos 配置

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“注册中心管实例地址,配置中心管运行参数”
0:30–1:30服务发现闭环注册、心跳、摘除、订阅
1:30–2:30配置动态化集中管理、热更新
2:30–3:30选型Nacos/Apollo/Eureka/ZK
3:30–4:30关系与边界同为控制面,职责不同
4:30–5:00收尾“动态发现 + 动态配置,才能支撑多实例快速交付”

【关键数字】

参数经验值说明
心跳/摘除秒级~30s 级看组件与模式
配置推送秒级(长轮询/推送)可灰度
Nacos 一体注册+配置减少组件
Apollo 灰度支持实例/分组发布工程强
本地缓存消费者必须有注册中心短暂不可用仍可调用
注册写 QPS实例数÷心跳间隔1000÷5=200/s
发现读 QPS调用方×查找频率本地缓存后大幅下降

【追问链】(三层)

L1|“注册中心和配置中心能不能只上一个?” → 可以用 Nacos 一体;但概念上仍要分清“服务实例”与“配置项”。只做配置或只做发现的场景也可单上对应组件。

L2|“实例列表多久更新一次?” → 两条通道叠加:变更推送 + 客户端定时全量核对。正常路径是订阅——实例上下线时注册中心把变更推给消费者,长轮询/推送是秒级到;客户端再按周期做一次全量比对,防止漏推送。这就是第 1 条「注册→心跳→健康检查摘除→订阅通知」四步闭环的最后一环。端到端时延主要由心跳决定,关键数字给的窗口是秒级到 30 秒级,坏实例通常要等续约超时才被摘掉。压力不用担心:第 4 条算过 1000 个实例、5s 心跳只有 200 写/秒,而发现侧的数千~数万 QPS 读被客户端本地缓存挡掉(关键数字:本地缓存消费者必须有)。也正因这份缓存,注册中心短暂不可用时调用不会全断。

L3|“配置改错了怎么回滚?” → 配置中心应有版本、灰度、一键回滚、审计。代码侧避免启动时一次性读死,保证热更新与回滚真正生效。

【评分标准】

档位答案特征
60 分知道有注册中心、配置中心两个东西
80 分能分别讲清解决什么、常见组件
95 分讲注册闭环与动态刷新机制;强调本地缓存与灰度回滚;能区分职责

【关联题】

  • 故障表现: 第 113 题(注册中心宕机)
  • 治理: 第 120 题(配置灰度)
  • 理论: 第 97 题(CAP,Eureka AP vs ZK CP)

【自测】

  1. 注册中心解决什么核心问题? 参考答案:服务实例地址动态变化时的自动发现与更新,避免硬编码 IP。
  2. 配置中心的核心价值是什么? 参考答案:参数集中管理与动态刷新,改配置不必发版重启,可灰度回滚。
  3. Nacos 的特点是什么? 参考答案:注册与配置一体,AP/CP 可切换,国内微服务常用。

113. 半夜注册中心宕机,服务调用会全断吗(注册中心故障) ​

【考察内容】服务发现的容错设计

【题目】半夜注册中心(Nacos)宕机,值班的人慌了:“服务之间是不是全断了?”请分析:注册中心挂了之后,服务间调用会中断吗?为什么?消费者端的缓存机制、心跳续约、摘除逻辑分别是什么?

【参考答案】

  1. 答案:短时间不会全断,取决于实现:
    • Eureka:消费者本地缓存服务实例列表,注册中心挂了不影响已缓存的调用(AP 设计,自我保护模式“宁可保留旧实例也不摘除”);
    • Nacos:临时实例模式下,消费者有本地缓存+订阅推送,短暂不可用期间仍可调用已缓存实例;服务上下线感知延迟;
    • ZooKeeper:客户端缓存连接,ZK 挂后调用依赖缓存的节点列表(CP,但新发现不可用);
  2. 真实风险:长时间不可用——新服务上线无法被发现、故障实例无法摘除(调用到已挂实例报错);
  3. 增强方案:消费者侧多级容错(本地配置兜底实例列表)、重试与故障转移、熔断降级;
  4. 心跳续约与摘除(题干直问的两问,按三步答全):续约=实例按周期上报心跳(Eureka 客户端 30s 续约、Nacos 临时实例 5s 心跳);标记不健康=超时未续约先标「不健康」但不立刻摘(Nacos 15s 标不健康、30s 才移除;Eureka 90s 才出注册表);摘除=到阈值才从列表移除并推送变更,且自我保护/阈值保护会在心跳成功率过低时停止摘除(网络抖动比实例真挂更常见)。别答成「心跳一跳没回就摘」。顺带结论:注册中心高可用(集群部署)仍是必须的;但调用链不因注册中心单点而中断是分布式系统设计原则。
  5. 容量/故障窗口数字:Eureka 客户端默认缓存服务列表,注册中心宕机后在缓存 TTL/续约周期内调用不受影响(约分钟级窗口);Nacos 客户端也有本地缓存与快照。调用方 QPS 本身不打注册中心,而是打服务提供者——注册中心挂了影响的是扩缩容与故障摘除,不是已知实例的转发。容量上要评估:故障期间实例下线无法被摘除,靠调用方超时+重试+熔断消化,重试放大系数约 1+失败率×重试次数,不能超过剩余健康实例容量。
  6. 失败与降级:注册中心宕机→继续用本地缓存;实例下线→调用方超时/熔断/重试其他实例;长时间故障→运维静态路由表应急;恢复后自动重新注册。禁止:注册中心一挂就重启所有应用。演练:拔掉注册中心,验证核心交易是否仍可用。

【原理溯源】

  • 为什么短时间不断? 服务发现是“元数据”,不是每次调用都同步询问注册中心。消费者拿到列表后本地缓存,调用直接打实例。注册中心挂了,已有缓存仍可用。
  • 心跳续约做什么? 实例定期上报存活;超时未续约被标记不健康/摘除。注册中心宕机期间心跳失败,但消费者不一定立刻清空本地列表。
  • 摘除逻辑的风险? 若故障检测过于激进,网络抖动会误摘;过于保守会调到死实例。Eureka 自我保护在成功率低时停止摘除,优先保可用。
  • 为什么长时间宕机会出事? 新实例无法注册(扩容无效)、旧坏实例无法摘除、配置/元数据停止更新。系统进入“冻结的旧视图”,故障放大。
  • 为什么还要集群部署注册中心? 缓存只是短暂缓冲,不能替代高可用控制面。生产必须多副本、跨机房,并演练注册中心故障。

【选型判断树】

注册中心宕机应急判断:
├─ 影响时间 < 本地缓存有效期/可接受窗口
│   → 业务通常无感;尽快恢复控制面
└─ 长时间不可用
    ├─ 消费者有静态兜底列表 → 可继续调用
    ├─ 无兜底 → 新变更冻结;调用依赖旧缓存+重试
    └─ 同时启动:集群恢复、必要时本地直连应急

设计层:消费者必须本地缓存 + 重试 + 熔断

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30结论先行“短时间不会全断,因为调用走本地缓存”
0:30–1:30机制缓存、心跳、摘除
1:30–2:30组件差异Eureka/Nacos/ZK
2:30–3:30真实风险长时间不可用的影响
3:30–4:30增强静态兜底、重试熔断、集群
4:30–5:00收尾“控制面挂,数据面要有缓冲”

【关键数字】

参数经验值说明
Eureka 自我保护15 分钟成功率 <85%停止摘除
Nacos 临时实例5s 心跳,15s 不健康,30s 摘除可调
本地缓存必配故障缓冲
调用超时百 ms 级快失败
注册中心集群≥3 节点高可用
客户端缓存窗口分钟级(默认配置相关)挂了不应立刻全断
重试放大1+失败率×重试次数勿超过剩余容量
应急静态路由/本地快照长故障预案

【追问链】(三层)

L1|“挂了 10 分钟会怎样?” → 若实例稳定,多数调用仍靠缓存成功;新发布/扩缩容失败;部分坏实例无法摘除导致错误率上升。要监控注册中心与错误率。

L2|“缓存里的实例已经挂了怎么办?” → 这条只能靠调用侧兜——注册中心自己正挂着,缓存列表里的坏实例没人摘。四步:① 百 ms 级超时后立刻重试到列表内其他实例,但要先按第 5 条核算放大系数(约 1 + 失败率 × 重试次数),不能超过剩余健康实例的容量,否则从「一个实例挂」变成「全挂」;② 客户端对连续失败的实例做临时剔除或降权,下个刷新周期再放回;③ 熔断保住线程池不被拖死,避免局部故障放大;④ 平时把优雅下线做扎实:先注销、等消费方列表刷新到空窗,再停进程。还要记得 Eureka 自我保护在 15 分钟内成功率 <85% 时停止摘除,这时客户端剔除与重试是唯一防线。

L3|“要不要给消费者配静态 IP 列表?” → 可作为最后兜底(本地配置/环境变量),尤其核心链路。但要防止与动态发现长期不一致,仅应急使用。

【评分标准】

档位答案特征
60 分觉得会全挂或全不挂,理由含糊
80 分知道本地缓存所以短时间不断
95 分讲心跳摘除与长时间风险;组件差异;给出静态兜底与熔断重试

【关联题】

  • 上游: 第 112 题(注册/配置中心)、第 97 题(AP/CP)
  • 容错: 第 122 题(超时重试熔断)
  • 运维: 高可用与故障演练相关

【自测】

  1. 注册中心宕机为什么服务调用不一定中断? 参考答案:消费者本地缓存了实例列表,调用不依赖每次询问注册中心。
  2. 长期宕机的主要风险是什么? 参考答案:新实例无法发现、故障实例无法摘除、元数据停止更新,错误率上升。
  3. 消费者侧应做哪些容错? 参考答案:本地缓存、静态兜底列表、超时重试、熔断降级。

114. 订单超时扫描任务在多台机器上重复执行(分布式定时任务) ​

【考察内容】分布式任务调度

【题目】订单超时关单的定时任务每分钟扫一次,但服务部署了 10 个实例——10 个实例同时扫,订单被重复处理。怎么让定时任务只执行一次?数据量大时怎么分片并行?XXL-JOB 的分片广播机制是什么?

【参考答案】

  1. 问题:多实例同时跑同一个定时任务会重复执行——需要分布式调度;
  2. 方案:
    • 分布式锁:任务执行前抢锁(Redis SETNX),抢到才执行——简单,适合单点执行任务;
    • 分布式调度框架(XXL-JOB/Elastic-Job):
      • 任务注册到调度中心,调度中心按路由策略分发给执行器(轮询/一致性哈希);
      • 分片广播:任务拆 N 片,每个执行器处理一片(如按订单 ID 取模),提高吞吐;
      • 失败重试、任务依赖、超时控制;
    • Elastic-Job:基于 ZK 分片,分片总数固定,实例增减自动重分片;
  3. 幂等兜底:即使调度框架保证了不重复,任务处理本身也要幂等(如关单前检查订单状态);
  4. 监控:任务执行记录、失败告警、执行耗时。
  5. 容量估算:日订单 50 万、超时关单 30%,扫描任务每分钟扫一次未支付超时单,先按题干口径把量算出来:日订单 50 万×30%=15 万条超时单/日,每分钟一轮 → 每轮均值只有 15万÷1440≈104 行;「峰值数万行/轮」要成立,得让单分钟占日量 20%–33%(峰值是均值的 288–480 倍),不现实。正常写「数百~数千行/轮」;只有把「积压若干轮」或「扫描窗口放宽到小时级」当明确前提时才写数万——按错值配分片数会把并行度高配一到两个数量级。分片扫描:按订单 ID 哈希到多机,每实例每轮处理 N 千行,处理能力要 > 到达速率。分布式锁/任务调度组件(XXL-Job、ElasticJob、Quartz 集群)自身操作 QPS 很低,容量瓶颈在扫描 SQL 与关单写。避免全表扫:索引 (status, expire_time) + 时间游标。
  6. 失败与降级:锁失效导致短暂重复执行→关单接口幂等+状态条件更新;调度中心宕机→备节点接管;扫描 SQL 慢→分页/分片/限流;MQ 延迟消息已有兜底扫描时注意勿双开冲突(统一入口)。监控:任务执行成功率、重复执行次数、关单耗时、积压待关单数。

【原理溯源】

  • 为什么会重复执行? 每个 JVM 的 @Scheduled 都独立触发,彼此不知情。单机互斥原语(synchronized)锁不住其他进程。
  • 分布式锁方案的本质? 用跨进程互斥把“多实例触发”收敛为“一实例执行”。简单,但吞吐受限于单实例,且锁可靠性依赖 Redis。
  • 调度框架解决了什么? 把“谁来跑、跑哪部分、失败怎么办”集中编排:路由策略防重复、分片提升并行、失败重试与日志可运维。
  • 分片广播为什么快? 把大扫描按 ID 取模拆给多执行器并行处理,避免单点扫描超时。分片键要均匀,数据倾斜要治理。
  • 为什么仍要任务幂等? 框架只能保证“调度层尽量一次”,不能消除重试、重复消息、业务并发。关单仍要检查状态,幂等是业务正确性底线。

【选型判断树】

任务特征?
├─ 必须单实例执行、频率低 → 分布式锁(Redis)+ 幂等
├─ 要可视化、重试、路由 → XXL-JOB
├─ 要自动分片与弹性 → Elastic-Job / XXL-JOB 分片广播
└─ 数据量极大 → 分片并行 + 幂等 + 告警监控
任何方案都要:业务幂等

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“本地定时器多实例必然重复,需要分布式调度”
0:30–1:30锁方案抢锁执行,简单但单点
1:30–2:30调度框架XXL-JOB 路由与重试
2:30–3:30分片广播取模并行
3:30–4:30幂等与监控状态检查、失败告警
4:30–5:00收尾“调度防重复,幂等保正确”

【关键数字】

参数经验值说明
扫描任务周期1 分钟常见按超时精度
分片数常等于执行器实例数可配置
锁等待0(抢不到就跳过)避免堆积
重试次数有限次+告警防止风暴
大表扫描按 ID 游标分批避免全表锁
扫描到达量日订单×超时比例÷轮次50万×30%≈15万/日 ÷1440 轮 ≈104 行/轮(均值),正常写「数百~数千行/轮」;「数万行/轮」只有把积压若干轮或小时级窗口当前提才成立,别据此配分片数
关键索引(status, expire_time)防全表扫
正确性幂等+条件更新锁只是减少重复不是唯一保证

【追问链】(三层)

L1|“只靠 Redis 锁够吗?” → 单点任务够用,但锁丢失/过期仍可能重复,所以处理必须幂等;要监控与执行日志。

L2|“分片不均匀怎么办?” → 先分清是「分片键选错」还是「数据本身有热点」。第 5 条的场景:日订单 50 万、超时约 30%(50 万×30%≈15 万单/日)要扫描关单,按订单 ID 取模各片本应等量;若按用户或商户维度分片,大商户订单整块落进同一片,就是选键问题。四个手段:① 分片键换成均匀且与业务无关的维度(订单 ID 哈希),必要时二次拆片;② 分片数与执行器实例数对齐,热片配更多执行器;③ 片内按 ID 游标分批并走 (status, expire_time) 索引,避免单片慢 SQL 拖死整轮;④ 每轮设上限并限流,慢片下一轮追赶。监控要盯各片耗时与积压待关单数——原理溯源那句「数据倾斜要治理」就是这条。

L3|“调度中心挂了任务会怎样?” → 调度中心需集群;任务定义与执行日志持久化;可降级为各实例抢锁模式。控制面高可用与业务幂等同时要。

【评分标准】

档位答案特征
60 分知道要加分布式锁
80 分能讲 XXL-JOB 分片广播
95 分强调业务幂等;分片倾斜;调度中心高可用与监控

【关联题】

  • 同簇: 第 104 题(分布式锁)、第 118 题(多实例并发)、第 108 题(分片思想)
  • 幂等: 第 110 题、第 117 题
  • 订单场景: 超时关单与库存回滚

【自测】

  1. 为什么本地 @Scheduled 多实例会重复? 参考答案:每个进程独立触发,无跨实例互斥。
  2. XXL-JOB 分片广播解决什么问题? 参考答案:把任务拆成多片并行执行,提高吞吐并避免单实例扫描瓶颈。
  3. 调度框架上线后还需要幂等吗? 参考答案:需要。调度层无法消除重试与业务并发,处理逻辑仍要可重入。

115. 一个请求跨 5 个服务变慢,怎么定位是哪一环(链路追踪) ​

【考察内容】可观测性设计

【题目】一次请求要串行经过网关、订单、库存、支付、短信 5 个服务,线上变慢了,但每个服务的监控都“看着正常”。怎么定位到底慢在哪个服务哪一环?链路追踪的核心原理是什么(traceId/span、透传、采样)?

【参考答案】

  1. 核心机制:TraceID + SpanID:
    • 请求入口生成全局唯一 TraceID,通过 HTTP 头/消息头透传到所有下游服务;
    • 每个服务内每个调用(DB、RPC、MQ)生成 Span(父子关系),记录耗时、状态、参数;
    • 所有 Span 汇总到追踪系统(按 TraceID 聚合),还原完整调用链;
  2. 组件:SkyWalking(无侵入,字节码增强)、Zipkin、Jaeger(OpenTelemetry 标准);
  3. 使用场景:
    • 定位慢请求:看各 Span 耗时,找出最慢环节;
    • 定位报错:按 TraceID 查全链路日志;
    • 依赖分析:服务间调用关系图、依赖瓶颈;
  4. 配套:日志中打印 TraceID(logback MDC),日志检索与链路关联;
  5. 采样策略:全量采样压力大,生产按需采样(如 10% 采样+错误全采+超阈值慢请求 100% 采样)。题干要「定位慢在哪一环」,而慢请求通常不是错误,按 10% 抽样会漏掉九成现场,等于把要查的那批 trace 扔了;本条判断树与【关键数字】也正是按「错误/慢请求常 100% 采样」口径写的。
  6. 容量估算:日请求 1 亿、采样率 1% → 被采请求 100 万条;一条链路按本题【关键数字】的 5~10 个服务展开,日 span 样本约 500 万~1000 万条(别把被采请求数当 span 数);若全采,存储与索引成本高,通常头部全采或重要链路提高采样。单服务 QPS 1 万时,trace 上报不能阻塞业务:本地异步队列+批量上报。存储:Jaeger/SkyWalking 等按保留天数估磁盘。查询场景是排障,不是在线业务路径——追踪系统挂了不影响交易,但要能告警。
  7. 失败与降级:追踪 agent 故障→业务不受影响(旁路);采样导致没抓到现场→关键路径提高采样/错误全采;时钟不一致导致 span 乱序→统一 NTP(见 116);与日志/指标关联(traceId 打日志)才能闭环。监控:上报丢失率、存储水位、查询延迟。

【原理溯源】

  • 为什么单服务监控“看起来正常”? 每个服务 RT 可能都 OK,但串行相加、某次下游抖动、线程池排队只在“全链路视角”暴露。缺的是跨服务关联。
  • TraceID/Span 如何还原链路? TraceID 标识一次端到端请求;Span 标识一次工作单元及其父子关系。按 TraceID 聚合即可重建调用树与耗时分解。
  • 为什么必须透传? 若下游拿不到 TraceID,链路会断成孤岛。通过 HTTP Header / gRPC metadata / MQ 消息头传递,是接入追踪的第一要求。
  • 为什么要采样? 高 QPS 全量上报会打爆存储与带宽。常用概率采样 + 错误/慢请求全采,兼顾成本与排障。
  • 为什么日志要带 TraceID? 链路告诉你“哪一环慢/错”,日志告诉你“为什么”。两者用同一 ID 关联,排障才闭环。

【选型判断树】

排障需求?
├─ 只看单服务指标 → 现有 metrics 足够
└─ 跨服务慢/错 → 必须链路追踪
    ├─ 要无侵入 Java → SkyWalking
    ├─ 标准化/多语言 → OpenTelemetry + Jaeger/Zipkin
    └─ 高 QPS → 概率采样 + 错误全采 + 慢请求全采

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“单服务指标正常不代表全链路正常”
0:30–1:30Trace/Span全局 ID 与父子 Span
1:30–2:30透传Header/MQ 传递
2:30–3:30定位用法按 Trace 看耗时与错误
3:30–4:30采样与日志成本与 MDC 关联
4:30–5:00收尾“追踪定位环节,日志解释原因”

【关键数字】

参数经验值说明
采样率1%~10% 常见视 QPS 与成本
错误/慢请求常 100% 采样保排障
Span 数量控制在必要调用过多成本高
日志关联必须打 TraceIDMDC
典型链路5~10 服务微服务常态
上报异步批量,不阻塞业务旁路系统

【追问链】(三层)

L1|“TraceID 怎么传到 MQ 消费者?” → 生产端写入消息 Header/属性,消费端取出放入 MDC 与新 Span,保持与上游同一个 Trace。

L2|“采样后会不会丢掉关键故障?” → 会,所以采样不能只用一种规则。第 5 条的策略是概率采样叠加错误全采:概率采样按 1%~10% 控成本,第 6 条算过日请求 1 亿、1% 采样是被采请求百万级,按一条链路 5~10 个服务展开就是 500 万~1000 万 span,全采则存储与索引扛不住;但错误和超阈值的慢请求要 100% 采样(关键数字给的就是这个口径),这是关键故障的主要入口。剩下两类漏采要单独防:① 下游把异常吞成 200、或业务错误不体现在错误码上,概率采样就会漏,需要错误码与慢阈值双重触发;② 支付、下单这类核心链路单独提高采样率甚至全采。最后的兜底是第 4 条:日志必须带 TraceID(MDC),采样没命中时还能按 ID 把各服务日志串起来看现场。

L3|“追踪系统本身挂了影响业务吗?” → 上报应异步、有界队列、失败丢弃,不阻塞业务。追踪是旁路,不能反向拖垮主链路。

【评分标准】

档位答案特征
60 分听说过 SkyWalking/Zipkin
80 分能讲 TraceID/Span 与透传
95 分讲采样策略、日志关联、旁路上报不阻塞;能说如何用 Trace 定位慢节点

【关联题】

  • 相关: 第 122 题(超时与慢调用)、第 116 题(时间戳)、日志与监控体系题
  • 架构: 第 112 题(服务多了才需要)

【自测】

  1. 链路追踪的最小必要数据是什么? 参考答案:全局 TraceID + 各服务 Span(含耗时/状态/父子关系)。
  2. 为什么高 QPS 要采样? 参考答案:全量上报成本过高;用概率采样并对错误/慢请求全采。
  3. 日志如何与链路关联? 参考答案:日志中输出同一个 TraceID(MDC),可按 ID 检索全链路日志。

116. 两台服务器时间差了 5 分钟,日志排序和限流全乱了(时钟不一致) ​

【考察内容】分布式时间一致性

【题目】排查问题时发现两台服务器系统时间差了 5 分钟:日志时间线对不上、限流窗口错乱、订单超时判断不准。分布式系统时钟不一致会产生哪些问题?如何解决(NTP 同步、逻辑时钟、不依赖物理时钟的设计)?

【参考答案】

  1. 问题:
    • 日志/链路追踪乱序:多机日志时间戳不一致,排查困难;
    • 业务时间错误:订单时间、定时任务触发、过期判断出错;
    • 分布式锁/雪花 ID:时钟回拨产生重复 ID;锁过期判断依赖时钟;
    • 跨时区/容器时钟漂移;
  2. 解决:
    • NTP 时钟同步:统一 NTP 服务器,chrony/ntpd 定期校准,监控偏差;
    • 不用本地时钟做关键决策:业务时间以数据库/服务端统一时间为准(NOW()),时间戳字段由 DB 生成;
    • 单调时钟 vs 墙上时钟:性能统计、超时计时用单调时钟;业务时间用墙上时钟并同步 NTP;
    • 雪花算法:监控偏差+回拨处理;
  3. 设计原则:分布式系统里“时间”要有唯一权威来源,本地时钟只用于辅助。
  4. 容量/偏差数字:机器间时钟偏差应控制在 毫秒级(NTP 通常 <50ms,好的环境 <10ms);超过秒级会影响:分布式锁 TTL、限流窗口、日志排序、雪花 ID、会话过期、安全审计。5 分钟偏差对限流窗口(1s/1min)是毁灭性的。审计与金融日志要求更高,常要 NTP 监控+偏差告警(如 >100ms 预警、>1s 严重)。跨机房要评估时间源(GPS/北斗/NTP 层级)。
  5. 失败与降级:NTP 漂移→自动校时+告警;容器环境用宿主机时钟与 chrony;生成 ID 回拨策略(107);日志排序以服务端接收时间+逻辑时钟辅助;限流可用 Redis 中心化计数减少对本机时钟依赖——但要认清它只合并「同一个窗口桶」里的多实例计数:分钟桶的 key 仍由本机时钟生成,两台实例差 5 分钟就会写进不同桶,中心化计数并不纠正桶本身的错位,题干「限流抖动」这个症状没被解决。要真消除时钟依赖,窗口标识必须由单一权威时钟产生:网关单点计数、Redis Lua 内用 TIME(服务端时间)取桶,或直接读 DB 时间。禁止人工改生产时间。

【原理溯源】

  • 为什么分布式没有“真·同时”? 物理时钟总有偏差,网络传输也有延迟。系统设计要么接受近似并同步,要么改用逻辑序(版本号、序列)。
  • 为什么关键业务时间要 DB 生成? 多实例本地时钟不一致时,各写各的时间会乱序。DB NOW() 提供单一权威时间,避免应用机器漂移污染数据。
  • 单调时钟和墙上时钟差在哪? 墙上时钟(wall clock)可被 NTP 回拨,适合展示与业务日期;单调时钟只保证本进程内递增,适合测量耗时与超时,不能用于跨机器排序。
  • 限流窗口为什么会乱? 基于本地时间的窗口在多实例不齐,导致限流计算不一致。应把窗口计算放到共享存储/统一时钟,或用逻辑计数——但「共享存储」本身不够:若桶 key(如分钟号)仍由各实例本机时钟生成,中心化计数只是把「同一个桶」的多实例计数合并,并不纠正桶本身的错位;窗口标识必须由单一权威时钟产生(网关单点计数、Redis Lua 内 TIME、或读 DB 时间)。
  • 为什么 NTP 也不是万能? 校准有延迟与幅度,仍可能回拨;虚拟机迁移更严重。所以应用层还要“不依赖可疑时钟做关键决策”。

【选型判断树】

这个时间用来做什么?
├─ 展示/业务日期 → DB/统一时间服务生成
├─ 耗时/超时 → 本进程单调时钟
├─ 跨机器排序/追踪 → 统一时间源 + 逻辑序辅助
├─ 发号/锁过期 → 特殊算法与兜底(见 107/105)
└─ 全集群基础 → NTP/chrony + 偏差监控

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“分布式时间必须有权威来源,不能信各自本地时钟”
0:30–1:30问题面日志、过期、锁、雪花
1:30–2:30NTP同步与监控
2:30–3:30设计规避DB 时间、单调时钟
3:30–4:30算法敏感点雪花回拨、限流窗口
4:30–5:00收尾“时间也是一种分布式共识问题”

【关键数字】

参数经验值说明
机房内 NTP 偏差目标毫秒级超标告警
公网 NTP可能几十 ms~更大生产用内网 NTP
日志时间乱序分钟级偏差就明显5 分钟必炸
墙上 vs 单调不可混用排序currentTimeMillis vs nanoTime
容器时钟易漂移宿主机与 guest 配置
偏差目标毫秒级(<50–100ms)NTP 监控
严重阈值>1s 告警处理影响锁/限流/ID
限流中心化计数只合并「同一个窗口桶」里多实例的计数桶 key 若仍由本机时钟生成,实例间差 5 分钟就写进不同桶——窗口标识须由权威时钟产生(网关单点/Lua TIME/DB 时间)

【追问链】(三层)

L1|“同步 NTP 就万事大吉?” → 不是。NTP 仍有偏差与回拨。关键决策要避免依赖可疑本地时钟,用 DB 时间或逻辑版本。

L2|“订单过期时间谁生成?” → 由统一时间源生成,实践中就是数据库:下单那条本地事务里用 DB NOW() 写下 expire_time(或由中心时间服务给一个绝对时刻),应用实例不拿本机时钟写业务时间。原因是第 1 条把它列为「业务时间错误」的重灾区:订单时间、定时任务触发、过期判断都依赖同一个时刻;两台机器差 5 分钟时各实例自己算,就会一部分订单提前被关、另一部分延后被关,用户看到的就是「刚下单就失效」。关单判断也写成 WHERE expire_time <= NOW(),让比较发生在同一个权威时钟上。第 4 条的运维口径同时成立:偏差目标毫秒级、>1s 必须处理——NTP 是地基,权威时间源是兜底。

L3|“能不能完全不用物理时间?” → 可用逻辑时钟/版本号表达顺序,但很多业务仍要人类可读时间。工程上是“物理时间近似 + 逻辑序保正确”。

【评分标准】

档位答案特征
60 分知道要 NTP 对时
80 分能列举时钟不一致的业务影响
95 分讲权威时间源、单调 vs 墙上、雪花/锁风险;不过度神话 NTP

【关联题】

  • 同簇: 第 107 题(雪花回拨)、第 105 题(RedLock 时钟)、第 115 题(追踪时间戳)

【自测】

  1. 为什么业务时间戳建议由 DB 生成? 参考答案:提供唯一权威时间,避免多实例本地时钟偏差污染数据。
  2. 耗时统计应使用什么时钟? 参考答案:单调时钟(如 nanoTime),不受墙上时钟回拨影响。
  3. 时钟不一致还会破坏哪些组件? 参考答案:分布式锁过期、雪花 ID、限流窗口、日志与追踪排序。

117. 用户手抖连点两次“提交订单”,生成了两单(重复下单) ​

【考察内容】防重复下单是电商最高频实战题

【题目】用户手抖或网络重试导致“提交订单”被调用了两次,生成了两个一模一样的订单,还被扣了两份钱。前端只做按钮置灰不够,后端怎么防?幂等方案(幂等键、唯一约束、状态校验)怎么设计?

【参考答案】

  1. 前端:按钮提交后置灰/loading(防手抖,防不了脚本);
  2. 服务端 Token 机制:下单前获取唯一 token(绑定用户),下单请求携带 token,服务端原子消费 token:Lua 里判断存在并删除(或 6.2+ 的 GETDEL)——取不到即视为已使用,拒绝;
  3. 业务幂等键(前提要写出来,否则第 4 条防线形同虚设:一次提交流程只生成一次、失败重试沿用同一个键,若手抖两次各自新生成 UUID,两条请求都「合法」,唯一键根本不冲突):前端生成/服务端下发幂等键,服务端以“用户+幂等键”为唯一键处理,重复请求返回首个结果;
  4. DB 最终防线:唯一键按业务语义选,三种情况别混——① 普通下单:用 (user_id, 幂等键/请求号)(或订单号本身唯一),重复请求返回首单;不能按「用户+商品」建唯一索引,那会把正常复购直接判重报错;② 限购/一人一单活动:(user_id, sku_id, 活动期) 或 (user_id, activity_id),此时“同商品不允许第二次下单”正是业务要求,与第 131 题同口径;③ 支付回调:渠道流水号唯一,重复回调只入账一次。分布式锁/SETNX 只降低冲突概率,正确性始终以 DB 唯一索引为准(锁丢了索引还在;索引没有时,锁挡不住多实例竞态);
  5. 顺序(题干第三问的「状态校验」在这一环,不能只答幂等键+唯一约束):校验(Token)→ 状态校验(按幂等键查该请求是否已成单:SELECT order_id,status FROM ... WHERE idempotent_key=?,命中即直接返回首单结果,不再往下走)→ 防重(唯一索引)→ 生成订单(事务;后续状态流转用 WHERE status=待支付 这类条件更新守住状态机)→ 返回。要点:「查—判—插」必须原子(同一事务内靠唯一索引兜竞态,或先用 Redis SETNX 占位),只查不判会双写,只插不查则重复请求拿到的是报错而不是首单;
  6. 注意:幂等键需用户维度(同一用户同一操作同一结果),不能全局一个。
  7. 容量估算:防重复下单的幂等层按提交峰值设计,秒杀场景提交峰值可达 数千~数万 QPS。token/唯一键校验若走 Redis,需同量级;DB 唯一索引插入热点要分片时,分片键必须含(或等于)幂等唯一键的判定维度,即按 user_id/幂等键分片;不能按「订单号」分片:每次重试都带一个新订单号,同一幂等键会落到不同分片,跨分片唯一约束不成立,第 4 条的「最终防线」就被这条自己拆掉(订单号只作唯一列,不作分片键)。按钮防抖只能挡前端,挡不住脚本——服务端幂等才是关键。库存扣减与创建订单要原子或条件更新,重复提交不应导致超卖。
  8. 失败与降级:Redis token 层挂→DB 唯一约束+条件更新兜底;重复提交返回已有订单(查询态)而不是报错;脚本刷单→限流+风控+验证码。监控:重复提交拦截次数、创建订单成功率、唯一键冲突率。

【原理溯源】

  • 为什么前端置灰不够? 双开标签、脚本、弱网重试、网关重发都会造成重复。客户端提示是体验优化,不是正确性机制。
  • Token 机制防的是什么? 防“重复提交进入业务”。一次一 token,用后作废,相当于门票。适合“提交前先领票”的流程。
  • 幂等键与 Token 差别? Token 强调一次性准入;幂等键强调“同一业务操作多次请求返回同一结果”。下单两者可叠加。
  • 为什么必须 DB 唯一索引? 应用层锁/缓存都可能失效或竞态。唯一索引在存储引擎保证同一业务组合只成功一次,是不可绕过的最终防线。
  • 为什么重复时应返回已下单而不是报错? 用户/调用方往往不知道首次是否成功,返回已有订单符合幂等语义,也避免误报失败导致再次下单。

【选型判断树】

防重复下单分层:
1. 前端置灰(体验)
2. Token 准入(防进入)
3. 幂等键/唯一索引(防重复业务效果;**前提:一次提交流程只生成一个键、重试沿用同键**,否则两条请求各自唯一、索引根本不冲突)+**状态校验**:按幂等键查是否已成单,命中即直接返回首单(题干点名的第三问)
4. 库存与支付也幂等(链路下游)

秒杀一人一单:
└─ 唯一索引 (user_id, activity_id) 或 用户维度锁 + 幂等

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“防重复下单是多层防御,不是单点按钮”
0:30–1:30重复来源手抖、重试、多端
1:30–3:00四层前端置灰 / Token 准入 / 状态校验(按幂等键查已成单即返首单) / 唯一索引兜底
3:00–4:00返回语义返回首次订单
4:00–4:30秒杀场景一人一单唯一键
4:30–5:00收尾“正确性在 DB 约束,体验在前端”

【关键数字】

参数经验值说明
Token 有效期分钟级领取后尽快使用
唯一索引普通下单 (user_id, 幂等键/请求号);限购活动 (user_id, sku_id, 活动期);支付回调 (渠道流水号)切勿对订单表建 (user_id,item_id)——会挡正常复购
重复响应200 + 首次订单信息非 500
Redis 锁仅辅助不能替代唯一索引
超时重试必须可安全重试幂等前提
提交峰值秒杀可达数千–数万 QPS幂等层按峰值设计
前端防抖按钮置灰/token 一次有效挡不住脚本
服务端唯一键+状态条件更新最后防线

【追问链】(三层)

L1|“只用 Redis 锁行不行?” → 不行作最终手段。锁过期/丢锁仍可能双单。锁可降并发,正确性靠唯一索引/幂等。

L2|“Token 存 Redis,Redis 挂了?” → 票发不出来,但不能把业务一起挂掉。按第 8 条:Redis token 层挂→直接由 DB 唯一约束加条件更新兜底,第 4 条那条「用户+商品+活动」唯一索引还在,重复提交仍然只成功一次——Redis 在这里只降低并发冲突,正确性不依赖它,关键数字也写明 Redis 锁「仅辅助,不能替代唯一索引」。副作用要提前算:token 这道前置闸门失效后,第 7 条说的数千~数万 QPS 提交峰值会直接打在唯一索引冲突上,所以 Redis 不可用期间要收紧该接口限流,必要时上验证码与风控。对用户不能返回 500:重复请求要返回已创建的订单(查询态),否则用户以为失败再下一单,反而制造更多冲突。

L3|“同一用户不同商品能不能各下一单?” → 可以。幂等键维度要与业务规则对齐:一人一单活动用活动维度;普通下单用订单幂等键维度,不能错误全局互斥。

【评分标准】

档位答案特征
60 分提到前端防抖
80 分会讲 Token/幂等键
95 分唯一索引最终防线(含分片键必须含幂等维度)、状态校验+返回首次结果、秒杀一人一单设计、幂等键「一次流程一个键」前提

【关联题】

  • 同簇: 第 110 题(接口幂等)、第 118 题(并发控制)、第 104 题(锁)
  • 支付: 重复扣款与幂等

【自测】

  1. 防重复下单为什么不能只靠前端? 参考答案:脚本、多标签、重试仍会重复请求,服务端必须有幂等与约束。
  2. 一人一单秒杀最可靠的手段是什么? 参考答案:数据库唯一索引(user_id+activity_id),冲突则返回已下单。
  3. 重复提交应如何响应? 参考答案:返回首次成功结果(已有订单),而不是报错。

118. 多实例部署后 synchronized 全失效了(多实例并发控制) ​

【考察内容】分布式并发安全是综合必考题

【题目】服务从单机改成多实例后,原来靠 synchronized 保护的并发逻辑全部失效(每个实例各锁各的)。并发安全怎么办?从数据库乐观锁、Redis 分布式锁、消息队列串行化三个层面,给出完整方案与适用场景。

【参考答案】

  1. 认知:多实例=多个 JVM 进程,本地锁只能锁住本进程,跨进程必须用分布式互斥机制;
  2. 方案矩阵:
    • 数据库:悲观锁(SELECT FOR UPDATE)、乐观锁(version/CAS 条件更新)——简单可靠,适合低频写;
    • Redis 分布式锁:SET NX EX + Lua 解锁 + 看门狗(Redisson)——性能好,适合高并发临界区;
    • ZooKeeper/etcd 锁:临时顺序节点/租约,强一致,适合严格互斥;
    • 消息队列串行化:并发写投递 MQ,单消费者串行处理——用排队换无锁;
  3. 配套原则:
    • 能避免锁就避免:幂等、唯一索引、状态机;
    • 锁粒度最小化:按用户/订单维度,避免全局锁;
    • 锁+幂等双保险:锁是事前互斥,幂等是事后兜底;
  4. 验证:多实例并发压测。
  5. 容量估算:多实例下本地 synchronized 只能保证单机互斥。若全局临界区操作峰值 1000 次/s,Redis 分布式锁要能扛同量级且持锁时间短;若临界区耗时 50ms,则全局串行上限约 20/s——说明这类热点应改设计(分段锁/队列/合并),而不是硬加锁。本地锁适合单实例内存结构;跨实例一致性要靠分布式协调或无共享分片。
  6. 失败与降级:Redis 锁不可用→可降级为“允许短暂重复+业务幂等”或 DB 行锁;锁性能不够→按 key 分段,把全局锁拆成 64/256 个分片锁,提高并行。监控:锁等待 RT、冲突率、重复执行次数。

【原理溯源】

  • 为什么 synchronized 失效? JVM 锁对象在进程内存,进程间不可见。水平扩展后临界区从“一个 JVM”变成“多个 JVM + 共享存储”,本地锁鞭长莫及。
  • 为什么数据库方案稳? 行锁/条件更新发生在共享的 DB,天然跨实例。代价是热点行锁竞争与 DB 吞吐上限。
  • 为什么 Redis 锁性能好? 内存操作+异步复制,延迟低。正确性弱于 ZK,需要幂等兜底。
  • 为什么 MQ 串行化可行? 把并发改成排队,消费者单线程处理同一队列/分区,用顺序性换简单正确。适合可异步、吞吐可削峰的写。
  • 为什么“能不锁就不锁”? 锁引入阻塞、死锁、性能瓶颈。唯一索引、状态机、CAS 用数据约束表达互斥,往往更稳。

【选型判断树】

冲突频率与正确性要求?
├─ 冲突少、低频 → 乐观锁 version/CAS
├─ 冲突多、要强一致行级 → 悲观锁 SELECT FOR UPDATE
├─ 高并发临界区、可接受小概率丢锁 → Redis 锁 + 幂等
├─ 严格互斥、不能丢锁 → ZK/etcd
└─ 可异步、要削峰 → MQ 串行化
通用:锁粒度最小 + 幂等/唯一索引兜底

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“本地锁跨不了进程,必须分布式互斥”
0:30–1:30数据库乐观/悲观
1:30–2:30Redis/ZK性能 vs 强一致
2:30–3:30MQ 串行排队换无锁
3:30–4:30原则最小粒度、锁+幂等
4:30–5:00收尾“先数据约束,再分布式锁”

【关键数字】

参数经验值说明
乐观锁冲突重试有限次,否则降级防活锁
Redis 锁 TTL略大于临界区耗时 + 看门狗防死锁与提前释放
行锁等待超时秒级可配避免拖垮连接
MQ 分区/队列同一业务键同一分区保证串行
压测必须多实例验证真实并发
本地锁范围仅单实例多实例失效
全局串行上限1/临界区RT50ms→约20/s,需改设计
优化分段锁/消息合并避免全局大锁

【追问链】(三层)

L1|“乐观锁和悲观锁怎么选?” → 读多写少/冲突少用乐观锁(失败重试);写冲突多、必须强制排队用悲观锁。热点极高时考虑拆分或异步化。

L2|“Redis 锁与 DB 谁是最终正确性?” → DB 侧的数据约束才是最终正确性,Redis 锁是并发优化。三条依据:① 第 6 条明写「Redis 锁不可用→降级为允许短暂重复 + 业务幂等,或 DB 行锁」——一个可以被降级的组件不承担正确性;② 机制上 Redis 是内存操作 + 异步复制,主从切换会丢锁、TTL 抖动会提前释放,而行锁/条件更新/唯一索引发生在共享的 DB 上,天然跨实例且不可绕过;③ 第 3 条把「幂等、唯一索引、状态机」排在锁前面——锁是事前互斥,幂等是事后兜底。也别用锁硬扛热点:第 5 条算过临界区 50ms 时全局串行上限约 20 次/秒,这种量级必须改设计(分段锁、队列串行化、合并写)。

L3|“会不会死锁?” → 分布式锁要统一加锁顺序、设置超时与续期;DB 锁要缩短事务、按固定顺序访问行。死锁预防靠协议与超时,不靠运气。

【评分标准】

档位答案特征
60 分知道 synchronized 多实例无效
80 分能列 DB/Redis/ZK/MQ 方案
95 分讲选型条件、锁+幂等、最小粒度;知道数据约束优于盲目加锁

【关联题】

  • 同簇: 第 104/105 题(锁)、第 110/117 题(幂等)、第 114 题(任务)
  • 相关: 第 102 题(全局锁瓶颈)

【自测】

  1. 为什么单机锁在多实例下失效? 参考答案:锁状态在进程内存,进程间互不可见。
  2. 高并发扣库存你会怎么组合? 参考答案:Redis/本地预扣 + DB 条件更新或唯一约束 + 幂等,极端热点再拆分异步。
  3. “锁+幂等”分别防什么? 参考答案:锁防并发进入;幂等防重复执行/锁失效后的错误结果。

119. 本地缓存+Redis+DB 三级,更新一处到处不一致(多级缓存一致性) ​

【考察内容】多级缓存一致性治理

【题目】系统做了“本地缓存 → Redis → DB”三级缓存,更新数据时只更新了 DB,用户读到的还是本地缓存/Redis 里的旧值,三层互相不一致。怎么设计更新策略?本地缓存怎么失效(广播/短TTL)?怎么取舍一致性 vs 性能?

【参考答案】

  1. 更新策略(写路径):
    • 先更新 DB(事务提交);
    • 删除 Redis 缓存(Cache Aside),删除失败用重试/MQ/Canal binlog 兜底;
    • 本地缓存失效:通知所有实例——Redis Pub/Sub 广播失效消息、版本号比对、短 TTL 兜底;
  2. 读路径:本地缓存 → Redis → DB → 逐级回填;
  3. 一致性等级:
    • 强一致(很难):读写串行化/绕过缓存,一般不追求;
    • 最终一致(主流):删缓存+异步补偿+短 TTL,秒级收敛;
  4. 特殊场景:
    • 更新频繁的数据不进本地缓存;
    • 广播失效实例多时压力大——用版本号替代广播;
    • 对账任务兜底修复;
  5. 原则:能容忍的短暂不一致用最终一致;不能容忍的直读 DB。
  6. 容量估算:本地缓存抗热点,进程内访问是微秒级(纳秒–微秒,无网络往返)、容量 MB~GB 级;0.1–1ms 是 Redis 那一层;Redis 扛主要读,单实例 10 万+ QPS;DB 扛残余与写。命中率目标:本地 30%+、Redis 90%+ 组合下落到 DB 的比例很低。例:读峰值 10 万 QPS,本地命中 30%→7 万打 Redis,Redis 命中 95%→约 3500 QPS 落 DB,DB 可接受。更新风暴时失效比例上升,要限流回源。缓存内存:热点 key 体积×数量×副本。
  7. 失败与降级:本地缓存不一致→短 TTL+版本号/失效广播;Redis 挂→本地缓存+限流打 DB——先把降级后的量写出来再谈限流:按本条第 6 项口径,本地只挡 30%,Redis 一挂就有 ≈7 万 QPS 直接落 DB,而同一条里 DB 的「可接受」是 3500 QPS,差 20 倍;只说「限流打 DB」等于当场拒掉 2/3 以上读请求,且本地未命中仍要回源 DB,回源风暴更糟。降级要分级写:① 本地 TTL 临时放大+全量预热(把命中率从 30% 顶到 70%+);② 切 Redis 多副本/哨兵或只读降级;③ 只保核心接口,非核心直接返回兜底;④ 限流阈值按 DB 实测能力设,并明确「超出即拒绝」而非默默排队;DB 挂→只读缓存降级(容忍短暂旧数据);更新失败→延迟双删/binlog 订阅重试。监控:三级命中率、回源 QPS、不一致投诉、缓存内存与逐出。

【原理溯源】

  • 为什么“只更新 DB”会不一致? 缓存是副本。不主动失效/更新,TTL 到期前一直读旧值。多级副本让不一致窗口更多、更难清。
  • 为什么写路径是“先更 DB 再删缓存”? 先删缓存可能在回填旧值后 DB 更新失败,脏数据驻留更久。业界常用 Cache Aside:更新 DB 后删缓存,读时回填;仍有小概率不一致,靠重试/TTL/对账收敛。
  • 本地缓存为何更难失效? 每实例一份,无中心入口。必须广播失效、版本号轮询或短 TTL,否则实例间长期各读各的旧值。
  • 为什么更新频繁的数据不进本地缓存? 失效广播频率高、命中率低、不一致窗口难压。这类数据只放 Redis 或直读 DB。
  • 强一致缓存为何难? 多副本+异步失效无法保证“任何读都看到最新写”。要强一致只能串行化或不缓存,性能与一致性必须取舍。

【选型判断树】

数据一致性要求?
├─ 不能读到旧值(资金、权限)→ 不进多级缓存 / 直读 DB / 短 TTL 强一致方案
└─ 可秒级旧值 → 多级缓存最终一致
    └─ 本地缓存失效方式:
        ├─ 实例少、变更少 → Pub/Sub 广播
        ├─ 实例多、怕广播风暴 → 版本号/短 TTL
        └─ 更新频繁 → 根本不进本地缓存

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“多级缓存本质是多副本,一致性靠失效协议”
0:30–1:30写路径DB → 删 Redis → 本地失效
1:30–2:30本地失效广播/版本号/短 TTL
2:30–3:30读路径逐级回填
3:30–4:30取舍不能容忍就绕过缓存
4:30–5:00收尾“先定一致性等级,再定缓存层级”

【关键数字】

参数经验值说明
Redis TTL秒~分钟按业务兜底
本地 TTL通常更短(1s~30s)限制脏读窗口
失效广播秒级到达丢消息靠 TTL
Cache Aside主流更新 DB 后删缓存
热点更新不进本地缓存保一致性
读分流示例10万QPS:本地30%→Redis95%→DB约3.5k组合命中率决定 DB 压力
更新策略延迟双删/binlog/版本号防脏读

【追问链】(三层)

L1|“先删缓存再更 DB 行不行?” → 可能回填旧值后 DB 失败,或并发下脏数据驻留。更常用先更 DB 再删缓存,并接受小概率不一致+重试/TTL。

L2|“广播消息丢了怎么办?” → 不能把广播当可靠通道——Pub/Sub 掉线的订阅者就是永久漏收,所以整套设计要按「一定会丢一条」来做。三层兜底:① 本地缓存必须有短 TTL(关键数字给的 1s~30s 量级),丢一次推送的最坏后果被压成一个 TTL 的脏读窗口;② 用版本号比对替代纯广播:实例周期性拉一个轻量版本号,发现自己持有的版本落后就主动失效,漏掉的推送下一轮自动纠正;③ DB→Redis 那层删除同样不能只发一次,第 1 条给的是重试/MQ/Canal 订阅 binlog 兜底。观测按第 7 条看三级命中率与不一致投诉:某实例版本长期落后、或回源 QPS 从第 6 条算的约 3500 抬起,就是失效链路断了。

L3|“什么时候该放弃本地缓存?” → 强一致要求、更新极频繁、广播成本高于收益时。不是所有读都值得多级缓存。

【评分标准】

档位答案特征
60 分知道更新 DB 后要删 Redis
80 分能讲本地缓存失效与最终一致
95 分写读路径完整、更新频繁不进本地、强一致绕过缓存的取舍

【关联题】

  • 相关: 第 98 题(BASE)、第 109 题(数据一致性)、缓存击穿/雪崩/穿透题
  • 锁: 第 118 题(并发更新)

【自测】

  1. Cache Aside 典型写路径是什么? 参考答案:先更新 DB,再删除缓存;读时未命中则读 DB 并回填。
  2. 本地缓存多实例如何失效? 参考答案:Pub/Sub 广播、版本号比对、短 TTL 兜底;更新频繁的数据不进本地缓存。
  3. 资金账户余额能放本地缓存吗? 参考答案:一般不能。需要强一致,应直读 DB/强一致缓存或极短 TTL 并严格串行化。

120. 改个限流阈值,改完整个集群立刻生效把线上搞挂了(配置灰度) ​

【考察内容】配置治理与发布工程

【题目】有人在配置中心把限流阈值改小,配置实时推送到全量实例,线上接口立刻大面积被限。配置变更怎么灰度发布(按实例/按比例/按环境)?改坏了怎么快速回滚?配置中心和发布系统怎么配合?

【参考答案】

  1. 需求:配置错误/新配置需要小范围验证,不能一键全量生效;
  2. 方案:
    • 按实例灰度:指定部分实例(按 IP/分组)应用新配置,验证后再全量;
    • 按用户/流量灰度:配置值带生效规则(user_id 百分比/白名单);
    • 按环境/集群:预发 → 生产小集群 → 全量;
  3. 流程:灰度发布 → 监控(错误率/RT/业务指标)→ 通过全量 / 失败回滚(一键回滚旧值);
  4. 工程要点:配置变更审计、变更后校验、回滚预案;Apollo/Nacos 支持命名空间/灰度;与发布系统配合:配置项随代码版本一起声明(同一条流水线里带默认值与取值范围),发版前做配置校验与灰度门禁,新代码依赖的新配置必须先于流量放量生效,回滚时代码与配置一并回退(旧代码读新配置是二次事故)。
  5. 业务代码注意:配置读取不能启动时一次性加载(动态刷新,@RefreshScope),否则灰度不生效。
  6. 容量/生效时间数字:全量配置推送,数千实例在秒级同时生效——若新阈值错误,故障也是秒级全集群。灰度目标:先 1%→10%→50%→100%,每档观察 5–15 分钟。配置变更 QPS 本身很低,关键是爆炸半径而不是吞吐。限流阈值这类配置,错误值的影响是线上的直接成功率,必须有:默认值、范围校验、一键回滚、变更审计。
  7. 失败与降级:配置中心故障→应用用本地快照;推送失败→客户端定时拉取;错误配置→秒级回滚到上一版本;高危配置需二次确认/审批流。监控:配置变更事件、关键阈值生效状态、变更后错误率/RT 关联看板。

【原理溯源】

  • 为什么配置全量即时推送是事故源? 配置是“没有编译期校验的代码”。错误阈值/开关一旦全量生效,爆炸半径=整个集群,且推送比发版更快,来不及人工反应。
  • 灰度的本质是什么? 把变更爆炸半径从 100% 降到可控子集,用监控验证后再放量。与代码发布灰度同理,只是变更载体是配置。
  • 为什么要分实例灰度和用户灰度? 实例灰度验证“运行时行为/性能”;用户/流量灰度验证“业务效果”。限流阈值更适合实例/集群灰度;功能开关更适合用户灰度。
  • 为什么必须能一键回滚? 配置变更快,出事也要同样快地恢复。没有版本与回滚的配置中心等于裸奔。
  • 为什么动态刷新是前提? 若代码启动时读一次配置,灰度改配置不生效,灰度机制形同虚设。可变更性是配置治理的地基。

【选型判断树】

配置变更类型?
├─ 限流/超时/池大小(性能敏感)→ 实例/集群灰度 + 强监控
├─ 功能开关 → 用户/流量灰度
├─ 中间件地址/密钥 → 通常不灰度,走双写/切换预案
└─ 高风险全局项 → 分环境:预发 → 小流量 → 全量
任何变更:审计 + 可回滚 + 监控

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“配置变更风险不亚于发版,必须灰度”
0:30–1:30事故机理全量推送爆炸半径过大
1:30–2:30灰度维度实例/用户/环境
2:30–3:30流程发布-监控-回滚
3:30–4:30工程要求审计、校验、动态刷新
4:30–5:00收尾“配置即变更,变更即风险”

【关键数字】

参数经验值说明
灰度比例1 台 → 1% → 10% → 50% → 100%每档观察 5–15 分钟,按风险可加档
观察时间分钟级到小时级看指标周期
回滚目标秒级到分钟级越快越好
配置中心Apollo/Nacos支持灰度版本
刷新机制@RefreshScope / 长轮询必须真热更新
生效延迟秒级(长轮询)错误也秒级爆炸

【追问链】(三层)

L1|“配置中心挂了怎么办?” → 客户端本地缓存配置 + 启动全量拉取;推送失败用长轮询/定时拉取兜底。业务应能在旧配置下运行。

L2|“限流阈值灰度时,负载均衡会不会导致灰度实例被打爆?” → 会,这是按实例灰度的固有副作用:只有 1% 实例换了新阈值,负载均衡仍是均匀分发,这几台就得按新阈值扛下自己那一份流量。阈值往高调,它们先把本该被挡住的请求放行,保护没生效、自己先过载;往低调,同比例的请求在它们身上被误拒,观察窗口里的错误率失真,还容易被误读成「新配置没问题、全量放心」。所以第 6 条的 1%→10%→50%→100% 节奏必须配两件事:① 每档 5–15 分钟观察里同时盯灰度实例的 RT/CPU 与全集群被限流比例,任一异常就停放量;② 限流阈值属高危配置,要有默认值、范围校验、审批与双人复核,出错秒级回滚到上一版本并验证真的生效。

L3|“如何防止有人直接改生产配置?” → 权限分级、变更审批、双人复核、审计日志、高危项二次确认/只读。把“人祸”纳入治理。

【评分标准】

档位答案特征
60 分知道配置中心能热更新
80 分能讲灰度与回滚
95 分按参数类型选灰度维度;强调动态刷新与审计;考虑流量打爆等副作用

【关联题】

  • 上游: 第 112 题(配置中心)
  • 相关: 发布工程、限流治理、第 122 题(超时阈值)

【自测】

  1. 为什么配置全量推送容易出事故? 参考答案:爆炸半径等于全集群,且生效极快,缺少验证与缓冲。
  2. 功能开关和限流阈值灰度方式有何不同? 参考答案:功能开关多按用户/流量;限流阈值多按实例/集群并配强监控。
  3. 业务代码如何支持配置灰度? 参考答案:运行时动态刷新(@RefreshScope/监听),不能只在启动时读一次。

121. 单机自增主键好好的,为什么要引入分布式 ID(ID 方案) ​

【考察内容】ID 方案选型判断

【题目】有人质疑:单机数据库自增主键用得好好的,为什么要搞分布式 ID?请说明自增主键在什么场景下会失效(分表、合并、跨库、泄露业务量),以及“全局唯一”和“分布式 ID”的区别,什么场景必须上。

【参考答案】

  1. 单机自增的问题:
    • 多库/分库分表:每个库独立自增,会重复;
    • 高并发写入:单库自增是热点,且依赖 DB;
    • 数据合并/迁移:自增 ID 跨库合并会冲突;
  2. 必须用分布式 ID 的场景:
    • 分库分表后的主键(全局唯一);
    • 分布式系统的主键(订单、消息、流水)需要全局唯一;
    • 高并发写入(不依赖 DB 生成);
    • 需要趋势递增或含时间信息的 ID;
  3. 不需要的场景:单库单表、ID 无业务含义、量级小——自增/业务 ID 即可;
  4. 附加考量:ID 安全性(防遍历)、前端精度(雪花 64 位转字符串)。
  5. 容量估算:单库自增在中低并发写(数百 TPS)通常足够;分库后若每库都要全局单号,自增失效。分布式 ID 容量见 106 题:号段/雪花可到数十万 ID/s 量级,远超多数业务。是否上分布式 ID 的判断阈值:是否分库分表、是否跨系统要唯一、单库自增是否成为热点、是否需要趋势递增。不是所有表都要雪花——单库配置表用自增更简单。
  6. 失败与降级:发号服务故障→双缓冲号段/本地雪花降级/备用号段;时钟回拨→107 题策略;ID 冲突→DB 唯一索引兜底。设计时不要把“自增号”当对外可枚举业务主键(防遍历)。

【原理溯源】

  • 为什么单库自增会重复? 自增计数器在库内。订单库与用户库都是 1,2,3...;分表后每表一套。唯一性范围=计数器作用域,作用域一扩就不唯一。
  • 为什么合并/迁移会冲突? 历史自增 ID 不带来源信息,多库数据落到同一存储时主键撞车。
  • 为什么高并发不喜欢单点发号? 每次 INSERT 都要获取自增值,热点与 DB 瓶颈耦合。分布式 ID 目标之一是发号去中心化/批量化。
  • “全局唯一”≠“必须用雪花”? 全局唯一是需求;雪花/号段/UUID/发号服务都是手段。单库场景用自增也能在“该库全局”唯一。要先定义唯一范围。
  • 为什么有人觉得“自增够用”? 在单体、单库、低并发成立。质疑者往往默认架构不变。正确回答是展示哪些架构变化会打破假设,而不是为分布式而分布式。

【选型判断树】

唯一性范围是否超过单库/单表?
├─ 否 → 自增即可,别上复杂 ID 系统
└─ 是
   ├─ 要趋势递增+简单 → 号段
   ├─ 要本地高性能生成 → 雪花
   ├─ 只要唯一可随机 → UUID
   └─ 另加:防遍历可加随机位;前端传字符串
分库分表主键:号段或雪花最常见

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“自增的唯一性范围是单库,架构一变就不够”
0:30–1:30失效场景分库、合并、热点
1:30–2:30概念澄清全局唯一是需求,方案有多种
2:30–3:30何时必须上分库分表、跨系统单据、高并发
3:30–4:30何时不必单库低量,克制设计
4:30–5:00收尾“按唯一范围和并发选,不为分布式而分布式”

【关键数字】

参数经验值说明
自增热点单点发号有上限与实例写入能力相关
分库数量≥2 就可能冲突若需跨库唯一
雪花/号段见 106 题具体结构
安全避免纯自增对外暴露防遍历
自增适用单库、中低并发、无跨库唯一别过度设计
分布式 ID 吞吐号段/雪花数十万/s 量级见 106
必上场景分库分表/跨系统单号/高并发热点先定义唯一范围

【追问链】(三层)

L1|“分表但同库,自增行不行?” → 若所有表共享同一自增序列或 ID 空间隔离,可能可以;但常见分表组件/多表并行仍会冲突,稳妥用号段/雪花并规划号段位。

L2|“分布式 ID 会不会泄露业务量?” → 会。趋势自增/雪花时间戳可推测单量与时间。对外单号可:加密编码、加随机位、改用不可枚举 ID(UUID 部分场景)、对外号与内部主键分离。内部主键仍可用趋势递增保索引性能。面试点:性能友好的内部 ID ≠ 适合暴露的安全单号。

L3|“为什么说不是所有表都该雪花?” → 单库日志表、配置表用自增更简单;盲目换雪花增加时钟与运维复杂度。按约束驱动设计。

【评分标准】

档位答案特征
60 分知道分库自增会重复
80 分能列失效场景并引出分布式 ID
95 分区分需求与方案;强调克制与唯一范围;提安全与精度

【关联题】

  • 同簇: 第 106 题(方案对比)、第 107 题(回拨)
  • 相关: 分库分表设计题

【自测】

  1. 自增主键失效的三类典型场景? 参考答案:分库分表、跨库合并迁移、高并发单点热点。
  2. “全局唯一”和“雪花算法”是什么关系? 参考答案:前者是需求,后者是实现之一;也可用号段、发号服务、UUID 等。
  3. 什么场景不必上分布式 ID? 参考答案:单库单表、量级小、无跨库唯一要求。

122. 服务 A 调 B 慢,A 的线程全被占住拖垮自己(调用容错) ​

【考察内容】分布式调用容错设计

【题目】服务 A 每次调用 B 要 3 秒才超时,B 一抖动,A 的所有线程都被挂起等待,A 自己也跟着挂。怎么设计调用容错(超时、重试、熔断、降级、隔离)?各个机制分别解决什么问题、怎么配置?

【参考答案】

  1. 调用前:设置合理的超时时间(连接超时+读超时,按 B 的 P99 设定,如 500ms/1s),超时即失败,避免无限等待;
  2. 调用失败处理策略:
    • 重试:仅对“幂等操作”重试,且限制重试次数+退避(1~2 次指数退避),防止重试风暴;写操作默认不重试;
    • 降级:返回兜底数据(缓存/默认值/上一次结果);
    • 熔断:B 连续失败(错误率>50%)时打开熔断器,快速失败不再调用 B,半开试探恢复;
    • 异步化:非核心调用转 MQ,失败进重试队列;
  3. 隔离:线程池隔离(B 慢只占自己的线程池,不拖垮其他调用);
  4. 兜底:失败记录+定时补偿;告警人工介入;
  5. 设计原则:超时+重试(幂等限次)+降级+熔断+隔离五件套,且“调用失败不能阻塞主流程”。
  6. 容量/线程预算估算:A 线程池 200,B 超时 3s,则最坏情况下 A 的吞吐被拖到 200/3s≈67 QPS——这就是被拖垮的数学。若超时改为 500ms,同线程池理论恢复到 200/0.5=400 QPS(仍要看 CPU)。熔断打开后错误率上升但线程释放,整体可用。线程池隔离:给 B 分配 20 线程,B 挂了最多占 20,主链路 180 仍可服务其他依赖。重试放大:重试 2 次时,对 B 的最大调用流量≈3 倍输入,熔断与限流必须配合。
  7. 失败与降级:超时→快速失败;重试→仅幂等、1–2 次退避;熔断→错误率超阈值打开,半开试探;降级→缓存/默认值/只读;隔离→舱壁线程池/信号量。非核心调用转 MQ。失败补偿与人工预案。监控:依赖 RT/错误率、熔断状态、线程池水位、降级触发次数。

【原理溯源】

  • 为什么 A 会被拖垮? 同步阻塞调用把下游延迟变成自己的线程占用。B 变慢→A 线程池耗尽→A 整体不可用。这是级联故障的经典形态。
  • 超时为什么是第一道防线? 它限制单次调用最坏占用,把“无限等待”变成“有限失败”。没有超时,其他容错都无从谈起。
  • 为什么重试必须限次且偏幂等? 无限重试会放大流量,把局部故障打成全局雪崩;非幂等重试可能重复扣款。重试是药,过量是毒。
  • 熔断解决什么? 在 B 已经持续失败时,继续调用只是浪费线程并加重 B 负担。熔断快速失败,给 B 恢复时间;半开探测避免过早恢复流量。
  • 隔离为什么必要? 即便有超时,多个下游共享大线程池时,一个慢下游仍可能占满池子。按下游拆线程池/信号量,故障域隔离。

【选型判断树】

调用特征?
├─ 核心同步路径
│   → 短超时 + 有限重试(仅幂等)+ 熔断 + 降级 + 线程池隔离
├─ 非核心通知/统计
│   → 异步 MQ + 重试队列,不占主链路线程
├─ 读多可缓存
│   → 降级读缓存/默认值
└─ 写且非幂等
    → 默认不重试;失败记录补偿/人工,而不是盲目重试

超时设置:
└─ 连接超时 < 读超时 < 上游总超时;超时要略大于下游 P99

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“同步慢调用会占满线程,形成级联故障”
0:30–1:30超时第一道防线
1:30–2:30重试与幂等限次退避,写不乱重试
2:30–3:30熔断与降级快失败与兜底
3:30–4:30隔离线程池拆分
4:30–5:00收尾“五件套组合,失败不阻塞主流程”

【关键数字】

参数经验值说明
超时略大于下游 P99如 500ms~2s 视业务
重试次数1~2 次指数退避
熔断阈值错误率 50%+(可配)结合最小请求数
半开探测少量请求试探成功再放量
线程池隔离按下游/依赖拆分限制故障域
被拖垮公式线程池÷超时200/3s≈67 QPS
降超时收益200/0.5s=400 QPS超时是第一防线
重试放大×(1+重试次数)必须配熔断限流

【追问链】(三层)

L1|“超时时间怎么定?” → 基于下游 P99/P999 与业务可接受延迟:超时要大于正常慢尾,小于上游总超时与用户可等待时间,并预留重试预算。

L2|“熔断打开后用户体验很差?” → 熔断是保命不是终点。打开后:① 快速失败+明确错误码;② 读接口走缓存/降级数据;③ 写接口排队或提示稍后重试;④ 半开状态放少量真实流量探测;⑤ 持续失败升级人工预案。产品侧要有“部分功能不可用”的文案。关键指标:降级触发后的成功率要高于“硬调慢下游”的成功率。

L3|“重试会不会把 B 彻底打死?” → 会,若全员无限重试。必须限次、退避、熔断、隔离,并对非核心流量降级/丢弃,形成负载削减而不是放大。

【评分标准】

档位答案特征
60 分提到要设超时
80 分能讲超时+重试+熔断
95 分五件套完整;重试限幂等限次;线程池隔离;超时与 P99 关系;能解释级联故障

【关联题】

  • 同簇: 第 110 题(幂等,重试前提)、第 113 题(注册中心故障下的重试)、第 115 题(定位慢调用)
  • 相关: 雪崩、限流、隔离舱壁模式相关题
  • 配置: 第 120 题(阈值灰度)

【自测】

  1. 为什么超时是容错第一原则? 参考答案:限制单次调用最坏线程占用,避免无限等待拖垮自身。
  2. 什么操作可以安全重试? 参考答案:幂等操作(查询、带幂等键的写);非幂等写默认不重试。
  3. 熔断器半开状态做什么? 参考答案:放少量试探请求,成功则恢复,失败则继续保持打开。


持续学习,持续积累。