← 返回题库 / 大纲

分布式系统设计

第 16 题:跨账户转账——TCC vs Saga vs 消息事务 分布式事务

【真实企业业务场景】
某持牌互联网银行核心账务系统:日转账 2000 万笔、峰值 TPS 8000、单笔 1 元~500 万。账户按 userId % 32 分 32 库 × 64 表,A、B 账户常落在不同库甚至不同 Region。强约束:①资金守恒(A 减 = B 加,不可多/少一分);②不能重复记账;③跨行最终一致、到账 P99 < 2s;④日终对账差错率 = 0。此时"传统本地事务"已失效——跨库无法用单机 ACID。
【面试官问题】
    1. 如何保证资金不超扣、不丢失?2. TCC / Saga / 本地消息表(事务消息) 分别怎么落地?3. 转出成功、转入失败如何补救?4. 消息重复 / 丢失怎么办?5. 热点账户(大 V余额频繁变动)怎么处理?6. 跨行跨系统怎么保证一致?7. 日终对账怎么做?
【候选人的标准回答】

第一步:分析问题。转账不是"两个 UPDATE",而是跨库、跨服务、可部分失败的分布式事务。目标不是强一致 ACID,而是资金守恒 + 可补偿 + 最终一致

第二步:核心挑战。跨库无法本地事务;网络/服务随时失败需可补偿;重复请求需幂等;热点账户行锁竞争;对账需覆盖所有边缘。

第三步:整体架构。采用"转账编排服务 + 账户服务(TCC 参与者) + 事务消息":Try 阶段冻结双方资金(同库本地事务),Confirm 真正扣加,Cancel 解冻;对跨行/异步场景用 RocketMQ 事务消息 + 对账兜底。

第四步:技术选型。同库内用 TCC(Seata);跨系统/跨行用本地消息表 + 事务消息 + 幂等消费;Saga 用于长流程(如"转账→购汇→汇款"多步可补偿链路)。

第五步:一致性。冻结(预留资源)→确认/补偿;消息表保证本地事务与发消息原子;消费者幂等保证 at-least-once 不重复记账。

第六步:高可用。Confirm/Cancel 失败重试(指数退避 + 最大次数);终有"对账 + 人工挂账"兜底;热点账户用明细流水 + 余额异步汇总降锁。

第七步:性能优化。Try 阶段只冻结不入账,锁持有极短;热点账户拆子账户/按金额分段;消费端批量确认。

【架构设计】
用户/渠道 ▼ Transfer-Orchestrator(转账编排服务) ├ Try:accountA.freeze(txId,a,amt) ┐ 同库本地事务 ├ Try:accountB.freeze(txId,b,amt) ┘ ▼ Account-Service(TCC 参与者,每库一个) ├ try : 写冻结流水(唯一 tx_id) + 余额-冻结额 ├ confirm: 冻结额→实际扣减/增加,删冻结流水 └ cancel: 释放冻结额,删冻结流水 ▼ 跨行/异步分支 RocketMQ 事务消息(half → 本地事务 → Commit) ▼ CDC/消费 对账服务(T+0 准实时 + T+1 日终强对账) ▼ 异常 挂账 + 告警 + 运营补账

瓶颈:Confirm/Cancel 重试风暴、热点账户行锁、跨 Region 网络延迟。

【技术方案深度解析】

三种方案对比:

方案优点缺点适用场景
TCC强预留、一致快、无长锁侵入业务、需写 Try/Confirm/Cancel、空回滚/防悬挂复杂同域内核心资金、低延迟
Saga长流程、松耦合、易编排无隔离性、补偿逻辑复杂、中间态可见跨多服务长事务(购汇汇款)
本地消息表/事务消息对业务侵入小、天然异步削峰只保证最终一致、依赖消费者幂等跨系统/跨行通知类

为什么当前场景选 TCC + 消息兜底?核心同库转账要"资金守恒且快"→ TCC;跨行通知要"解耦异步"→ 事务消息。两者用对账收口,构成"强一致为主、最终一致兜底"的双保险。

为什么不直接 2PC?2PC 有全局锁、协调者单点、同步阻塞,银行峰值 8000 TPS 下锁持有过长,性能与可用性都不可接受。

【关键技术点】
Seata TCC冻结/确认/补偿幂等(唯一 tx_id)RocketMQ 事务消息空回滚/防悬挂热点账户拆分日终对账资金守恒校验
【Java 实现示例】

① TCC 冻结(账户服务):

// 同库:业务余额表 + 冻结流水表(tx_id 唯一索引防重)
public boolean tryFreeze(String txId, Long acctId, BigDecimal amt) {
    int n = mapper.freeze(txId, acctId, amt); // UPDATE ... SET frozen=frozen+? WHERE acct=? AND (balance-frozen)>=?
    return n == 1; // 余额不足返回 false,触发 Cancel
}
public boolean confirm(BusinessActionContext ctx) {
    return mapper.unfreezeAndApply(ctx.getTxId()) == 1;
}
public boolean cancel(BusinessActionContext ctx) {
    return mapper.unfreeze(ctx.getTxId()) == 1; // 幂等:未冻结则直接成功
}

② 事务消息发送(跨行分支):

TransactionSendResult r = producer.sendMessageInTransaction(
    new Message("TRANSFER_OUT", payload), txId);  // 本地事务=写转账单(status=处理中)+Try冻结;回查查状态
【面试官可能继续追问】
【常见错误回答】
  • ❌ 用 @Transactional 包住两个 RPC 调用——跨库/跨服务不生效,且长事务锁库。
  • ❌ 先扣 A 再调 B,B 失败不补偿——造成资金丢失。
  • ❌ Cancel 不做幂等——重试时重复解冻导致超发。
【架构师评分标准】
初级 0~40
只会说"用分布式事务/2PC",讲不清补偿与幂等。
中级 40~60
知道 TCC 三阶段,能写出 Try/Confirm/Cancel。
高级 60~80
区分 TCC/Saga/消息表适用场景,讲清空回滚、防悬挂、幂等。
架构师 80~100
给出"TCC 主 + 消息兜底 + 对账收口"全链路,含热点账户与跨行降级。

第 17 题:全链路幂等性体系设计 幂等

【真实企业业务场景】
订单中心日均 3000 万单,入口链路:App → API Gateway → 订单服务 → MQ → 库存/优惠/物流。现实:①前端双击提交;②网关超时重试;③MQ 至少一次投递(at-least-once)重复消费;④上游服务重试。任一环节重复都会导致重复下单、重复扣款、重复发货。要求:任意重复请求结果一致、资损 = 0。
【面试官问题】
    1. 幂等要在哪几层做?2. 如何防止前端重复提交?3. 网关重试如何识别同一请求?4. MQ 重复消费怎么处理?5. 状态机如何防重?6. 幂等 token 怎么设计才安全?7. 幂等表怎么不成为新瓶颈?
【候选人的标准回答】

第一步:分析问题。幂等不是单点能力,是分层防御体系:越靠前拦截成本越低。

第二步:核心挑战。请求唯一标识如何生成与传递;重复判定存储的原子性与性能;不同业务对"幂等"语义不同(下单按单号、支付按流水号)。

第三步:整体架构。四层:①前端 防重 token;②网关按 X-Request-Id 去重(短 TTL);③服务层业务唯一键 + 唯一索引;④MQ 消费层 msgId 幂等表。最权威的是"数据库唯一约束 + 状态机"。

第四步:技术选型。Redis SET NX 做 token 校验(快、可过期);MySQL 唯一索引做最终裁判(强);本地状态机做业务层防重。

第五步:一致性。token 与订单号同源;消费幂等表与业务表同库同事务,保证"处理过"状态持久。

第六步:高可用。幂等存储(Redis/DB)本身需集群;唯一索引冲突即视为重复,不抛错给用户。

第七步:性能优化。幂等判断只在写入口做;读接口天然幂等不处理;幂等表按时间分片/定期清理。

【架构设计】
App(点击生成 requestId + 申请防重 token) ▼ API Gateway(X-Request-Id 透传;5s 内同 id 直接返回首次结果) ▼ 订单服务 ├ 校验 token(Redis SET NX,1 次性) ├ 落单:order_no 唯一索引 → INSERT IGNORE / ON CONFLICT └ 状态机:仅 (待支付→已支付) 等合法跃迁 ▼ RocketMQ ▼ 库存/优惠/物流服务(消费幂等表 msg_id 唯一) ▼ 重复 直接返回"已处理"

瓶颈:唯一索引写竞争、幂等表膨胀、token 服务成为新单点。

【技术方案深度解析】

为什么必须分层?只靠 DB 唯一索引,重复请求已打到库、浪费连接;前端+网关先挡掉 90% 重复,DB 只做"最终裁判"。

为什么用唯一索引而非先查后插?SELECTINSERT 有并发竞态(两个线程都查到不存在然后都插入),唯一索引由数据库保证原子性,DuplicateKeyException 即重复。

状态机防重:支付回调重复到达,只有 待支付→已支付 成功,第二次 已支付→已支付 被拒绝,天然幂等,且不依赖外部存储。

权衡:Redis token 快但可能丢(需 DB 唯一索引兜底);纯 DB 唯一索引可靠但每次写竞争。生产是"Redis 挡量 + DB 兜底"。

【关键技术点】
防重 TokenX-Request-Id 透传Redis SET NX唯一索引 INSERT IGNORE状态机跃迁MQ 消费幂等表ON CONFLICT幂等表分片
【Java 实现示例】

① 下单幂等(唯一索引兜底):

public OrderResult create(OrderCmd cmd) {
    // 1. token 一次性校验
    if (!tokenSvc.consume(cmd.token(), cmd.userId()))
        return OrderResult.repeat();
    // 2. 唯一索引最终裁判
    try { orderMapper.insert(cmd.toOrder()); }
    catch (DuplicateKeyException e) { return OrderResult.exist(cmd.orderNo()); }
    return OrderResult.ok(cmd.orderNo());
}

② MQ 消费幂等:

@Transactional
public void onConsume(Message msg) {
    if (idempotentMapper.insertIgnore(msg.getMsgId()) == 0) return; // 已处理
    bizHandler.handle(msg); // 与幂等表同库同事务
}
【面试官可能继续追问】
【常见错误回答】
  • ❌ 先 SELECT 查"是否存在"再决定插——并发竞态必重复。
  • ❌ 只前端 disabled 按钮——刷新/重发/恶意请求绕过。
  • ❌ 幂等键用 msgId 却忽略业务单号——换消息重发仍重复处理。
【架构师评分标准】
初级 0~40
以为前端 disabled 就够了。
中级 40~60
会用唯一索引防重复插入。
高级 60~80
分层(前端/网关/服务/MQ)+ 状态机。
架构师 80~100
全链路体系 + 降级兜底 + 幂等表治理 + 业务语义区分。

第 18 题:分布式锁选型与 Redlock 争议 分布式锁

【真实企业业务场景】
场景 A:集群部署的定时任务(报表统计)每天 02:00 跑,但 8 个实例都起来会重复跑 8 次。场景 B:营销"每人限领 1 张券",瞬时 5 万 QPS。场景 C:资金类"同一笔流水只清算一次"。三类场景对锁的可靠性要求天差地别,不能一套锁打天下。
【面试官问题】
    1. Redis 锁怎么实现才安全?2. 什么是看门狗/锁续期?3. Redlock 为什么被质疑?4. 锁超时但业务没跑完怎么办?5. 资金级互斥该用 Redis 还是 ZK?6. 锁误释放怎么防?7. 锁和事务谁先谁后?
【候选人的标准回答】

第一步:分析问题。分布式锁本质是在分布式系统里找一个"全局互斥点"。关键属性:互斥、死锁自由、容错、谁加锁谁释放。

第二步:核心挑战。网络延迟/GC 停顿导致"锁过期但业务还在跑";Redis 主从切换导致锁丢失;误释放他人锁;红锁的 fencing 缺失。

第三步:整体架构。按可靠性分级:①协调类/可重入(互斥即可)→ Redisson(SET NX + 看门狗续期 + Lua 释放);②强一致要求 → ZooKeeper/etcd 临时节点(顺序+watch,带 fencing token);③资金级互斥 → 不用锁,用"唯一约束/状态机/TCC"。

第四步:技术选型。Redisson(基于 Redis,性能高,适合非资金互斥);Curator/ZK(强一致,慢);etcd(lease+watch,云原生友好)。

第五步:一致性。释放锁用 Lua 校验 value(唯一 token)再删,防误释放;Redlock 多节点多数派获取,但 Martin Kleppmann 指出仍需 fencing token 防 GC 致旧锁复活。

第六步:高可用。锁服务本身集群;业务侧"获取锁失败即快速失败或排队",不让请求堆积;锁内业务必须可重入安全。

第七步:性能优化。锁粒度尽量小(行级而非表级);锁外不做 IO;能用乐观锁/唯一索引替代就不加分布式锁。

【架构设计】
场景A 定时任务(弱互斥) └ Redisson 锁(lease 30s + 看门狗每 10s 续期),抢到者执行 场景B 限领1券(高并发互斥) └ Redis Lua 原子:SET NX EX + 用户已领 SET,1 次性 场景C 资金清算(强一致) └ 不用锁 → 唯一流水号约束 + 状态机(见 Q16/Q17) └ 若必须锁 → ZK 临时节点 + fencing token 顺序执行

瓶颈:Redlock 多节点 RTT、ZK 写性能、锁粒度过大导致串行。

【技术方案深度解析】

为什么 Redlock 有争议?Martin Kleppmann 指出:即使多数派拿到锁,若持锁线程发生长时间 GC 停顿超过锁租期,锁过期,另一线程拿到锁,出现"双持锁"。Redis 无单调 fencing token,无法让旧锁操作失效。结论:Redlock 适合"互斥即可"场景,不适合"互斥正确性攸关资金"场景

为什么用看门狗?固定 EX 过期,业务偶尔超时就被误释放。看门狗在业务未结束前自动续期(Redisson 默认 1/3 租期续一次),业务完成才释放。

为什么 Lua 释放?GETDEL 有窗口:A 查到自己锁→A 锁过期→B 拿到锁→A 删了 B 的锁。Lua 原子校验 value 再删,杜绝误释放。

权衡:Redis 锁快但弱一致;ZK 锁慢但强一致带 fencing;etcd 介于其间且 K8s 原生。资金场景干脆不用锁,改用约束/状态机。

【关键技术点】
SET NX EX看门狗续期Lua 原子释放Redlockfencing tokenZK 临时节点etcd lease锁粒度
【Java 实现示例】

① Redisson 安全使用:

private final RedissonClient redisson;
public void safeRun() {
    RLock lock = redisson.getLock("job:report:day");
    if (!lock.tryLock(0, 30, TimeUnit.SECONDS)) return; // 抢不到即退出
    try { doReport(); }            // 看门狗后台自动续期
    finally { lock.unlock(); }   // 校验持有者后释放
}

② ZK 强一致锁(带 fencing,Curator):

InterProcessMutex zkLock = new InterProcessMutex(client, "/locks/settle");
zkLock.acquire(); try { Long fence = zkLock.getFence(); settle(fence); } finally { zkLock.release(); }
【面试官可能继续追问】
【常见错误回答】
  • setnx 后不设置过期 → 进程挂死锁。
  • ❌ 用固定 EX 又不做续期 → 业务超时锁被他人抢走,双写。
  • ❌ 资金清算用 Redis 锁 → 主从切换瞬间丢锁造成重复清算。
【架构师评分标准】
初级 0~40
只用 setnx 不加过期。
中级 40~60
知道加过期 + 唯一 value 释放。
高级 60~80
讲清看门狗、Lua 释放、Redlock 局限。
架构师 80~100
按可靠性分级选型 + fencing + "资金不用锁用约束"的判断力。

第 19 题:订单履约最终一致性——Outbox + CDC 最终一致

【真实企业业务场景】
下单成功后需联动 5 个域:扣库存、发优惠券、通知物流、记积分、发短信。各域独立数据库、独立服务。要求:下单成功则各域最终都执行,不允许"下单了但没扣库存/没发货"。峰值 1 万订单/秒,不能因某个下游慢而拖垮下单。
【面试官问题】
    1. 为什么不用同步 RPC 调 5 个服务?2. 本地事务 + 发 MQ 怎么保证原子?3. Outbox 表怎么设计?4. CDC 是什么、为什么用?5. 消费失败怎么办?6. 消息顺序有要求吗?7. 如何保证不漏发?
【候选人的标准回答】

第一步:分析问题。这是典型"一个写、多个副作用"的最终一致问题。核心是本地事务与发消息的原子性——不能出现"库写了但消息没发"或反之。

第二步:核心挑战。本地 DB 事务与 MQ 发送不在同一事务;下游可能失败/慢;需保证 at-least-once 且消费者幂等。

第三步:整体架构。本地消息表(Outbox) + CDC 投递:下单时在同一 DB 事务里写订单表 + outbox 表(待发消息);Debezium/Canal 监听 binlog 把 outbox 发到 Kafka;各消费者幂等处理。

第四步:技术选型。Outbox 表 + Debezium(基于 binlog,无侵入)/Canal;Kafka 做可靠管道;消费端 Redis/DB 幂等表。

第五步:一致性。事务提交则 outbox 必落库 → CDC 必捕获 → 必发出;消费端幂等保证重复无害。at-least-once + 幂等 = 最终一致。

第六步:高可用。CDC 集群多副本;outbox 有"已发/未发"状态,发失败可重投;消费失败入死信 + 告警。

第七步:性能优化。outbox 批量扫发;binlog 异步不阻塞主链路;只发必要字段。

【架构设计】
订单服务(单体事务) ├ INSERT 订单 (status=已创建) └ INSERT outbox (topic, payload, status=待发) ← 同事务,原子 ▼ 事务提交 → binlog Debezium / Canal(CDC) └ 捕获 outbox 变更 → 发 Kafka(标记已发) ▼ Kafka(ORDER_CREATED) ├→ 库存服务(幂等扣减) ├→ 优惠券服务(幂等发券) ├→ 物流服务(创建运单) ├→ 积分服务 └→ 通知服务 ▼ 任一失败 死信队列 + 重试 + 告警

瓶颈:CDC 消费滞后、Kafka 分区不均、下游幂等表写竞争。

【技术方案深度解析】

为什么不用"先发 MQ 再写库"或"先写库再发 MQ"?前者 MQ 成功库失败→下游收到假消息;后者库成功 MQ 失败→下游收不到。两者都不原子。Outbox 把消息当数据写进同一库,靠 DB 事务保证原子,再由 CDC 异步搬运,彻底解决。

为什么用 CDC 而非定时扫 outbox?定时扫有延迟、且增加 DB 压力;CDC 基于 binlog 近实时、对业务零侵入、不抢主库连接。

为什么需要幂等?CDC 至少一次投递(如重平衡重发),消费端必须按业务键幂等,否则重复扣库存。

权衡:事务消息(RocketMQ)也能解决原子性但耦合 MQ;Outbox+CDC 与 MQ 解耦、可换管道,更适合异构下游,但引入 CDC 运维复杂度。

【关键技术点】
本地消息表 OutboxDebezium/Canalbinlog CDCKafka 可靠投递消费幂等at-least-once死信重试事务原子性
【Java 实现示例】

① 同事务写订单 + Outbox:

@Transactional
public void createOrder(OrderCmd cmd) {
    Order o = orderMapper.insert(cmd.toOrder());
    outboxMapper.insert(new Outbox("ORDER_CREATED", o.toJson(), "PENDING"));
} // 提交即两者都持久,CDC 必然捕获

② 消费端幂等:

@Transactional
public void onOrderCreated(OrderCreated ev) {
    if (idempotent.insertIgnore(ev.orderId(), "STOCK") == 0) return;
    stockSvc.deduct(ev.skuId(), ev.qty());
}
【面试官可能继续追问】
【常见错误回答】
  • ❌ 下单里同步 RPC 调 5 个服务——一个下游慢拖垮下单,且部分成功难回滚。
  • ❌ 先发 MQ 再写库——库失败下游已执行,状态不一致。
  • ❌ 消费不幂等——CDC 重发导致重复扣库存。
【架构师评分标准】
初级 0~40
同步 RPC 调所有下游。
中级 40~60
知道用 MQ 异步解耦。
高级 60~80
讲清 Outbox + 事务原子 + 消费幂等。
架构师 80~100
Outbox+CDC 全链路 + 兜底重发 + 选型权衡(vs 事务消息)。

第 20 题:CAP 与 BASE 的取舍实战 CAP/BASE

【真实企业业务场景】
某电商系统由多个子系统构成:注册中心(Nacos)、配置中心、订单库(分库分表)、商品缓存(Redis)。一次机房网络分区(Partition)发生时,每个组件要决定"保一致性 C 还是保可用性 A"。选错轻则体验差,重则资损或全站不可用。
【面试官问题】
    1. CAP 三选二怎么理解?2. 分区发生时注册中心该选 C 还是 A?3. 订单库分区怎么选?4. Redis 缓存分区怎么选?5. BASE 是什么?6. 最终一致何时可接受、何时不可?7. 如何度量"一致性窗口"?
【候选人的标准回答】

第一步:分析问题。CAP 的精确含义:当网络分区 P 发生时,只能在一致性 C 与可用性 A 间选一。无分区时两者可兼得。所以讨论 CAP 实际是"分区时怎么选"。

第二步:核心挑战。不同组件对 C/A 的容忍度不同;错误选 A 会脏读/资损,错误选 C 会拒绝服务。

第三步:整体架构。按数据性质分级:①注册/配置中心 → 选 A(AP,短暂不一致可自愈,全站不能因配置中心挂而不可用);②订单/账户核心库 → 选 C(CP,宁拒绝不可错账);③缓存 → 选 A(AP,允许短暂脏读,最终回源)。

第四步:技术选型。注册中心 Nacos/Consul(AP 模式或 Raft CP);订单库用主从+强一致写;缓存用 Redis 最终一致。

第五步:一致性。核心库靠主从同步+半同步;缓存靠"写后删/更新"最终一致,容忍秒级窗口。

第六步:高可用。注册中心多节点;核心库主从自动切换;缓存多副本。

第七步:性能优化。能放宽的 C 尽量放宽(缓存、计数),把强一致留给"金额/库存/订单状态"等少数核心。

【架构设计】
组件 分区时选择 后果 注册中心 Nacos AP(可用) 短暂拿到旧节点列表,但服务不中断 配置中心 AP 读到旧配置,但发布不卡住 订单/账户库 CP(一致) 主库不可达则拒绝写入,绝不脏写 Redis 缓存 AP 读到旧值,后台回源/过期纠正 商品搜索 ES AP 近实时索引,秒级延迟可接受

瓶颈:CP 组件在分区时成为不可用点;AP 组件需业务容忍短暂不一致。

【技术方案深度解析】

为什么注册中心选 A?注册中心短暂不一致(某节点列表旧)只会导致请求短暂打到旧实例,可重试自愈;但若选 C,注册中心主节点挂了全网无法发现服务 → 全站雪崩,代价更大。

为什么订单库选 C?金额写错是资损,不可接受;分区时拒绝部分写入(返回"系统繁忙")比脏写安全。

BASE = Basically Available + Soft state + Eventual consistency。是 CAP 中 AP 的延伸工程化:基本可用(降级)、软状态(中间态存在)、最终一致。

权衡:不是"要么 C 要么 A"的永久选择,而是按子域分别决策。架构师的功力在于划清"哪些必须 C、哪些可 A"。

【关键技术点】
CAP 定理分区容错AP vs CPBASE最终一致性窗口主从半同步注册中心选 A核心库选 C
【Java 实现示例】

① 缓存与 DB 双写的最终一致(AP 容忍):

public void updatePrice(Long skuId, BigDecimal price) {
    db.update(skuId, price);              // 先更源
    redis.delete("sku:" + skuId);    // 删缓存,下次读回源(Cache-Aside)
}                                        // 短暂窗口内可能读到旧值(AP 可接受)

② 核心写入强一致(CP 保护):

// 订单状态变更走状态机 + 行锁,分区时主库不可达即失败,绝不脏写
orderMapper.updateWithVersion(o); // 乐观锁 version 防并发脏写
【面试官可能继续追问】
【常见错误回答】
  • ❌ "CAP 就是三选二,永远只能保两个"——忽略"仅分区时"的前提。
  • ❌ 把注册中心也设成 CP 强一致——主挂全站雪崩。
  • ❌ 不分场景全用最终一致——核心金额脏写资损。
【架构师评分标准】
初级 0~40
死记"三选二"。
中级 40~60
知道分区时 C/A 取舍。
高级 60~80
能按组件分别决策(注册中心 A、订单 C)。
架构师 80~100
结合实际划清"必须 C 的边界"+ BASE 工程化 + 一致性窗口度量。

第 21 题:分布式 ID——雪花算法与时钟回拨 分布式ID

【真实企业业务场景】
订单、支付流水、物流单号日均数十亿,需全局唯一、趋势递增(利于 B+Tree 索引)、高吞吐(>10 万/秒)、低延迟。不能用 MySQL AUTO_INCREMENT(单点+泄露总量);UUID 无序导致索引随机 IO。引入雪花算法(Snowflake)后,某次宿主机 NTP 校时时钟回拨 30ms,出现 ID 重复告警。
【面试官问题】
    1. 雪花结构怎么设计?2. 时钟回拨为什么危险?3. 回拨 30ms 怎么处理?4. 回拨几秒怎么办?5. 机器号如何分配不冲突?6. 序列号溢出怎么办?7. 还有哪些方案(号段/UUID)?
【候选人的标准回答】

第一步:分析问题。分布式 ID 要解"全局唯一 + 有序 + 高性能"。雪花用"时间戳+机器+序列"组合,单节点内存生成无中心依赖,但依赖时钟单调递增

第二步:核心挑战。时钟回拨产生重复 ID;机器号分配冲突;单节点序列溢出;不同数据中心时钟漂移。

第三步:整体架构。雪花(工作机号由部署平台分配)+ 时钟回拨防御(等待/缓存上次最大 + 报警)+ 号段(Leaf-segment)作备用。

第四步:技术选型。Snowflake(简单高性能);美团 Leaf(号段 + snowflake 双模式);百度 UidGenerator(解决时钟回拨+机器号由数据库分配);Redis INCR(简单但有中心)。

第五步:一致性。机器号全局唯一(注册中心/配置分配)保证不同节点不撞;同节点靠序列+时间戳保证不重复。

第六步:高可用。时钟回拨防御保证不产重复;号段模式可预取缓存,DB 短暂不可用仍发号。

第七步:性能优化。号段预取(一次取 1000 个缓存在内存);序列号 12 位支持单节点 4096/ms。

【架构设计】
Snowflake 64bit: 1bit 符号 | 41bit 毫秒时间戳 | 10bit 机器号 | 12bit 序列号 ▼ ID 生成服务(每实例持 workId) ├ 正常:timestamp<<22 | workId<<12 | seq++ ├ 回拨<阈值:自旋等待时钟追平 ├ 回拨略大:用 lastMaxId+1 推进(借用序列高位) └ 回拨严重:抛异常 + 切备用 workId / 号段模式 ▼ 备用 Leaf-segment(DB 号段预取,内存发号)

瓶颈:时钟回拨、workId 冲突、单节点 4096/s 上限(实际够,不够则多实例)。

【技术方案深度解析】

为什么不用 AUTO_INCREMENT?单点瓶颈、暴露业务量、跨库分片无法全局有序。但分库分表内仍可用步长自增(如 32 库各 id%32)。

为什么时钟回拨危险?回拨后时间戳变小,若序列号也重置,可能与历史 ID 完全相同 → 主键冲突/幂等失效。防御:记录 lastTimestamp,回拨时等待;或回拨量大时用"扩展位/历史最大+1"。

为什么用号段?Leaf-segment 从 DB 取一段号(如 1~1000)缓存在内存,DB 挂不影响发号;缺点是 ID 不连续、可预测。

权衡:雪花高性能但怕时钟;号段稳但 ID 可预测;UidGenerator 用"原子级"解决回拨。按业务选:订单用雪花(或号段),内部流水可用 UUID。

【关键技术点】
Snowflake 结构时钟回拨防御workId 分配序列号溢出Leaf-segment 号段UidGenerator趋势递增幂等与唯一
【Java 实现示例】

① 雪花(含回拨防御):

public synchronized long nextId() {
    long ts = System.currentTimeMillis();
    if (ts < lastTs) {                       // 时钟回拨
        long delta = lastTs - ts;
        if (delta <= 5) { ts = waitUntil(lastTs); }  // 小幅:等追平
        else { throw new ClockBackException(delta); } // 大幅:报警切备用
    }
    seq = (ts == lastTs) ? (seq + 1) & MASK : 0;
    if (seq == 0 && ts == lastTs) ts = nextMillis();
    lastTs = ts;
    return (ts << 22) | (workId << 12) | seq;
}
【面试官可能继续追问】
【常见错误回答】
  • ❌ 用 new Random() / UUID 做订单主键——无序导致索引碎片化、IO 飙升。
  • ❌ 回拨时不处理直接生成——ID 重复主键冲突。
  • ❌ workId 写死在配置——多实例冲突。
【架构师评分标准】
初级 0~40
用 UUID/随机做主键。
中级 40~60
能讲雪花结构。
高级 60~80
讲清时钟回拨防御 + workId 分配。
架构师 80~100
多方案对比(雪花/号段/UidGenerator)+ 跨 DC + 回拨分级应对。

第 22 题:一致性 Hash 与数据分片路由 分片路由

【真实企业业务场景】
缓存集群 8 节点、分库分表 32 库。当扩容到 16 节点时,若用普通 hash(key)%N几乎所有 key 的映射都会变,引发缓存击穿 + 全量数据迁移。热点 SKU(某爆款)又集中落到同一节点,单节点被打满。需要"扩缩容只迁移少量数据 + 热点打散"。
【面试官问题】
    1. 普通取模扩容的问题?2. 一致性 Hash 怎么解决?3. 虚拟节点解决什么?4. 数据倾斜怎么办?5. 分库分表路由怎么做?6. Redis Cluster 的槽位?7. 热点 key 如何再打散?
【候选人的标准回答】

第一步:分析问题。分片路由要解决两件事:①均匀分布;②扩缩容时迁移成本最低;③热点可打散。

第二步:核心挑战。取模法扩容迁移率近 100%;节点性能不均导致倾斜;单 key 热点。

第三步:整体架构。一致性 Hash(带虚拟节点)做缓存路由(扩缩容仅迁移 1/N 数据);分库分表用"基因法 + 范围/取模混合";Redis Cluster 用 16384 固定哈希槽(槽可在节点间迁移,与节点数解耦)。

第四步:技术选型。缓存:一致性 Hash + 虚拟节点;分库分表:ShardingSphere(取模/基因/范围);Redis Cluster:哈希槽。

第五步:一致性。路由规则全局一致(配置中心下发),避免不同节点算到不同库。

第六步:高可用。虚拟节点让单物理节点失效时流量均摊到其他节点;槽迁移在线进行不中断。

第七步:性能优化。热点 key 加随机后缀(如 sku:123:{0..99})拆分为多个子 key 分散读。

【架构设计】
一致性 Hash 环(0~2^32) ├ 物理节点 A/B/C 映射到环 └ 每物理节点 100~200 虚拟节点(均匀撒环) ▼ key 顺时针找最近节点 扩容 +1 节点:仅该节点到下一节点间 key 迁移 ≈ 1/(N+1) ▼ Redis Cluster:16384 槽,槽→节点映射可迁移 ▼ 分库分表:user_id 基因(后几位) % 32 路由,同用户订单同库(避免跨库 JOIN)

瓶颈:虚拟节点数不足仍倾斜;槽迁移时的双写/原子切换;热点 key 单点。

【技术方案深度解析】

为什么取模扩容灾难?k % 8 → k % 16 只有 1/16 的 key 命中旧位置,其余需重分布,缓存全失效、DB 被打穿。

为什么虚拟节点?物理节点少时 Hash 环上分布稀疏,易倾斜;每个物理节点映射成多个虚拟节点,key 更均匀落到各物理机。

为什么 Redis 用固定 16384 槽?槽与节点数解耦:扩缩容迁移"槽"而非"key 直接按节点算";槽数固定使客户端/集群元数据稳定,迁移粒度可控。

权衡:一致性 Hash 适合缓存(允许失效重加载);分库分表因要稳定路由多用取模+基因法(避免跨片查询),迁移靠双写/CDC 灰切。

【关键技术点】
一致性 Hash虚拟节点哈希槽 16384基因法分片ShardingSphere热点打散在线槽迁移取模扩容陷阱
【Java 实现示例】

① 一致性 Hash(带虚拟节点):

TreeMap<Long, String> ring = new TreeMap<>();   // hash → 物理节点
void addNode(String node) {
    for (int i = 0; i < VNODES; i++)
        ring.put(hash(node + "#" + i), node);
}
String route(String key) {
    Long h = hash(key);
    Map.Entry<Long,String> e = ring.ceilingEntry(h);
    return e == null ? ring.firstEntry().getValue() : e.getValue(); // 顺时针
}

② 热点打散读取:

String k = "sku:" + skuId + ":" + (ThreadLocalRandom.current().nextInt(SUB)); // 写时全量写,读时随机取一副本
【面试官可能继续追问】
【常见错误回答】
  • ❌ 缓存与分库都用 hash%N——扩容全量失效/迁移。
  • ❌ 一致性 Hash 不加虚拟节点——数据严重倾斜。
  • ❌ 热点 key 不拆分——单节点被打满拖垮集群。
【架构师评分标准】
初级 0~40
只会 hash%N。
中级 40~60
知道一致性 Hash。
高级 60~80
讲清虚拟节点 + 槽迁移 + 基因法。
架构师 80~100
缓存/分表/Redis Cluster 多方案对比 + 热点打散 + 扩容零停机。

第 23 题:分布式定时任务设计与防重复执行 定时任务

【真实企业业务场景】
每天 02:00 跑"前日交易对账 + 账单生成 + 用户积分结算",数据量千万级,单实例 40 分钟跑不完。系统 8 实例部署,要求:①只跑一次不重复;②能分片并行缩短时间;③失败可重试;④某实例宕机任务不丢;⑤执行过程可观测。
【面试官问题】
    1. @Scheduled 在集群下的问题?2. 如何保证只跑一次?3. 分片广播怎么实现?4. 任务失败如何重试?5. 执行中实例宕机怎么办?6. 任务幂等怎么做?7. XXL-JOB / Elastic-Job 区别?
【候选人的标准回答】

第一步:分析问题。单机 @Scheduled 集群下会每个实例都跑 → 重复。需"调度中心统一编排 + 执行器分片 + 执行幂等"。

第二步:核心挑战。重复执行、大数据量串行慢、失败无重试、宕机任务丢失、缺乏监控。

第三步:整体架构。引入调度中心(XXL-JOB Admin / Elastic-Job / SchedulerX):触发时选一个执行器执行(或分片广播到所有执行器并行处理不同分片);任务有失败重试、超时告警、执行日志。

第四步:技术选型。XXL-JOB(轻量、易上手、Bean/GLUE 模式);Elastic-Job(基于 ZooKeeper、弹性分片);阿里 SchedulerX(云原生、可视化强)。

第五步:一致性。调度中心用 DB 锁/选主保证"同一任务同一时刻只有一个触发";分片参数下每个执行器处理指定分片;业务按分片键幂等。

第六步:高可用。调度中心集群 + DB;执行器多实例,宕机任务由其他实例接管(失效转移);分片重平衡。

第七步:性能优化。分片并行(8 实例各处理 1/8);每分片内线程池批量;任务拆小步可断点续跑。

【架构设计】
调度中心(XXL-JOB Admin,集群 + DB 选主) ├ 02:00 触发 job:settle ├ 路由策略:分片广播 → 通知全部 8 执行器 ▼ 执行器 A~H(各持 shardIndex / shardTotal) ├ A 处理 user%8==0,B 处理 ==1 ...(按分片键并行) ├ 失败:指数退避重试(最多 N 次) └ 超时:标记失败 + 告警 + 可手动重跑 ▼ 业务表(按分片键幂等:biz_date+shard 唯一)

瓶颈:分片不均、单分片过大、DB 锁竞争、重试风暴。

【技术方案深度解析】

为什么不用 @Scheduled?集群每实例各跑一次,重复对账/重复发账单。除非加分布式锁兜底,但失去调度能力(重试/监控/分片)。

为什么分片广播?千万数据单线程 40 分钟;分片后 8 实例并行,每实例处理 1/8,时间降到约 1/8(理想)。分片键用 user_id % shardTotal

为什么还要业务幂等?重试/失效转移可能让同一分片被两个实例各跑一次;按 biz_date + shard 唯一约束,重复执行安全。

权衡:XXL-JOB 简单够用;Elastic-Job 弹性分片更强但依赖 ZK;云上直接用 SchedulerX 省运维。

【关键技术点】
调度中心选主分片广播失效转移失败重试分片键任务幂等XXL-JOBElastic-Job
【Java 实现示例】

① XXL-JOB 分片执行:

@XxlJob("settleJob")
public void settle() {
    int idx = XxlJobHelper.getShardIndex();
    int total = XxlJobHelper.getShardTotal();
    long max = userIdMax();
    for (long u = idx; u <= max; u += total)   // 本分片负责 user%total==idx
        settleOne(u);
}

② 业务幂等防重:

try { settleLogMapper.insert(bizDate, shard); }  // 唯一键 (biz_date,shard)
catch (DuplicateKeyException e) { return; }     // 已跑过,跳过
【面试官可能继续追问】
【常见错误回答】
  • ❌ 集群直接 @Scheduled 不加锁——8 实例跑 8 次对账。
  • ❌ 任务不幂等——重试/转移导致重复结算。
  • ❌ 单线程跑千万数据——超时失败。
【架构师评分标准】
初级 0~40
集群 @Scheduled 无防护。
中级 40~60
加分布式锁防重复。
高级 60~80
用调度框架 + 分片 + 重试。
架构师 80~100
分片并行 + 失效转移 + 幂等 + 监控告警全链路。

第 24 题:分布式鉴权——JWT vs Session 鉴权

【真实企业业务场景】
平台有 App、Web、小程序、开放 API 四类客户端,后端 30+ 微服务。要求:①用户登录一次多端可用(SSO);②网关统一鉴权不过业务库;③支持单设备踢下线;④敏感操作二次校验;⑤令牌泄露风险可控。当前方案 Session + Redis 集中存储,但网关每请求查 Redis 成为瓶颈(峰值 20 万 QPS)。
【面试官问题】
    1. JWT 与 Session 本质区别?2. JWT 怎么验签不查库?3. 注销/踢人怎么做?4. JWT 过期与刷新?5. 令牌泄露怎么办?6. 网关怎么统一鉴权?7. 什么时候该用 Session?
【候选人的标准回答】

第一步:分析问题。鉴权要解决"我是谁 + 我有什么权限 + 怎么可信"。Session 把状态存服务端,JWT 把状态放令牌里(无状态)。

第二步:核心挑战。Session 集中存储成瓶颈 + 需共享;JWT 无法主动失效 + 体积大;两者都要防泄露。

第三步:整体架构。JWT(短期访问令牌)+ Redis 黑名单(仅存已注销 jti)+ 刷新令牌:网关验 JWT 签名(本地公钥,不查库);注销把 jti 加黑名单(短 TTL);刷新令牌长有效期存 Redis,可吊销实现踢人。

第四步:技术选型。JWT(RS256 非对称,网关用公钥验签);Redis 存黑名单/刷新令牌;Spring Security + 资源服务器(Gateway 统一 OAuth2 资源校验)。

第五步:一致性。黑名单在各网关节点共享(Redis);令牌时钟用 NTP 同步防 exp 误判。

第六步:高可用。JWT 无状态天然水平扩展;黑名单 Redis 集群;刷新令牌可轮换。

第七步:性能优化。网关只验签不查库(20 万 QPS 无压力);黑名单用布隆过滤器前置减少 Redis 查询。

【架构设计】
登录服务 ├ 校验凭证 → 发 access_token(JWT, exp=15m) + refresh_token(Redis, exp=7d) ▼ 客户端(携带 Authorization: Bearer JWT) ▼ API Gateway(本地公钥验签 exp/签名 → 解析 claims → 查黑名单布隆过滤器) ├ 通过 → 透传 userId/roles 到下游(Header) └ 黑名单/过期 → 401 → 用 refresh 换发 ▼ 微服务( trusts gateway,直接读 userId/roles)

瓶颈:黑名单查询(布隆过滤缓解)、JWT 体积、时钟漂移。

【技术方案深度解析】

为什么 JWT 能不查库?签名(RS256)由授权服务私钥签、网关公钥验,篡改即失效;claims 自带 userId/roles/exp,网关本地解析即可,无需访问用户库。把"状态"从服务端搬到令牌。

为什么 JWT 难注销?令牌自包含、服务端无状态,无法"让某令牌失效"。方案:①短 exp(15m)+ 刷新;②注销时把 jti 加黑名单(只存到令牌自然过期);③踢人=删刷新令牌。

为什么还要 Redis?纯 JWT 无法主动失效,黑名单/刷新令牌仍需 Redis——所以"完全无状态"是误解,JWT 是把高频读(鉴权)无状态化,低频写(注销)仍用 Redis。

权衡:Session 适合"需频繁吊销、服务端可控"场景(如后台管理);JWT 适合"无状态、跨域、微服务网关统一校验"场景。

【关键技术点】
JWT RS256公钥验签黑名单 jti刷新令牌布隆过滤器OAuth2 资源服务器网关统一鉴权时钟同步
【Java 实现示例】

① 网关验签(Spring Security 资源服务器):

// 资源服务器用公钥验证 JWT,无需查用户库
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
// 黑名单过滤:已注销 jti 拒绝
if (bloom.mightContain(jti) && redis.has(jti)) throw new RevokedException();

② 注销(加入黑名单,TTL=令牌剩余有效期):

public void logout(String jti, long ttlSec) {
    redis.set("bl:" + jti, "1", Duration.ofSeconds(ttlSec)); // 仅存到令牌自然过期即失效
}
【面试官可能继续追问】
【常见错误回答】
  • ❌ 把用户完整信息塞进 JWT——体积大、且无法吊销。
  • ❌ 以为 JWT 完全无状态——注销仍需黑名单/Redis。
  • ❌ 用 HS256 对称密钥且前端也持有——密钥泄露可伪造。
【架构师评分标准】
初级 0~40
只知道 JWT 不用存库。
中级 40~60
知道签名验签。
高级 60~80
讲清黑名单/刷新令牌/网关统一鉴权。
架构师 80~100
无状态边界 + 吊销方案 + 性能(布隆)+ 安全(绑定/二次校验)。

第 25 题:配置中心设计——动态推送与灰度 配置中心

【真实企业业务场景】
系统有 200+ 微服务、3000+ 实例。运营要"动态调整限流阈值、降级开关、灰度比例"而不重启。当前配置写在配置文件,改一次需发版 30 分钟。且一次误改"全局限流=0"导致全站 502。需要:①动态生效;②按集群/IP 灰度;③改错可秒级回滚;④变更可审计。
【面试官问题】
    1. 配置中心要解决什么?2. 动态推送怎么实现(长轮询 vs 长连接)?3. 灰度发布怎么做?4. 配置热更新踩过什么坑?5. 回滚怎么做?6. 配置和代码如何解耦?7. Nacos vs Apollo 区别?
【候选人的标准回答】

第一步:分析问题。配置中心把"会变的环境参数"从代码剥离,要求:实时推送、灰度、回滚、审计、高可用。

第二步:核心挑战。推送实时性、全量推送风暴、热更新导致 Bean 重建副作用、误改全局影响、配置版本管理。

第三步:整体架构。配置中心(Nacos/Apollo)存配置 + 版本;客户端长连接/长轮询监听变更;变更按"集群/App/IP"维度灰度下发;本地缓存兜底(断网用上次值);管理端支持回滚。

第四步:技术选型。Nacos(动态服务发现+配置一体,长连接推送);Apollo(灰度/回滚/审计强,长轮询);Spring Cloud Config(Git 后端,需配合 bus)。

第五步:一致性。配置版本号递增;客户端拿到新版本校验后生效;本地文件缓存保证配置中心宕机仍可启动。

第六步:高可用。配置中心集群;客户端本地快照;推送失败定时补偿拉取。

第七步:性能优化。只推送变更 key(增量);灰度缩小影响面;高频配置(限流)走本地内存 + 监听。

【架构设计】
管理后台(改配置 / 灰度 / 回滚 / 审计) ▼ 写 + 版本 配置中心集群(Nacos/Apollo,配置+版本存储) ├ 长连接/长轮询 → 推变更(增量 key) └ 本地快照文件(客户端断网兜底) ▼ 灰度:先 1 台 → 10% → 全量 客户端(监听 @RefreshScope / ConfigListener) ├ 校验(格式/范围)→ 生效 └ 异常 → 拒绝 + 告警(不破坏运行实例)

瓶颈:全量推送风暴、热更新 Bean 重建、误改全局。

【技术方案深度解析】

为什么用长轮询而非长连接?Apollo 用长轮询(HTTP 挂起 30s),实现简单、穿透防火墙好、服务端无连接数压力;Nacos 2.x 用 gRPC 长连接推送更实时。两者都能"准实时"。

热更新大坑:@RefreshScope 会销毁并重建 Bean,若 Bean 持有连接池/线程池/定时任务,重建会泄漏或重复注册。对策:连接池等"不便热更"的配置走重启;只有开关/阈值类安全热更。

为什么必须灰度?"限流=0"全量下发=全站 502。先 1 台验证、再 10%、再全量,出现副作用及时停。

权衡:Nacos 轻量一体(发现+配置);Apollo 配置治理(灰度/回滚/审计)更专业;小团队 Nacos 够,强治理选 Apollo。

【关键技术点】
长轮询/长连接推送灰度发布配置版本回滚本地快照兜底@RefreshScope 陷阱增量推送Nacos/Apollo变更审计
【Java 实现示例】

① Nacos 监听配置变更:

configService.addListener(dataId, group, new Listener() {
    public void receiveConfigInfo(String cfg) {
        LimitConfig c = parse(cfg);
        if (c.threshold() <= 0) { log.error("阈值非法,拒绝"); return; } // 防误改
        limiter.update(c);                     // 热更新限流阈值
    }
});

② 安全热更(只用可变开关,不动连接池):

@RefreshScope // 仅用于无副作用的纯配置 Bean
@Configuration
public class SwitchConfig { @Value("${feature.x:false}") boolean x; }
【面试官可能继续追问】
【常见错误回答】
  • ❌ 把数据库密码/连接池也做成热更新——重建 Bean 连接泄漏。
  • ❌ 配置全量直发无灰度——一次误改全站故障。
  • ❌ 配置中心无本地兜底——中心挂服务起不来。
【架构师评分标准】
初级 0~40
配置写死在文件靠发版。
中级 40~60
会用配置中心动态读。
高级 60~80
讲清推送机制 + 灰度 + 回滚 + 本地兜底。
架构师 80~100
@RefreshScope 陷阱 + 误改防护 + 治理规范 + 选型权衡。

第 26 题:单元化架构与跨机房数据同步 单元化

【真实企业业务场景】
全国性电商平台:注册 5 亿、日单 3000 万。三机房(华东/华北/华南)各自承载本地用户。要求:①单机房故障,其余机房照常服务(故障隔离);②用户就近访问(RT < 30ms);③跨单元请求(异地下单、跨区退货)最终一致;④异地多活,RTO < 5min、RPO < 1s。若所有机房连同一套中心 DB,跨地域 100ms+ 延迟会让写操作不可用。
【面试官问题】
    1. 什么是单元化(SET 化)?2. 用户怎么路由到归属单元?3. 单元内如何闭环?4. 跨单元写怎么保证最终一致?5. 单元故障如何切流?6. 脑裂怎么防?7. 和"同城双活/异地多活"区别?
【候选人的标准回答】

第一步:分析问题。跨地域部署的核心矛盾是延迟与一致性:强一致写必须靠近数据,所以把"数据 + 计算"按用户归属切成一个个自包含单元,让绝大多数请求在单元内闭环。

第二步:核心挑战。路由正确性(用户归属稳定)、跨单元写、数据同步延迟与冲突、单元故障流量切换、脑裂。

第三步:整体架构。单元化(SET):每个单元 = 接入层 + 应用 + 数据库,自包含处理本单元用户;全局数据(用户中心/配置)多单元同步;跨单元请求经"跨单元同步通道"(Canal/DRC)最终一致。

第四步:技术选型。单元路由(网关按 uid 归属地/哈希);数据同步用阿里 DRC / Canal / TiDB Placement Driver;全局配置走配置中心多单元推送。

第五步:一致性。单元内主从强一致;跨单元 CDC 异步同步 + 冲突裁决(业务维度,如 last-write-win 或人工);核心资金仍走总部单元。

第六步:高可用。单元故障 → DNS/网关把该单元流量切到其他单元(需数据已同步);多活避免单点。

第七步:性能优化。路由本地化让 95%+ 请求单元内闭环;跨单元调用批量/异步;同步通道压缩。

【架构设计】
用户 → DNS/网关(按 uid 归属地路由) ▼ 单元A(华东) 单元B(华北) 单元C(华南) 接入+应用+DB(自包含) 接入+应用+DB 接入+应用+DB ▲ 单元内闭环 95% 请求 │ 跨单元同步通道(Canal/DRC,异步) └ 用户跨区下单/异地退货 → 目标单元处理后回写源单元 全局数据(用户中心/配置):多单元同步,冲突裁决

瓶颈:跨单元写延迟、同步延迟导致短暂不一致、切流时数据完整性。

【技术方案深度解析】

为什么不直接所有机房连同一 DB?跨地域 RTT 50~100ms,一次写涉及多次交互,事务持有锁过久,吞吐崩塌且任一链路抖全都卡。

单元化 vs 同城双活 vs 异地多活:同城双活(同城市两机房,低延迟可强一致);单元化(按用户分片,各单元自治,跨单元最终一致);异地多活(多地域均可写,复杂度最高)。单元化是异地多活的主流落地形态。

脑裂防护:切流时旧单元可能还在收写,用租约/ fencing token 让旧单元写失效,避免双写。路由层用全局一致的用户归属表。

权衡:单元化牺牲"全局强一致跨单元"换"局部高可用与低延迟",适合用户态、地域亲和业务;不适合强全局一致(如全局库存)的场景。

【关键技术点】
SET 单元化单元路由单元封闭DRC/Canal 同步异地多活冲突裁决fencing 防脑裂流量切换
【Java 实现示例】

① 单元路由(按 uid 归属):

public String routeUnit(Long uid) {
    // 归属地稳定映射;变更走配置中心灰度
    return unitMap.getOrDefault(uid % 3, "east");
}
// 跨单元写:投到目标单元 MQ,目标单元处理后 CDC 回写
crossMq.send(targetUnit, buildCrossEvent(uid, order));

② 切流防护(fencing):

// 单元切换时旧单元拿不到租约,写被拒绝
if (!leaseMgr.hold(unit)) throw new UnitFencedException();
【面试官可能继续追问】
【常见错误回答】
  • ❌ 所有机房共享中心 DB——跨地域延迟拖垮写。
  • ❌ 单元间频繁同步强一致——退化成中心化,失去意义。
  • ❌ 切流不做 fencing——脑裂双写数据冲突。
【架构师评分标准】
初级 0~40
只说"多机房部署"。
中级 40~60
知道按地域分库。
高级 60~80
单元化 + 路由 + 跨单元最终一致。
架构师 80~100
单元封闭 + 异地多活 + 切流/fencing + 与双活对比权衡。

第 27 题:集群级限流设计 集群限流

【真实企业业务场景】
API 网关 50 节点,需对"下单接口"做全局总 QPS 10 万限流,同时对单用户 > 100/s、单 IP > 200/s 限流。若每节点各自限流(每节点 2000/s),50 节点实际放行 10 万但流量不均时会局部超限;某用户集中打 1 个节点会击穿。需全局精确限流且不成为瓶颈。
【面试官问题】
    1. 单机限流为什么不够?2. 集群限流怎么计数?3. Redis 方案性能与精度?4. Sentinel 集群流控原理?5. 限流误判/组件故障怎么办?6. 滑动窗口 vs 令牌桶?7. 热点参数限流?
【候选人的标准回答】

第一步:分析问题。限流有三维度:接口级(保护系统)、用户级(防刷)、IP 级(防攻击)。单机限流无法约束"集群总和"与"跨节点单用户",需集中计数。

第二步:核心挑战。集群计数一致性、限流本身性能/RTT、故障 fail-open/close、精度与开销权衡。

第三步:整体架构。多层:WAF(粗) → 网关集群流控(全局 Token/窗口) → 服务单机(Sentinel 兜底)。集群计数用 Redis(Lua 原子)或 Sentinel Token Server。

第四步:技术选型。Guava RateLimiter(单机);Sentinel(单机+集群流控,Token Server 模式);Redis + Lua 令牌桶/滑动窗口;Nginx limit_req

第五步:一致性。Redis INCR/Lua 原子自增 + TTL 实现窗口计数;Token Server 集中发放令牌保证全局精确。

第六步:高可用。限流组件故障默认fail-open(放行)避免误杀;Redis 集群;本地单机限流兜底。

第七步:性能优化。本地预取令牌(一次取一批)、滑动窗口近似计数、批量上报减 RTT。

【架构设计】
请求 → WAF(IP 粗限) ▼ API Gateway 集群(50 节点) ├ 集群流控:每节点向 Redis/LUA 原子窗口计数(全局 QPS 总闸) ├ 用户维度:Redis 用户计数(>100/s 拒绝) └ 本地兜底:Sentinel 单机(Redis 不可用时仍有限流) ▼ 放行 下游服务(资源隔离 + 自身 Sentinel 规则)

瓶颈:Redis 计数 RTT、热点 key(单用户计数)、Token Server 单点。

【技术方案深度解析】

为什么单机限流不够?集群总闸需全局视图;单用户可能全打一个节点,单机看不到全局,击穿。

Redis 滑动窗口(Lua):ZSET 存请求时间戳,统计窗口内数量,超过阈值拒绝;Lua 保证"计数+判断"原子,避免并发超放。

Sentinel 集群流控:独立 Token Server 统一发令牌,各节点向 Server 取令牌,精度高但 Server 需高可用(可嵌入模式降级为单机)。

fail-open vs fail-close:限流组件挂,fail-open 放行(宁可风险不误杀),fail-close 拒绝(保系统但可能误伤正常流量)。一般限流选 fail-open,熔断选 fail-close。

权衡:Redis 方案简单但有 RTT(≈1ms/请求);Token Server 精确但有单点;生产常"Redis 集群流控 + 本地单机兜底"。

【关键技术点】
集群流控Redis Lua 滑动窗口Sentinel Token Server令牌桶fail-open/close用户/IP 维度本地预取热点参数限流
【Java 实现示例】

① Redis 滑动窗口限流(Lua):

-- KEYS[1]=limit:api  ARGV[1]=now(ms) ARGV[2]=window ARGV[3]=max
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]-ARGV[2])
local n = redis.call('ZCARD', KEYS[1])
if n >= tonumber(ARGV[3]) then return 0 end   -- 超阈拒绝
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1]); redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1

② Sentinel 规则:

FlowRule r = new FlowRule("createOrder").setCount(100000).setGrade(RuleConstant.FLOW_GRADE_QPS);
r.setClusterMode(true);  // 集群流控,Token Server 统一发放
【面试官可能继续追问】
【常见错误回答】
  • ❌ 只做单机限流——集群总闸失效、单用户击穿。
  • ❌ 限流组件故障 fail-close——Redis 抖动全站 503。
  • ❌ 滑动窗口不用 Lua——并发超放。
【架构师评分标准】
初级 0~40
只知 Guava 单机限流。
中级 40~60
会用 Redis 计数限流。
高级 60~80
集群流控 + 多维 + fail-open 兜底。
架构师 80~100
多层限流 + 性能优化 + 与熔断区分 + Sentinel 集群模式。

第 28 题:全链路追踪与可观测性体系 可观测性

【真实企业业务场景】
后端 30+ 微服务,一次"下单"跨 12 个服务(网关→订单→库存→优惠→支付→物流…)。P99 通常 800ms,但每天偶发 5s+。运维无法定位"慢在哪一段"。要求:①一次请求全链路可见;②慢调用/错误快速定位;③监控告警;④接入开销 < 5%。
【面试官问题】
    1. 可观测性三大支柱?2. TraceID 怎么跨服务透传?3. 采样率怎么定?4. Metrics/Logs/Tracing 怎么关联?5. MQ 异步链路怎么追踪?6. 探针性能开销?7. SkyWalking vs Jaeger 区别?
【候选人的标准回答】

第一步:分析问题。微服务下"一次请求 = 多次跨进程调用",靠日志 grep 已不可能。需要把指标(Metrics)、日志(Logs)、链路(Tracing)打通。

第二步:核心挑战。TraceID 透传(含 MQ/线程池)、数据量爆炸、低开销、关联查询。

第三步:整体架构。OpenTelemetry 统一埋点 → W3C TraceID/SpanID 透传 → Collector → Tracing(SkyWalking/Jaeger) + Metrics(Prometheus+Grafana) + Logs(ELK),三者用 TraceID 关联。

第四步:技术选型。OpenTelemetry(标准 SDK);SkyWalking(Java 字节码探针,零侵入);Jaeger(Tracing);Prometheus(指标);ELK/Loki(日志)。

第五步:一致性。同一请求 TraceID 贯穿所有服务(HTTP header traceparent、MQ message property);日志打 TraceID 便于关联。

第六步:高可用。Collector 集群、采样降量、异步上报不阻塞业务;存储按保留期分层。

第七步:性能优化。采样(常规 1%~10%,异常/慢链路 100%);探针开销 < 5%;关键路径全采。

【架构设计】
服务(OTel SDK / SkyWalking Agent 自动埋点) ├ 生成 TraceID + SpanID,注入 context ├ HTTP: traceparent header 透传 └ MQ: message property 透传(跨线程/异步) ▼ 异步上报(不阻塞) Collector(OTel Collector 集群) ├ → Tracing 存储(Jaeger/SkyWalking) ├ → Metrics(Prometheus → Grafana) └ → Logs(含 trace_id → ELK) ▼ 关联 Grafana:RED 指标 + 点击 TraceID 跳链路详情

瓶颈:数据量、存储成本、透传遗漏(线程池/MQ 丢 context)、采样导致漏抓偶发慢链路。

【技术方案深度解析】

为什么用 OpenTelemetry?厂商中立标准,一套埋点可后端接 Jaeger/SkyWalking/Zipkin,避免绑定。

透传是灵魂:HTTP 用 W3C traceparent;但线程池/ MQ/异步会丢 context(新线程无旧 ThreadLocal),必须显式传递(TaskDecorator / MQ 生产者塞 property / 消费者取出恢复)。漏传=链路断裂。

采样策略:全量采数据量太大(30 服务 × 10 万 QPS);常规 1%~10% 采样;但对错误/慢(>1s)/关键业务 100% 采样,既控成本又保排查。

权衡:SkyWalking 零侵入(字节码增强)但绑定生态;Jaeger 轻量标准但需手动埋点;Prometheus 拉模型适合指标,不适合链路。

【关键技术点】
OpenTelemetryW3C traceparentTraceID 透传采样策略SkyWalking/JaegerPrometheus+GrafanaRED 指标ELK 关联
【Java 实现示例】

① 线程池透传 TraceID(Spring TaskDecorator):

public class TracingDecorator implements TaskDecorator {
    public Runnable decorate(Runnable r) {
        var ctx = Context.current();   // 捕获当前 trace context
        return () -> ctx.makeCurrent().close(); try { r.run(); } finally {};
    }
}
// @Async 线程池 setTaskDecorator(new TracingDecorator()) 防链路断裂

② MQ 透传(生产者塞 property,消费者恢复):

producer.send(msg.setProperty("trace_id", Tracing.currentTraceId()));
// 消费端:Tracing.startWith(msg.getProperty("trace_id")) 恢复链路
【面试官可能继续追问】
【常见错误回答】
  • ❌ 只靠日志 grep——微服务跨 12 服务无法串联。
  • ❌ 全量采样——存储成本爆炸。
  • ❌ 线程池/MQ 不传 context——链路断裂定位失效。
【架构师评分标准】
初级 0~40
只知加日志。
中级 40~60
会用 SkyWalking 看链路。
高级 60~80
三大支柱 + TraceID 透传 + 采样。
架构师 80~100
OTel 标准化 + 透传全覆盖 + 采样策略 + 性能/成本权衡。

第 29 题:对账系统设计(实时 + 离线) 对账

【真实企业业务场景】
支付平台日交易 2000 万笔,涉及三方:银行/微信/支付宝(渠道)、平台内部账户、订单系统。现实问题:①渠道回调丢失导致"掉单"(用户付了平台没记);②重复回调导致重复入账;③金额/状态不一致。要求:T+1 离线对账差错率归零,实时对账 5 分钟内发现掉单。
【面试官问题】
    1. 为什么需要对账?2. 三方对账怎么做?3. 长款/短款/错账是什么?4. 实时对账怎么实现?5. 掉单怎么补救?6. 差错处理流程?7. 数据量大怎么加速?
【候选人的标准回答】

第一步:分析问题。分布式系统中"本地成功 ≠ 对方成功",靠对账兜底发现并纠正不一致。本质是对多源数据的集合差集与字段比对

第二步:核心挑战。数据量(千万级)、格式不一、延迟、差错裁决、补账幂等。

第三步:整体架构。离线对账(T+1):渠道流水文件/内部流水按"业务流水号"join 比对;实时对账:Canal 捕获平台库 binlog vs 渠道回调,延迟窗口内比对。差错入差错表,按类型处理(补账/冲正/挂账)。

第四步:技术选型。离线:Spark/Flink/DataX 批处理;实时:Canal + Flink CEP;存储:MySQL/ES(差错检索);比对键:渠道流水号/平台订单号。

第五步:一致性。以"权威方"(通常银行流水为准)比对;补账走幂等(唯一流水号),避免重复补。

第六步:高可用。对账任务幂等可重跑;差错告警;人工复核通道。

第七步:性能优化。按天/按渠道分片并行;建索引;增量对账(只比当日)。

【架构设计】
渠道(银行/微信) 平台内部订单/账户 └ 流水文件(T+1) └ binlog(Canal) + 业务流水 ▼ 离线 join ▼ 实时比对(Flink CEP) 对账引擎(按 流水号/金额/状态 比对) ├ 一致:忽略 ├ 平台有渠道无(长款)→ 可能重复/未回调,查实 ├ 渠道有平台无(短款/掉单)→ 补账(幂等) └ 金额/状态不符(错账)→ 挂账 + 人工 ▼ 差错表(告警 + 运营处理 + 补账流水)

瓶颈:千万级 join 性能、时区/金额精度、渠道文件延迟、补账幂等。

【技术方案深度解析】

三方对账:平台流水、渠道流水、内部账户三方两两比对,以"渠道为准"校验平台是否漏记/多记。掉单(渠道成功平台无)= 最危险,需实时发现补账。

长款/短款:长款=平台记录多于渠道(可能重复入账);短款=渠道成功平台未记(掉单,资损风险)。错账=都存在但金额/状态不符。

实时 vs 离线:离线 T+1 全面但慢;实时用 Canal 捕获平台库变更 + 渠道回调,在延迟窗口(如 5min)内比对,发现掉单立即告警补账,缩小资损窗口。

补账幂等:补账按"原渠道流水号"唯一,重复补账 INSERT IGNORE,绝不重复入账。

权衡:离线准确但滞后;实时快但需处理延迟/乱序;生产两者结合,实时缩窗口、离线终态兜底。

【关键技术点】
三方对账长款/短款/错账掉单补救Canal 实时比对Flink CEP补账幂等差错挂账分片并行
【Java 实现示例】

① 离线比对(按流水号 join):

// 平台流水 Map<bizNo, Order> vs 渠道流水 Map<bizNo, ChannelTx>
channelMap.forEach((bizNo, ch) -> {
    Order o = platformMap.get(bizNo);
    if (o == null) diffDao.insert(new Diff(bizNo, SHORT, "掉单")); // 渠道有平台无
    else if (!o.amount().equals(ch.amount())) diffDao.insert(new Diff(bizNo, MISMATCH, "金额不符"));
});

② 补账(幂等):

@Transactional
public void repairDrop(String bizNo, BigDecimal amt) {
    if (repairLog.insertIgnore(bizNo) == 0) return; // 已补过
    accountSvc.credit(bizNo, amt);                    // 补入账
}
【面试官可能继续追问】
【常见错误回答】
  • ❌ 只靠业务断言"调用成功就一致"——回调丢失造成掉单资损。
  • ❌ 补账不幂等——重复补账二次资损。
  • ❌ 只有离线对账——掉单 T+1 才发现,资损窗口长。
【架构师评分标准】
初级 0~40
不知道要对账。
中级 40~60
会做 T+1 文件对账。
高级 60~80
三方对账 + 长/短/错账 + 实时 Canal。
架构师 80~100
实时+离线双轨 + 掉单补救 + 补账幂等 + 性能优化。

第 30 题:分布式文件与对象存储架构 对象存储

【真实企业业务场景】
UGC 平台:用户上传头像/视频/商品图/订单附件,日增 10TB,总量 PB 级。要求:①高可用(11 个 9 持久);②上传不占业务带宽(直传);③大文件断点续传;④相同文件秒传;⑤CDN 加速读;⑥冷数据降本(标准存储 vs 归档差 10 倍);⑦私有文件防盗链。
【面试官问题】
    1. 对象存储 vs 文件/HDFS?2. 为什么直传而非过业务服务器?3. 分片上传与断点续传?4. 秒传怎么实现?5. 权限与防盗链?6. 冷热如何分层降本?7. 多副本 vs 纠删码?
【候选人的标准回答】

第一步:分析问题。非结构化海量文件的核心是:写入降本(直传/分片)、读取提速(CDN)、存储降本(分层/去重)、安全(权限)。对象存储(KV 语义)比文件系统/块存储更适合。

第二步:核心挑战。大文件上传稳定性、去重、权限泄露、成本、跨地域。

第三步:整体架构。客户端 → 向业务申请签名 URL直传对象存储(OSS/COS/S3)→ CDN 回源加速读 → 生命周期策略自动转低频/归档。秒传靠内容哈希去重。

第四步:技术选型。托管对象存储(阿里 OSS/腾讯 COS/AWS S3);自建 MinIO/Ceph;CDN;计算哈希用 MD5/SHA256;纠删码降存储成本。

第五步:一致性。多副本/EC 保证持久;读最终一致(版本化);元数据(文件索引)存 DB。

第六步:高可用。多 AZ 冗余、跨区域复制、CDN 边缘缓存。

第七步:性能优化。分片并发上传、CDN 缓存热点、冷热分层、去重省空间。

【架构设计】
客户端 ├ 申请上传:业务签发 临时签名URL(限定 bucket/key/过期) ├ 直传 OSS(分片并发 + 断点续传 + 秒传:先查 hash 已存在则直返) ▼ 对象存储(多 AZ + 纠删码,11个9 持久) ├ 生命周期:标准 → 30天 → 低频 → 归档 └ 跨区域复制(容灾) ▼ 读 CDN(边缘缓存热点文件,回源 OSS) └ 私有文件:签名 URL + Referer 防盗链

瓶颈:大文件上传超时、CDN 回源带宽、冷数据取回延迟、权限泄露。

【技术方案深度解析】

为什么直传而非过服务器?文件过业务服务器占用带宽/连接池、成瓶颈;签名 URL 让客户端直连 OSS,业务只签发临时凭证,安全且省资源。

分片上传:大文件切成 5~10MB 片并发上传,每片独立重试;记录已传分片实现断点续传;全部完成合并。

秒传:上传前算内容哈希,查存储已有则直接返回已有 key(不重复传),省带宽。注意哈希碰撞——关键文件用 SHA256。

冷热分层:标准存储贵但立取;归档便宜 10 倍但取回需分钟级。按访问频率生命周期自动沉降,成本降数倍。

权衡:托管 OSS 省运维但绑定+流量费;自建 MinIO/Ceph 灵活但运维重;多副本简单可靠但空间 3 倍,EC 省空间但重建慢。

【关键技术点】
对象存储 OSS/S3签名 URL 直传分片上传断点续传秒传(哈希去重)CDN 回源冷热分层纠删码 EC
【Java 实现示例】

① 签发上传签名 URL(OSS 风格):

// 业务侧:临时、限定 key 与过期,客户端拿此 URL 直传,不过业务服务器
GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(bucket, key);
req.setExpiration(Date.from(Instant.now().plusSeconds(300)));
URL url = ossClient.generatePresignedUrl(req);  // 返回给客户端直传

② 秒传(哈希查重):

public String upload(MultipartFile f) {
    String hash = Sha256.of(f);                  // 内容哈希
    return fileMetaDao.findByHash(hash)        // 已存在 → 秒传直返 key
        .map(FileMeta::key).orElseGet(() -> doStore(f, hash));
}
【面试官可能继续追问】
【常见错误回答】
  • ❌ 文件过业务服务器再转存——带宽/连接池被打满。
  • ❌ 永久公开 URL——盗链与泄露。
  • ❌ 所有文件标准存储——冷数据成本高 10 倍。
【架构师评分标准】
初级 0~40
文件存本地磁盘/数据库。
中级 40~60
会用 OSS 存文件。
高级 60~80
直传+分片+CDN+冷热分层。
架构师 80~100
签名 URL 安全 + 秒传去重 + EC 降本 + 容灾 + 选型权衡。
WorkBuddy · Java 系统架构师面试题 · 第二部分分布式系统设计(第 16~30 题)已完整生成