109. 服务 A 改了数据,B/C/D 都要跟着变(微服务数据一致性)
【考察内容】微服务数据一致性是架构岗必考
【题目】微服务 A 修改了核心数据(如用户信息),下游 B(订单)、C(消息)、D(统计)都要基于新数据。A 不能直接改 B/C/D 的库,怎么保证数据最终一致?请给出完整方案(事件驱动、本地消息表、事务消息、订阅消费)与失败兜底。
【参考答案】
- 核心思想:事件驱动 + 最终一致,避免跨服务强事务;
- 方案组合:
- 可靠事件(本地消息表/事务消息):A 的业务与事件同事务落库,事件异步投递到 MQ,B/C 订阅消费——保证“事件不丢”;
- 消费幂等:B/C 用业务唯一键去重,保证重复事件只生效一次;
- Outbox 模式:业务表旁建 outbox 表,同事务写入,relay(如 Debezium 监听 binlog)把 outbox 记录转发 MQ——比本地消息表更解耦;
- 对账兜底:定时任务比对 A 与 B/C 的数据,发现不一致自动补偿(重发事件/直接修复);
- 状态机推进:B/C 各自维护状态(如“待同步→已同步”),对账按状态处理;
- 什么时候不能最终一致:资金强一致(扣款+入账)用 TCC/Seata;大部分业务最终一致足够;
- 设计原则:明确每个数据的所有者(单一写入方),其他服务只读或事件同步。
- 容量/延迟估算:A 服务写峰值 2000 TPS,若每条变更需通知 3 个下游,则事件峰值 6000 msg/s;B/C/D 消费能力必须覆盖。不一致窗口:MQ 正常秒级;本地消息表秒~十秒;下游批量同步可到分钟级。对账任务按数据量:日变更要先从峰值折回均值(取峰值的 1/10 ≈ 200 TPS)再乘全天,200×86400 ≈ 1700 万事件/日;若按峰值 2000 TPS 直乘会得出 1.7 亿,那是几乎不可能的上限口径,别拿它做容量。对账不能每晚全量扫,应增量+抽样+关键账户全量。缓存失效风暴:热点 key 更新时多个下游同时失效,可用延迟双删/版本号。
- 失败与降级:消息丢失→本地消息表/事务消息;下游消费失败→重试+死信+补偿;缓存与 DB 不一致→延迟双删/订阅 binlog;对账发现差异→差错工单自动补偿。非核心下游可降级关闭;核心扣减不可最终一致了事,要强一致或预留。
【原理溯源】
- 为什么不让 A 直接改 B/C/D 的库? 破坏服务边界与自治:耦合表结构、部署与故障相互拖累,且形成事实上的分布式事务。单一数据所有者才能让演进与故障隔离。
- 为什么“先写库再发 MQ”不够? 写库成功、发消息前宕机,消息丢失。本地消息表/Outbox 把“业务变更”和“待发事件”放进同一本地事务,消除丢消息窗口。
- Outbox 与本地消息表差别? 思路相同;Outbox 常配合 CDC(Debezium 读 binlog)转发,应用不必写投递任务,业务与发布管道解耦。
- 为什么消费必须幂等? MQ 至少一次投递 + 网络重试 + 生产者重发,重复是常态。没有幂等,重复事件会把下游状态打乱。
- 为什么还要对账? 消息可能进死信、消费者可能 bug、人工可能改数。对账是最终一致的最后收敛器,按所有者数据修复下游。
【选型判断树】
数据被多个服务使用?
├─ 是否资金/强约束?
│ ├─ 是 → TCC / Seata 强一致链路
│ └─ 否 → 事件最终一致
└─ 最终一致选型:
├─ 应用要简单、已有 MQ → 本地消息表 / 事务消息
├─ 要与业务代码解耦、有 CDC → Outbox + Debezium
└─ 必配:消费幂等 + 对账 + 状态机【口述骨架】(5 分钟;框架 A,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 relay | Debezium/Canal | CDC 转发 |
| 幂等键 | 业务单号/版本号 | 不要用随机 UUID 作业务幂等 |
| 事件峰值 | 写TPS×下游数 | 2000×3=6000 msg/s |
| 不一致窗口 | 秒~分钟级 | 按业务 SLA |
| 对账策略 | 增量+抽样+关键全量 | 避免全表扫 |
【追问链】(三层)
L1|“同步 RPC 调下游改数据不行吗?” → 行但差:形成调用耦合与分布式事务,下游慢/挂会拖垮 A,扩下游要改 A。事件异步化更稳。
L2|“消费失败进死信后业务就一直不一致?” → 不会「一直」,但前提是死信不是终点。第 6 条的失败处置里(第 2 条的方案组合只写到对账兜底),死信之后还有两件事要做:① 有限重试后进 DLQ,而 DLQ 必须可人工或自动重投(关键数字写死了这条),重投时带上原业务单号或版本号,配合消费幂等保证只生效一次;② 对账作为最后收敛器——原理溯源说得很清楚,消息可能进死信、消费者可能有 bug、人工可能改数,对账按数据所有者 A 的数据修复下游,并落差错工单(第 6 条)。下游还要用状态机(待同步→已同步)表达中间态,补投才敢做。窗口按第 5 条是秒到分钟级:死信一旦持续积压并越过这个窗口就该告警,否则「最终一致」会悄悄变成「永远不一致」。
L3|“以谁的数据为准修不一致?” → 以数据所有者(A)为准。下游用事件重放或拉取全量修复,并记录审计。禁止下游反向覆盖所有者。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道发 MQ 异步通知下游 |
| 80 分 | 能讲本地消息表/Outbox+幂等 |
| 95 分 | 强调单一数据所有者;对账与状态机闭环;指出资金场景不能最终一致 |
【关联题】
【自测】
- 为什么 A 不能直接改 B 的数据库? 参考答案:破坏服务自治,引入耦合与分布式事务,故障相互拖累。
- Outbox 模式如何保证事件不丢? 参考答案:业务变更与 outbox 记录同一本地事务提交,再由 relay/CDC 转发 MQ。
- 下游消费乱序/重复怎么办? 参考答案:幂等键去重;必要时按版本号/状态机拒绝旧事件;对账兜底修复。
110. 扣款接口被重复调用就重复扣钱(接口幂等)
【考察内容】接口幂等是后端最高频场景题之一
【题目】扣款接口因为网络重试、用户双击、消息重投被调用了两次,用户被扣了两笔钱。接口幂等怎么设计?幂等 key 怎么生成与存储?数据库唯一约束、Redis 标记、状态机三种方案各适用什么场景?
【参考答案】
- 幂等定义:同一操作执行多次与执行一次效果相同;
- 实现层次:
- 前端/网关:按钮防重复提交、Token 机制——业务前先申请唯一 token,请求携带,服务端校验并删除(一次性消费必须原子:Redis≥6.2 用
GETDEL,更早版本用 Lua 的 EXISTS+DEL;SETNX 只能表达“首次写入成功”的占位语义); - 应用层:幂等键(Idempotency-Key)——客户端传业务唯一键(订单号/支付流水号),服务端以“唯一键+处理结果”存 Redis,重复请求直接返回上次结果;
- 数据库层(最终防线):业务唯一键唯一索引,重复插入报错捕获;状态机幂等——处理前检查状态;乐观锁 version 字段;
- 消息消费幂等:唯一键+去重表/Redis SETNX;
- 前端/网关:按钮防重复提交、Token 机制——业务前先申请唯一 token,请求携带,服务端校验并删除(一次性消费必须原子:Redis≥6.2 用
- 关键设计:
- 幂等键要选业务唯一维度(订单号、支付单号),不能每次请求随机生成;
- 查询接口天然幂等;扣款/发券/下单必须幂等;
- 返回语义:重复请求返回与首次一致的结果(查已有结果而非报错);
- 验证:用相同请求重复调用测试。
- 容量估算:幂等层要按峰值接口 QPS 设计,而不是平均。扣款峰值 3000 QPS 时:唯一键写 DB 需要该表扛 3000 插入/s(应分表或热点优化);Redis SETNX 预检可挡住重复,但最终仍要 DB 唯一约束。幂等记录保留时间 ≥ 业务重试窗口(支付常见 24h~7 天)。存储:3000/s×86400×7 天若全存会巨大,需按天分区/定期清理成功记录。条件更新
UPDATE ... WHERE status=待支付的行锁热点要评估。 - 失败与降级:Redis 幂等层挂→降级只靠 DB 唯一索引(限流保护);重复请求视为成功返回(查询真实状态)而不是报错,避免客户端无限重试;分布式锁过期导致并发重复→仍靠 DB 约束兜底。监控:幂等冲突次数(异常高可能是攻击或客户端 bug)、扣款重复拦截数。
【原理溯源】
为什么会重复? 网络超时重试、用户双击、MQ 至少一次投递、网关重发——分布式里“调用一次”不保证“到达一次”。幂等是对不可避免的重复的防御。
为什么前端防重不够? 可被脚本绕过、多端不同步、网关/客户端仍会重试。前端只能降低概率,正确性必须在服务端。
为什么幂等键不能随机生成? 随机键每次请求都不同,服务端无法识别“这是同一次业务操作”。必须用业务唯一标识才能把重复请求映射到同一操作。
为什么重复请求应返回首次结果而不是报错? 客户端超时重试时,往往不知道首次是否成功。返回首次结果符合“效果与一次相同”的幂等语义,也避免把成功误判为失败。
为什么 DB 唯一索引是最终防线? Redis 可能丢标记、应用可能并发双写。唯一索引在存储引擎层保证“同一业务键只插一次”,冲突即失败,不会静默双写。
【选型判断树】
接口是否会产生副作用(写/扣减/发券)?
├─ 否(查询)→ 天然幂等
└─ 是
├─ 请求前可获取一次性凭证 → Token 机制(防进入)
├─ 有明确业务唯一键 → 幂等键 + 结果缓存(Redis)
├─ 落库可建唯一约束 → DB 唯一索引(最终防线)
└─ 有明确状态流转 → 状态机 + 乐观锁
生产建议:Token/幂等键 + DB 唯一索引 多层叠加【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 + 过期;一次性消费(Token)要 GETDEL(≥6.2)或 Lua EXISTS+DEL | SETNX 只表达「首次写入成功」,不保证取走即失效;另需注意与 DB 对齐 |
| 消息重复 | 至少一次语义下常态 | 必须消费幂等 |
| 重试间隔 | 指数退避 | 避免重试风暴 |
| 唯一索引 | (user_id, biz_no) 类复合键 | 按业务设计 |
| 幂等层容量 | = 峰值接口 QPS | 3000 QPS 峰值 |
| 记录保留 | ≥ 重试窗口 | 支付 24h–7d |
| 最后防线 | DB 唯一键/条件更新 | 缓存过期也能防重 |
【追问链】(三层)
L1|“只有 Redis 幂等标记够吗?” → 不够最终。Redis 可能丢数据或标记过期。关键扣减必须有 DB 唯一索引/状态机兜底。
L2|“幂等键存什么?” → 三选一或组合:① DB 唯一索引(最可靠,注意热点);② 状态机条件更新(仅允许合法迁移一次);③ Redis SETNX+TTL(快,但是缓存,过期后失效,必须 DB 兜底)。扣款场景推荐:请求唯一键+DB 唯一流水+账户余额条件更新,三重防护。返回策略:重复请求返回首次结果(或查询态),保证幂等语义。
L3|“并发两个相同请求同时到达怎么办?” → SETNX/唯一索引做互斥:一个成功占位,另一个失败或等待后读结果。幂等也要考虑并发竞态,不只是串行重试。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 知道要做防重复,会提前端防抖 |
| 80 分 | 讲清 Token + 幂等键 + 唯一索引 |
| 95 分 | 强调业务唯一键与返回首次结果;并发竞态;Redis 过期与 DB 兜底关系 |
【关联题】
【自测】
- 幂等键为什么不能用每次随机生成的 UUID? 参考答案:无法标识同一次业务操作,重复请求会被当成新请求。应使用订单号等业务唯一键。
- 重复扣款请求应报错还是返回首次结果? 参考答案:返回与首次一致的结果,符合幂等语义,避免客户端把成功当失败。
- 幂等的最终防线通常是什么? 参考答案:数据库唯一索引或状态机/乐观锁,在存储层挡住重复写入。
111. 用户登录后换台机器就掉登录(分布式 Session)
【考察内容】分布式会话设计
【题目】服务多实例部署后出现怪现象:用户这次请求被路由到 A 机器登录了,下次请求被路由到 B 机器,B 不认识他,又要重新登录。怎么解决登录态共享?Session 集中存储、JWT、Cookie 方案各自怎么做、怎么选?
【参考答案】
- 方案一:Session 集中存储——Session 放 Redis(spring-session),所有实例共享;登出/踢人可主动失效;适合传统 Web;
- 方案二:无状态 Token(JWT)——用户信息+签名放进 Token,服务端不存 Session,任意实例验签即可;适合移动端/开放 API/跨域;注意:JWT 无法主动失效(需黑名单/短有效期+refresh);
- 方案三:Cookie 加密(老式)——会话数据加密后放客户端;安全风险高,不推荐;
- 选型建议:PC 端管理系统(需要会话管理/强制下线)→Redis Session;开放平台/移动端→JWT;两者可结合(JWT 认证 + Redis 存可撤销状态);
- 安全要点:Token 存 HttpOnly Cookie 防 XSS、HTTPS、过期时间合理、防 CSRF(SameSite/CSRF Token)。
- 容量估算:日活 100 万、平均会话 2h——常驻 key 数取决于 TTL,不能直接拿日活当 key 数:TTL≥24h 或「记住我」才接近 100 万级 key(×2KB≈2GB);若按会话 2h 过期,常驻只有 100万×2/24≈8.3 万 key、约 166MB。下面按 100 万级取保守上限,Redis 单实例可容。QPS:鉴权校验几乎每个请求都要读一次——按日活 100 万×人均 100 次需鉴权的请求/日=1e8 次/日,摊算约 1160/s,集中时段按 8~14 倍 → 峰值 约 1~1.6 万 QPS——Session 读要走 Redis,可再加本地缓存(短 TTL)——但这一条会直接打掉第 1 条卖点「登出/踢人可主动失效」:TTL>0 就存在窗口,被踢/被封用户在窗口内仍被本地缓存判为有效。两种写法二选一:① 本地只缓存「JWT 验签结果+会话版本号」,踢人时版本号自增,本地比对不一致立即失效(可撤销态不落本地);② 对安全等级高的会话明确声明「可撤销态不走本地缓存」,只接受 Redis 的读放大与扩容成本。过期清理:惰性+定期。JWT 无服务端存储则 Redis 压力小,但吊销难,需要黑名单(登出/封禁时写 blacklist,其容量按需吊销比例估算)。
- 失败与降级: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 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “多实例掉登录本质是会话未共享” |
| 0:30–1:30 | Redis Session | 集中存储、可踢人 |
| 1:30–2:30 | JWT | 无状态验签、难撤销 |
| 2:30–3:30 | 选型 | 管理端 vs 开放端 |
| 3:30–4:30 | 安全 | HttpOnly、HTTPS、CSRF、过期 |
| 4:30–5:00 | 收尾 | “可撤销性与无状态性要做取舍或混合” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| Session 默认过期 | 30 分钟闲置~数小时 | 按安全策略 |
| JWT Access Token | 5 分钟~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 难撤销及混合方案;安全属性;指出粘性会话局限 |
【关联题】
【自测】
- 多实例掉登录的根因是什么? 参考答案:Session 存在单机内存,请求路由到其他实例后读不到。
- JWT 最大的工程缺点是什么? 参考答案:默认无法主动失效/踢人,需黑名单或短有效期+refresh。
- 管理后台强制下线你会选什么? 参考答案:Redis Session 或 JWT+Redis 会话版本/黑名单,保证可撤销。
112. 几十个微服务互相调用,怎么知道对方在哪、配置怎么统一改(注册/配置中心)
【考察内容】微服务基础设施
【题目】公司微服务拆了 30 个,A 调用 B 时怎么知道 B 的地址?B 的 IP 变了怎么办?另一个问题:限流阈值、功能开关散落在各服务配置文件里,改一次要重新发版。请说明注册中心和配置中心分别解决什么问题、怎么工作。
【参考答案】
- 注册中心(服务发现):服务启动时注册自己的地址(IP:port),消费者从注册中心获取服务实例列表并负载均衡调用;核心功能:服务注册、心跳续约、健康检查、摘除故障节点、服务订阅/变更通知。解决“服务地址动态变化”问题;
- 选型:Nacos(功能全、AP/CP 可切换)、Eureka(AP)、ZooKeeper(CP)、Consul(多数据中心);
- 配置中心(动态配置):配置集中管理,修改后动态刷新到运行中的服务(@RefreshScope/长轮询),无需重启;解决“改配置要发版重启”问题;
- 选型:Apollo、Nacos、Spring Cloud Config;
- 两者关系:Nacos 可同时提供;注册中心侧重“实例地址”,配置中心侧重“运行参数”。
- 容量估算:1000 个服务实例、心跳 5s,则注册中心写 ≈ 200 写/s,读(服务发现+配置拉取)=调用方数×每秒查找次数:1000 个实例各 5~10 次/s 才是 5000~1 万 QPS;改成订阅推送+本地缓存后,打到注册中心的实际读会掉到「实例数÷核对周期」的百级(每个调用方缓存实例列表,本地缓存后打到注册中心的实际读会低很多)。配置变更频率低,但变更推送要快:长轮询/WebSocket,秒级生效。元数据存储容量小,重点在可用性与推送而不是吞吐。多机房部署时注册中心本身要跨机房同步策略。
- 失败与降级:注册中心宕机→客户端本地缓存实例列表继续调用(关键);配置中心宕机→应用使用本地快照启动与运行;推送失败→客户端定时全量拉取兜底。核心原则:注册/配置中心是控制面,挂了不应导致数据面全灭。监控:心跳丢失率、推送延迟、客户端缓存命中。
【原理溯源】
- 为什么硬编码 IP 不行? 扩缩容、容器重建、故障迁移都会改地址。硬编码导致发布耦合与频繁改配置。注册中心把“地址发现”从静态配置变成动态数据。
- 注册中心四步闭环? 注册(上线声明)→ 心跳(证明活着)→ 健康检查/摘除(去掉死实例)→ 订阅通知(消费者更新列表)。缺一环就会调到死实例或发现不了新实例。
- 为什么配置要动态刷新? 限流阈值、开关、超时若绑定发布,响应慢且风险大。配置中心把“变更”从“发版”解耦,支持灰度与快速回滚。
- 为什么两者常一起谈却要分清? 都是“运行时元数据”,但生命周期与一致性要求不同:实例列表要快速摘除故障;配置变更要审计、灰度、可回滚。混用容易把配置当发现、把发现当配置。
- 为什么 Nacos 能两者通吃? 统一元数据与控制面,降低组件数;但仍要在使用上区分领域模型(服务 vs 配置项)。
【选型判断树】
要解决什么问题?
├─ 服务在哪、实例变了 → 注册中心
│ ├─ 偏可用、可容忍短暂旧列表 → Nacos 临时 / Eureka
│ └─ 偏强一致 → Nacos CP / ZK / etcd
└─ 参数/开关/阈值变更 → 配置中心
├─ 要灰度/审计/权限强 → Apollo
└─ 已用 Nacos、需求标准 → Nacos 配置【口述骨架】(5 分钟;框架 A,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 分 | 讲注册闭环与动态刷新机制;强调本地缓存与灰度回滚;能区分职责 |
【关联题】
【自测】
- 注册中心解决什么核心问题? 参考答案:服务实例地址动态变化时的自动发现与更新,避免硬编码 IP。
- 配置中心的核心价值是什么? 参考答案:参数集中管理与动态刷新,改配置不必发版重启,可灰度回滚。
- Nacos 的特点是什么? 参考答案:注册与配置一体,AP/CP 可切换,国内微服务常用。
113. 半夜注册中心宕机,服务调用会全断吗(注册中心故障)
【考察内容】服务发现的容错设计
【题目】半夜注册中心(Nacos)宕机,值班的人慌了:“服务之间是不是全断了?”请分析:注册中心挂了之后,服务间调用会中断吗?为什么?消费者端的缓存机制、心跳续约、摘除逻辑分别是什么?
【参考答案】
- 答案:短时间不会全断,取决于实现:
- Eureka:消费者本地缓存服务实例列表,注册中心挂了不影响已缓存的调用(AP 设计,自我保护模式“宁可保留旧实例也不摘除”);
- Nacos:临时实例模式下,消费者有本地缓存+订阅推送,短暂不可用期间仍可调用已缓存实例;服务上下线感知延迟;
- ZooKeeper:客户端缓存连接,ZK 挂后调用依赖缓存的节点列表(CP,但新发现不可用);
- 真实风险:长时间不可用——新服务上线无法被发现、故障实例无法摘除(调用到已挂实例报错);
- 增强方案:消费者侧多级容错(本地配置兜底实例列表)、重试与故障转移、熔断降级;
- 心跳续约与摘除(题干直问的两问,按三步答全):续约=实例按周期上报心跳(Eureka 客户端 30s 续约、Nacos 1.x 临时实例 5s 心跳(2.x 起改为 gRPC 长连接,不再按周期发心跳 beat));标记不健康=超时未续约先标「不健康」但不立刻摘(Nacos 1.x 是 15s 标不健康、30s 才移除,2.x 起长连接一断就剔除;Eureka 90s 才出注册表);摘除=到阈值才从列表移除并推送变更,且自我保护/阈值保护会在心跳成功率过低时停止摘除(网络抖动比实例真挂更常见)。别答成「心跳一跳没回就摘」。顺带结论:注册中心高可用(集群部署)仍是必须的;但调用链不因注册中心单点而中断是分布式系统设计原则。
- 容量/故障窗口数字:Eureka 客户端默认缓存服务列表,注册中心宕机后在缓存 TTL/续约周期内调用不受影响(约分钟级窗口);Nacos 客户端也有本地缓存与快照。调用方 QPS 本身不打注册中心,而是打服务提供者——注册中心挂了影响的是扩缩容与故障摘除,不是已知实例的转发。容量上要评估:故障期间实例下线无法被摘除,靠调用方超时+重试+熔断消化,重试放大系数约 1+失败率×重试次数,不能超过剩余健康实例容量。
- 失败与降级:注册中心宕机→继续用本地缓存;实例下线→调用方超时/熔断/重试其他实例;长时间故障→运维静态路由表应急;恢复后自动重新注册。禁止:注册中心一挂就重启所有应用。演练:拔掉注册中心,验证核心交易是否仍可用。
【原理溯源】
为什么短时间不断? 服务发现是“元数据”,不是每次调用都同步询问注册中心。消费者拿到列表后本地缓存,调用直接打实例。注册中心挂了,已有缓存仍可用。
心跳续约做什么? 实例定期上报存活;超时未续约被标记不健康/摘除。注册中心宕机期间心跳失败,但消费者不一定立刻清空本地列表。
摘除逻辑的风险? 若故障检测过于激进,网络抖动会误摘;过于保守会调到死实例。Eureka 自我保护在成功率低时停止摘除,优先保可用。
为什么长时间宕机会出事? 新实例无法注册(扩容无效)、旧坏实例无法摘除、配置/元数据停止更新。系统进入“冻结的旧视图”,故障放大。
为什么还要集群部署注册中心? 缓存只是短暂缓冲,不能替代高可用控制面。生产必须多副本、跨机房,并演练注册中心故障。
【选型判断树】
注册中心宕机应急判断:
├─ 影响时间 < 本地缓存有效期/可接受窗口
│ → 业务通常无感;尽快恢复控制面
└─ 长时间不可用
├─ 消费者有静态兜底列表 → 可继续调用
├─ 无兜底 → 新变更冻结;调用依赖旧缓存+重试
└─ 同时启动:集群恢复、必要时本地直连应急
设计层:消费者必须本地缓存 + 重试 + 熔断【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 临时实例 | 1.x:5s/15s/30s;2.x:断连即摘 | 可调 |
| 本地缓存 | 必配 | 故障缓冲 |
| 调用超时 | 百 ms 级 | 快失败 |
| 注册中心集群 | ≥3 节点 | 高可用 |
| 客户端缓存窗口 | 分钟级(默认配置相关) | 挂了不应立刻全断 |
| 重试放大 | 1+失败率×重试次数 | 勿超过剩余容量 |
| 应急 | 静态路由/本地快照 | 长故障预案 |
【追问链】(三层)
L1|“挂了 10 分钟会怎样?” → 若实例稳定,多数调用仍靠缓存成功;新发布/扩缩容失败;部分坏实例无法摘除导致错误率上升。要监控注册中心与错误率。
L2|“缓存里的实例已经挂了怎么办?” → 这条只能靠调用侧兜——注册中心自己正挂着,缓存列表里的坏实例没人摘。四步:① 百 ms 级超时后立刻重试到列表内其他实例,但要先按第 5 条核算放大系数(约 1 + 失败率 × 重试次数),不能超过剩余健康实例的容量,否则从「一个实例挂」变成「全挂」;② 客户端对连续失败的实例做临时剔除或降权,下个刷新周期再放回;③ 熔断保住线程池不被拖死,避免局部故障放大;④ 平时把优雅下线做扎实:先注销、等消费方列表刷新到空窗,再停进程。还要记得 Eureka 自我保护在 15 分钟内成功率 <85% 时停止摘除,这时客户端剔除与重试是唯一防线。
L3|“要不要给消费者配静态 IP 列表?” → 可作为最后兜底(本地配置/环境变量),尤其核心链路。但要防止与动态发现长期不一致,仅应急使用。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 觉得会全挂或全不挂,理由含糊 |
| 80 分 | 知道本地缓存所以短时间不断 |
| 95 分 | 讲心跳摘除与长时间风险;组件差异;给出静态兜底与熔断重试 |
【关联题】
【自测】
- 注册中心宕机为什么服务调用不一定中断? 参考答案:消费者本地缓存了实例列表,调用不依赖每次询问注册中心。
- 长期宕机的主要风险是什么? 参考答案:新实例无法发现、故障实例无法摘除、元数据停止更新,错误率上升。
- 消费者侧应做哪些容错? 参考答案:本地缓存、静态兜底列表、超时重试、熔断降级。
114. 订单超时扫描任务在多台机器上重复执行(分布式定时任务)
【考察内容】分布式任务调度
【题目】订单超时关单的定时任务每分钟扫一次,但服务部署了 10 个实例——10 个实例同时扫,订单被重复处理。怎么让定时任务只执行一次?数据量大时怎么分片并行?XXL-JOB 的分片广播机制是什么?
【参考答案】
- 问题:多实例同时跑同一个定时任务会重复执行——需要分布式调度;
- 方案:
- 分布式锁:任务执行前抢锁(Redis SETNX),抢到才执行——简单,适合单点执行任务;
- 分布式调度框架(XXL-JOB/Elastic-Job):
- 任务注册到调度中心,调度中心按路由策略分发给执行器(轮询/一致性哈希);
- 分片广播:任务拆 N 片,每个执行器处理一片(如按订单 ID 取模),提高吞吐;
- 失败重试、任务依赖、超时控制;
- Elastic-Job:基于 ZK 分片,分片总数固定,实例增减自动重分片;
- 幂等兜底:即使调度框架保证了不重复,任务处理本身也要幂等(如关单前检查订单状态);
- 监控:任务执行记录、失败告警、执行耗时。
- 容量估算:日订单 50 万、超时关单 30%,扫描任务每分钟扫一次未支付超时单,先按题干口径把量算出来:日订单 50 万×30%=15 万条超时单/日,每分钟一轮 → 每轮均值只有 15万÷1440≈104 行;「峰值数万行/轮」要成立,得让单分钟占日量 20%–33%(峰值是均值的 288–480 倍),不现实。正常写「数百~数千行/轮」;只有把「积压若干轮」或「扫描窗口放宽到小时级」当明确前提时才写数万——按错值配分片数会把并行度高配一到两个数量级。分片扫描:按订单 ID 哈希到多机,每实例每轮处理 N 千行,处理能力要 > 到达速率。分布式锁/任务调度组件(XXL-Job、ElasticJob、Quartz 集群)自身操作 QPS 很低,容量瓶颈在扫描 SQL 与关单写。避免全表扫:索引
(status, expire_time)+ 时间游标。 - 失败与降级:锁失效导致短暂重复执行→关单接口幂等+状态条件更新;调度中心宕机→备节点接管;扫描 SQL 慢→分页/分片/限流;MQ 延迟消息已有兜底扫描时注意勿双开冲突(统一入口)。监控:任务执行成功率、重复执行次数、关单耗时、积压待关单数。
【原理溯源】
- 为什么会重复执行? 每个 JVM 的 @Scheduled 都独立触发,彼此不知情。单机互斥原语(synchronized)锁不住其他进程。
- 分布式锁方案的本质? 用跨进程互斥把“多实例触发”收敛为“一实例执行”。简单,但吞吐受限于单实例,且锁可靠性依赖 Redis。
- 调度框架解决了什么? 把“谁来跑、跑哪部分、失败怎么办”集中编排:路由策略防重复、分片提升并行、失败重试与日志可运维。
- 分片广播为什么快? 把大扫描按 ID 取模拆给多执行器并行处理,避免单点扫描超时。分片键要均匀,数据倾斜要治理。
- 为什么仍要任务幂等? 框架只能保证“调度层尽量一次”,不能消除重试、重复消息、业务并发。关单仍要检查状态,幂等是业务正确性底线。
【选型判断树】
任务特征?
├─ 必须单实例执行、频率低 → 分布式锁(Redis)+ 幂等
├─ 要可视化、重试、路由 → XXL-JOB
├─ 要自动分片与弹性 → Elastic-Job / XXL-JOB 分片广播
└─ 数据量极大 → 分片并行 + 幂等 + 告警监控
任何方案都要:业务幂等【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 分 | 强调业务幂等;分片倾斜;调度中心高可用与监控 |
【关联题】
【自测】
- 为什么本地 @Scheduled 多实例会重复? 参考答案:每个进程独立触发,无跨实例互斥。
- XXL-JOB 分片广播解决什么问题? 参考答案:把任务拆成多片并行执行,提高吞吐并避免单实例扫描瓶颈。
- 调度框架上线后还需要幂等吗? 参考答案:需要。调度层无法消除重试与业务并发,处理逻辑仍要可重入。
115. 一个请求跨 5 个服务变慢,怎么定位是哪一环(链路追踪)
【考察内容】可观测性设计
【题目】一次请求要串行经过网关、订单、库存、支付、短信 5 个服务,线上变慢了,但每个服务的监控都“看着正常”。怎么定位到底慢在哪个服务哪一环?链路追踪的核心原理是什么(traceId/span、透传、采样)?
【参考答案】
- 核心机制:TraceID + SpanID:
- 请求入口生成全局唯一 TraceID,通过 HTTP 头/消息头透传到所有下游服务;
- 每个服务内每个调用(DB、RPC、MQ)生成 Span(父子关系),记录耗时、状态、参数;
- 所有 Span 汇总到追踪系统(按 TraceID 聚合),还原完整调用链;
- 组件:SkyWalking(无侵入,字节码增强)、Zipkin、Jaeger(OpenTelemetry 标准);
- 使用场景:
- 定位慢请求:看各 Span 耗时,找出最慢环节;
- 定位报错:按 TraceID 查全链路日志;
- 依赖分析:服务间调用关系图、依赖瓶颈;
- 配套:日志中打印 TraceID(logback MDC),日志检索与链路关联;
- 采样策略:全量采样压力大,生产按需采样(如 10% 采样+错误全采+超阈值慢请求 100% 采样)。题干要「定位慢在哪一环」,而慢请求通常不是错误,按 10% 抽样会漏掉九成现场,等于把要查的那批 trace 扔了;本条判断树与【关键数字】也正是按「错误/慢请求常 100% 采样」口径写的。
- 容量估算:日请求 1 亿、采样率 1% → 被采请求 100 万条;一条链路按本题【关键数字】的 5~10 个服务展开,日 span 样本约 500 万~1000 万条(别把被采请求数当 span 数);若全采,存储与索引成本高,通常头部全采或重要链路提高采样。单服务 QPS 1 万时,trace 上报不能阻塞业务:本地异步队列+批量上报。存储:Jaeger/SkyWalking 等按保留天数估磁盘。查询场景是排障,不是在线业务路径——追踪系统挂了不影响交易,但要能告警。
- 失败与降级:追踪 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 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “单服务指标正常不代表全链路正常” |
| 0:30–1:30 | Trace/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 数量 | 控制在必要调用 | 过多成本高 |
| 日志关联 | 必须打 TraceID | MDC |
| 典型链路 | 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 定位慢节点 |
【关联题】
【自测】
- 链路追踪的最小必要数据是什么? 参考答案:全局 TraceID + 各服务 Span(含耗时/状态/父子关系)。
- 为什么高 QPS 要采样? 参考答案:全量上报成本过高;用概率采样并对错误/慢请求全采。
- 日志如何与链路关联? 参考答案:日志中输出同一个 TraceID(MDC),可按 ID 检索全链路日志。
116. 两台服务器时间差了 5 分钟,日志排序和限流全乱了(时钟不一致)
【考察内容】分布式时间一致性
【题目】排查问题时发现两台服务器系统时间差了 5 分钟:日志时间线对不上、限流窗口错乱、订单超时判断不准。分布式系统时钟不一致会产生哪些问题?如何解决(NTP 同步、逻辑时钟、不依赖物理时钟的设计)?
【参考答案】
- 问题:
- 日志/链路追踪乱序:多机日志时间戳不一致,排查困难;
- 业务时间错误:订单时间、定时任务触发、过期判断出错;
- 分布式锁/雪花 ID:时钟回拨产生重复 ID;锁过期判断依赖时钟;
- 跨时区/容器时钟漂移;
- 解决:
- NTP 时钟同步:统一 NTP 服务器,chrony/ntpd 定期校准,监控偏差;
- 不用本地时钟做关键决策:业务时间以数据库/服务端统一时间为准(NOW()),时间戳字段由 DB 生成;
- 单调时钟 vs 墙上时钟:性能统计、超时计时用单调时钟;业务时间用墙上时钟并同步 NTP;
- 雪花算法:监控偏差+回拨处理;
- 设计原则:分布式系统里“时间”要有唯一权威来源,本地时钟只用于辅助。
- 容量/偏差数字:机器间时钟偏差应控制在 毫秒级(NTP 通常 <50ms,好的环境 <10ms);超过秒级会影响:分布式锁 TTL、限流窗口、日志排序、雪花 ID、会话过期、安全审计。5 分钟偏差对限流窗口(1s/1min)是毁灭性的。审计与金融日志要求更高,常要 NTP 监控+偏差告警(如 >100ms 预警、>1s 严重)。跨机房要评估时间源(GPS/北斗/NTP 层级)。
- 失败与降级: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 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “分布式时间必须有权威来源,不能信各自本地时钟” |
| 0:30–1:30 | 问题面 | 日志、过期、锁、雪花 |
| 1:30–2:30 | NTP | 同步与监控 |
| 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 |
【关联题】
【自测】
- 为什么业务时间戳建议由 DB 生成? 参考答案:提供唯一权威时间,避免多实例本地时钟偏差污染数据。
- 耗时统计应使用什么时钟? 参考答案:单调时钟(如 nanoTime),不受墙上时钟回拨影响。
- 时钟不一致还会破坏哪些组件? 参考答案:分布式锁过期、雪花 ID、限流窗口、日志与追踪排序。
117. 用户手抖连点两次“提交订单”,生成了两单(重复下单)
【考察内容】防重复下单是电商最高频实战题
【题目】用户手抖或网络重试导致“提交订单”被调用了两次,生成了两个一模一样的订单,还被扣了两份钱。前端只做按钮置灰不够,后端怎么防?幂等方案(幂等键、唯一约束、状态校验)怎么设计?
【参考答案】
- 前端:按钮提交后置灰/loading(防手抖,防不了脚本);
- 服务端 Token 机制:下单前获取唯一 token(绑定用户),下单请求携带 token,服务端原子消费 token:Lua 里判断存在并删除(或 6.2+ 的
GETDEL)——取不到即视为已使用,拒绝; - 业务幂等键(前提要写出来,否则第 4 条防线形同虚设:一次提交流程只生成一次、失败重试沿用同一个键,若手抖两次各自新生成 UUID,两条请求都「合法」,唯一键根本不冲突):前端生成/服务端下发幂等键,服务端以“用户+幂等键”为唯一键处理,重复请求返回首个结果;
- DB 最终防线:唯一键按业务语义选,三种情况别混——① 普通下单:用
(user_id, 幂等键/请求号)(或订单号本身唯一),重复请求返回首单;不能按「用户+商品」建唯一索引,那会把正常复购直接判重报错;② 限购/一人一单活动:(user_id, sku_id, 活动期)或(user_id, activity_id),此时“同商品不允许第二次下单”正是业务要求,与第 131 题同口径;③ 支付回调:渠道流水号唯一,重复回调只入账一次。分布式锁/SETNX 只降低冲突概率,正确性始终以 DB 唯一索引为准(锁丢了索引还在;索引没有时,锁挡不住多实例竞态); - 顺序(题干第二问括号里的「状态校验」落在这一环,不能只答幂等键+唯一约束):校验(Token)→ 状态校验(按幂等键查该请求是否已成单:
SELECT order_id,status FROM ... WHERE idempotent_key=?,命中即直接返回首单结果,不再往下走)→ 防重(唯一索引)→ 生成订单(事务;后续状态流转用WHERE status=待支付这类条件更新守住状态机)→ 返回。要点:「查—判—插」必须原子(同一事务内靠唯一索引兜竞态,或先用 Redis SETNX 占位),只查不判会双写,只插不查则重复请求拿到的是报错而不是首单; - 注意:幂等键需用户维度(同一用户同一操作同一结果),不能全局一个。
- 容量估算:防重复下单的幂等层按提交峰值设计,秒杀场景提交峰值可达 数千~数万 QPS。token/唯一键校验若走 Redis,需同量级;DB 唯一索引插入热点要分片时,分片键必须含(或等于)幂等唯一键的判定维度,即按 user_id/幂等键分片;不能按「订单号」分片:每次重试都带一个新订单号,同一幂等键会落到不同分片,跨分片唯一约束不成立,第 4 条的「最终防线」就被这条自己拆掉(订单号只作唯一列,不作分片键)。按钮防抖只能挡前端,挡不住脚本——服务端幂等才是关键。库存扣减与创建订单要原子或条件更新,重复提交不应导致超卖。
- 失败与降级:Redis token 层挂→DB 唯一约束+条件更新兜底;重复提交返回已有订单(查询态)而不是报错;脚本刷单→限流+风控+验证码。监控:重复提交拦截次数、创建订单成功率、唯一键冲突率。
【原理溯源】
为什么前端置灰不够? 双开标签、脚本、弱网重试、网关重发都会造成重复。客户端提示是体验优化,不是正确性机制。
Token 机制防的是什么? 防“重复提交进入业务”。一次一 token,用后作废,相当于门票。适合“提交前先领票”的流程。
幂等键与 Token 差别? Token 强调一次性准入;幂等键强调“同一业务操作多次请求返回同一结果”。下单两者可叠加。
为什么必须 DB 唯一索引? 应用层锁/缓存都可能失效或竞态。唯一索引在存储引擎保证同一业务组合只成功一次,是不可绕过的最终防线。
为什么重复时应返回已下单而不是报错? 用户/调用方往往不知道首次是否成功,返回已有订单符合幂等语义,也避免误报失败导致再次下单。
【选型判断树】
防重复下单分层:
1. 前端置灰(体验)
2. Token 准入(防进入)
3. 幂等键/唯一索引(防重复业务效果;前提:一次提交流程只生成一个键、重试沿用同键,否则两条请求各自唯一、索引根本不冲突)+状态校验:按幂等键查是否已成单,命中即直接返回首单(第三问要答到防重复的落地)
4. 库存与支付也幂等(链路下游)
秒杀一人一单:
└─ 唯一索引 (user_id, activity_id) 或 用户维度锁 + 幂等【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 条①那条 (user_id, 幂等键) 唯一索引还在(一人一单活动才换成②的用户+商品+活动维度),重复提交仍然只成功一次——Redis 在这里只降低并发冲突,正确性不依赖它,关键数字也写明 Redis 锁「仅辅助,不能替代唯一索引」。副作用要提前算:token 这道前置闸门失效后,第 7 条说的数千~数万 QPS 提交峰值会直接打在唯一索引冲突上,所以 Redis 不可用期间要收紧该接口限流,必要时上验证码与风控。对用户不能返回 500:重复请求要返回已创建的订单(查询态),否则用户以为失败再下一单,反而制造更多冲突。
L3|“同一用户不同商品能不能各下一单?” → 可以。幂等键维度要与业务规则对齐:一人一单活动用活动维度;普通下单用订单幂等键维度,不能错误全局互斥。
【评分标准】
| 档位 | 答案特征 |
|---|---|
| 60 分 | 提到前端防抖 |
| 80 分 | 会讲 Token/幂等键 |
| 95 分 | 唯一索引最终防线(含分片键必须含幂等维度)、状态校验+返回首次结果、秒杀一人一单设计、幂等键「一次流程一个键」前提 |
【关联题】
【自测】
- 防重复下单为什么不能只靠前端? 参考答案:脚本、多标签、重试仍会重复请求,服务端必须有幂等与约束。
- 一人一单秒杀最可靠的手段是什么? 参考答案:数据库唯一索引(user_id+activity_id),冲突则返回已下单。
- 重复提交应如何响应? 参考答案:返回首次成功结果(已有订单),而不是报错。
118. 多实例部署后 synchronized 全失效了(多实例并发控制)
【考察内容】分布式并发安全是综合必考题
【题目】服务从单机改成多实例后,原来靠 synchronized 保护的并发逻辑全部失效(每个实例各锁各的)。并发安全怎么办?从数据库乐观锁、Redis 分布式锁、消息队列串行化三个层面,给出完整方案与适用场景。
【参考答案】
- 认知:多实例=多个 JVM 进程,本地锁只能锁住本进程,跨进程必须用分布式互斥机制;
- 方案矩阵:
- 数据库:悲观锁(SELECT FOR UPDATE)、乐观锁(version/CAS 条件更新)——简单可靠,适合低频写;
- Redis 分布式锁:SET NX EX + Lua 解锁 + 看门狗(Redisson)——性能好,适合高并发临界区;
- ZooKeeper/etcd 锁:临时顺序节点/租约,强一致,适合严格互斥;
- 消息队列串行化:并发写投递 MQ,单消费者串行处理——用排队换无锁;
- 配套原则:
- 能避免锁就避免:幂等、唯一索引、状态机;
- 锁粒度最小化:按用户/订单维度,避免全局锁;
- 锁+幂等双保险:锁是事前互斥,幂等是事后兜底;
- 验证:多实例并发压测。
- 容量估算:多实例下本地 synchronized 只能保证单机互斥。若全局临界区操作峰值 1000 次/s,Redis 分布式锁要能扛同量级且持锁时间短;若临界区耗时 50ms,则全局串行上限约 20/s——说明这类热点应改设计(分段锁/队列/合并),而不是硬加锁。本地锁适合单实例内存结构;跨实例一致性要靠分布式协调或无共享分片。
- 失败与降级: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 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 0:00–0:30 | 定性 | “本地锁跨不了进程,必须分布式互斥” |
| 0:30–1:30 | 数据库 | 乐观/悲观 |
| 1:30–2:30 | Redis/ZK | 性能 vs 强一致 |
| 2:30–3:30 | MQ 串行 | 排队换无锁 |
| 3:30–4:30 | 原则 | 最小粒度、锁+幂等 |
| 4:30–5:00 | 收尾 | “先数据约束,再分布式锁” |
【关键数字】
| 参数 | 经验值 | 说明 |
|---|---|---|
| 乐观锁冲突重试 | 有限次,否则降级 | 防活锁 |
| Redis 锁 TTL | 略大于临界区耗时 + 看门狗 | 防死锁与提前释放 |
| 行锁等待超时 | 秒级可配 | 避免拖垮连接 |
| MQ 分区/队列 | 同一业务键同一分区 | 保证串行 |
| 压测 | 必须多实例 | 验证真实并发 |
| 本地锁范围 | 仅单实例 | 多实例失效 |
| 全局串行上限 | 1/临界区RT | 50ms→约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 分 | 讲选型条件、锁+幂等、最小粒度;知道数据约束优于盲目加锁 |
【关联题】
【自测】
- 为什么单机锁在多实例下失效? 参考答案:锁状态在进程内存,进程间互不可见。
- 高并发扣库存你会怎么组合? 参考答案:Redis/本地预扣 + DB 条件更新或唯一约束 + 幂等,极端热点再拆分异步。
- “锁+幂等”分别防什么? 参考答案:锁防并发进入;幂等防重复执行/锁失效后的错误结果。
119. 本地缓存+Redis+DB 三级,更新一处到处不一致(多级缓存一致性)
【考察内容】多级缓存一致性治理
【题目】系统做了“本地缓存 → Redis → DB”三级缓存,更新数据时只更新了 DB,用户读到的还是本地缓存/Redis 里的旧值,三层互相不一致。怎么设计更新策略?本地缓存怎么失效(广播/短TTL)?怎么取舍一致性 vs 性能?
【参考答案】
- 更新策略(写路径):
- 先更新 DB(事务提交);
- 删除 Redis 缓存(Cache Aside),删除失败用重试/MQ/Canal binlog 兜底;
- 本地缓存失效:通知所有实例——Redis Pub/Sub 广播失效消息、版本号比对、短 TTL 兜底;
- 读路径:本地缓存 → Redis → DB → 逐级回填;
- 一致性等级:
- 强一致(很难):读写串行化/绕过缓存,一般不追求;
- 最终一致(主流):删缓存+异步补偿+短 TTL,秒级收敛;
- 特殊场景:
- 更新频繁的数据不进本地缓存;
- 广播失效实例多时压力大——用版本号替代广播;
- 对账任务兜底修复;
- 原则:能容忍的短暂不一致用最终一致;不能容忍的直读 DB。
- 容量估算:本地缓存抗热点,进程内访问是微秒级(纳秒–微秒,无网络往返)、容量 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 体积×数量×副本。
- 失败与降级:本地缓存不一致→短 TTL+版本号/失效广播;Redis 挂→本地缓存+限流打 DB——先把降级后的量写出来再谈限流:按本条第 6 项口径,本地只挡 30%,Redis 一挂就有 ≈7 万 QPS 直接落 DB,而同一条里 DB 的「可接受」是 3500 QPS,差 20 倍;只说「限流打 DB」等于当场拒掉约 95% 的读请求(3500 ÷ 7 万 ≈ 5%,与前面的「差 20 倍」是同一个数),且本地未命中仍要回源 DB,回源风暴更糟。降级要分级写:① 本地 TTL 临时放大+全量预热(把命中率从 30% 顶到 70%+);② 切 Redis 多副本/哨兵或只读降级;③ 只保核心接口,非核心直接返回兜底;④ 限流阈值按 DB 实测能力设,并明确「超出即拒绝」而非默默排队;DB 挂→只读缓存降级(容忍短暂旧数据);更新失败→延迟双删/binlog 订阅重试。监控:三级命中率、回源 QPS、不一致投诉、缓存内存与逐出。
【原理溯源】
为什么“只更新 DB”会不一致? 缓存是副本。不主动失效/更新,TTL 到期前一直读旧值。多级副本让不一致窗口更多、更难清。
为什么写路径是“先更 DB 再删缓存”? 先删缓存可能在回填旧值后 DB 更新失败,脏数据驻留更久。业界常用 Cache Aside:更新 DB 后删缓存,读时回填;仍有小概率不一致,靠重试/TTL/对账收敛。
本地缓存为何更难失效? 每实例一份,无中心入口。必须广播失效、版本号轮询或短 TTL,否则实例间长期各读各的旧值。
为什么更新频繁的数据不进本地缓存? 失效广播频率高、命中率低、不一致窗口难压。这类数据只放 Redis 或直读 DB。
强一致缓存为何难? 多副本+异步失效无法保证“任何读都看到最新写”。要强一致只能串行化或不缓存,性能与一致性必须取舍。
元理论对应: 原理 4「时间差、多副本视图与一致性窗口」+原理 6「一致性定价与按业务分级购买」(题群见《核心考点与元理论》§4.7+§6.7)
【选型判断树】
数据一致性要求?
├─ 不能读到旧值(资金、权限)→ 不进多级缓存 / 直读 DB / 短 TTL 强一致方案
└─ 可秒级旧值 → 多级缓存最终一致
└─ 本地缓存失效方式:
├─ 实例少、变更少 → Pub/Sub 广播
├─ 实例多、怕广播风暴 → 版本号/短 TTL
└─ 更新频繁 → 根本不进本地缓存【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 分 | 写读路径完整、更新频繁不进本地、强一致绕过缓存的取舍 |
【关联题】
【自测】
- Cache Aside 典型写路径是什么? 参考答案:先更新 DB,再删除缓存;读时未命中则读 DB 并回填。
- 本地缓存多实例如何失效? 参考答案:Pub/Sub 广播、版本号比对、短 TTL 兜底;更新频繁的数据不进本地缓存。
- 资金账户余额能放本地缓存吗? 参考答案:一般不能。需要强一致,应直读 DB/强一致缓存或极短 TTL 并严格串行化。
120. 改个限流阈值,改完整个集群立刻生效把线上搞挂了(配置灰度)
【考察内容】配置治理与发布工程
【题目】有人在配置中心把限流阈值改小,配置实时推送到全量实例,线上接口立刻大面积被限。配置变更怎么灰度发布(按实例/按比例/按环境)?改坏了怎么快速回滚?配置中心和发布系统怎么配合?
【参考答案】
- 需求:配置错误/新配置需要小范围验证,不能一键全量生效;
- 方案:
- 按实例灰度:指定部分实例(按 IP/分组)应用新配置,验证后再全量;
- 按用户/流量灰度:配置值带生效规则(user_id 百分比/白名单);
- 按环境/集群:预发 → 生产小集群 → 全量;
- 流程:灰度发布 → 监控(错误率/RT/业务指标)→ 通过全量 / 失败回滚(一键回滚旧值);
- 工程要点:配置变更审计、变更后校验、回滚预案;Apollo/Nacos 支持命名空间/灰度;与发布系统配合:配置项随代码版本一起声明(同一条流水线里带默认值与取值范围),发版前做配置校验与灰度门禁,新代码依赖的新配置必须先于流量放量生效,回滚时代码与配置一并回退(旧代码读新配置是二次事故)。
- 业务代码注意:配置读取不能启动时一次性加载(动态刷新,@RefreshScope),否则灰度不生效。
- 容量/生效时间数字:全量配置推送,数千实例在秒级同时生效——若新阈值错误,故障也是秒级全集群。灰度目标:先 1%→10%→50%→100%,每档观察 5–15 分钟。配置变更 QPS 本身很低,关键是爆炸半径而不是吞吐。限流阈值这类配置,错误值的影响是线上的直接成功率,必须有:默认值、范围校验、一键回滚、变更审计。
- 失败与降级:配置中心故障→应用用本地快照;推送失败→客户端定时拉取;错误配置→秒级回滚到上一版本;高危配置需二次确认/审批流。监控:配置变更事件、关键阈值生效状态、变更后错误率/RT 关联看板。
【原理溯源】
- 为什么配置全量即时推送是事故源? 配置是“没有编译期校验的代码”。错误阈值/开关一旦全量生效,爆炸半径=整个集群,且推送比发版更快,来不及人工反应。
- 灰度的本质是什么? 把变更爆炸半径从 100% 降到可控子集,用监控验证后再放量。与代码发布灰度同理,只是变更载体是配置。
- 为什么要分实例灰度和用户灰度? 实例灰度验证“运行时行为/性能”;用户/流量灰度验证“业务效果”。限流阈值更适合实例/集群灰度;功能开关更适合用户灰度。
- 为什么必须能一键回滚? 配置变更快,出事也要同样快地恢复。没有版本与回滚的配置中心等于裸奔。
- 为什么动态刷新是前提? 若代码启动时读一次配置,灰度改配置不生效,灰度机制形同虚设。可变更性是配置治理的地基。
【选型判断树】
配置变更类型?
├─ 限流/超时/池大小(性能敏感)→ 实例/集群灰度 + 强监控
├─ 功能开关 → 用户/流量灰度
├─ 中间件地址/密钥 → 通常不灰度,走双写/切换预案
└─ 高风险全局项 → 分环境:预发 → 小流量 → 全量
任何变更:审计 + 可回滚 + 监控【口述骨架】(5 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 分 | 按参数类型选灰度维度;强调动态刷新与审计;考虑流量打爆等副作用 |
【关联题】
【自测】
- 为什么配置全量推送容易出事故? 参考答案:爆炸半径等于全集群,且生效极快,缺少验证与缓冲。
- 功能开关和限流阈值灰度方式有何不同? 参考答案:功能开关多按用户/流量;限流阈值多按实例/集群并配强监控。
- 业务代码如何支持配置灰度? 参考答案:运行时动态刷新(@RefreshScope/监听),不能只在启动时读一次。
121. 单机自增主键好好的,为什么要引入分布式 ID(ID 方案)
【考察内容】ID 方案选型判断
【题目】有人质疑:单机数据库自增主键用得好好的,为什么要搞分布式 ID?请说明自增主键在什么场景下会失效(分表、合并、跨库、泄露业务量),以及“全局唯一”和“分布式 ID”的区别,什么场景必须上。
【参考答案】
- 单机自增的问题:
- 多库/分库分表:每个库独立自增,会重复;
- 高并发写入:单库自增是热点,且依赖 DB;
- 数据合并/迁移:自增 ID 跨库合并会冲突;
- 必须用分布式 ID 的场景:
- 分库分表后的主键(全局唯一);
- 分布式系统的主键(订单、消息、流水)需要全局唯一;
- 高并发写入(不依赖 DB 生成);
- 需要趋势递增或含时间信息的 ID;
- 不需要的场景:单库单表、ID 无业务含义、量级小——自增/业务 ID 即可;
- 附加考量:ID 安全性(防遍历)、前端精度(雪花 64 位转字符串)。先把概念分清(题干第二问):「全局唯一」是需求,要先定范围(库内 / 跨库 / 跨系统);「分布式 ID」只是手段之一(号段、雪花、发号服务、UUID 都能实现),单库自增在「该库全局」内也唯一。题干列的「泄露业务量」同样不是唯一性失效,而是自增号可枚举的安全问题,对策是对外单号与内部主键分离。
- 容量估算:单库自增在中低并发写(数百 TPS)通常足够;分库后若每库都要全局单号,自增失效。分布式 ID 容量见 106 题:号段/雪花可到数十万 ID/s 量级,远超多数业务。是否上分布式 ID 的判断阈值:是否分库分表、是否跨系统要唯一、单库自增是否成为热点、是否需要趋势递增。不是所有表都要雪花——单库配置表用自增更简单。
- 失败与降级:发号服务故障→双缓冲号段/本地雪花降级/备用号段;时钟回拨→107 题策略;ID 冲突→DB 唯一索引兜底。设计时不要把“自增号”当对外可枚举业务主键(防遍历)。
【原理溯源】
- 为什么单库自增会重复? 自增计数器在库内。订单库与用户库都是 1,2,3...;分表后每表一套。唯一性范围=计数器作用域,作用域一扩就不唯一。
- 为什么合并/迁移会冲突? 历史自增 ID 不带来源信息,多库数据落到同一存储时主键撞车。
- 为什么高并发不喜欢单点发号? 每次 INSERT 都要获取自增值,热点与 DB 瓶颈耦合。分布式 ID 目标之一是发号去中心化/批量化。
- “全局唯一”≠“必须用雪花”? 全局唯一是需求;雪花/号段/UUID/发号服务都是手段。单库场景用自增也能在“该库全局”唯一。要先定义唯一范围。
- 为什么有人觉得“自增够用”? 在单体、单库、低并发成立。质疑者往往默认架构不变。正确回答是展示哪些架构变化会打破假设,而不是为分布式而分布式。
【选型判断树】
唯一性范围是否超过单库/单表?
├─ 否 → 自增即可,别上复杂 ID 系统
└─ 是
├─ 要趋势递增+简单 → 号段
├─ 要本地高性能生成 → 雪花
├─ 只要唯一可随机 → UUID
└─ 另加:防遍历可加随机位;前端传字符串
分库分表主键:号段或雪花最常见【口述骨架】(5 分钟;框架 C,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 分 | 区分需求与方案;强调克制与唯一范围;提安全与精度 |
【关联题】
【自测】
- 自增主键失效的三类典型场景? 参考答案:分库分表、跨库合并迁移、高并发单点热点。
- “全局唯一”和“雪花算法”是什么关系? 参考答案:前者是需求,后者是实现之一;也可用号段、发号服务、UUID 等。
- 什么场景不必上分布式 ID? 参考答案:单库单表、量级小、无跨库唯一要求。
122. 服务 A 调 B 慢,A 的线程全被占住拖垮自己(调用容错)
【考察内容】分布式调用容错设计
【题目】服务 A 每次调用 B 要 3 秒才超时,B 一抖动,A 的所有线程都被挂起等待,A 自己也跟着挂。怎么设计调用容错(超时、重试、熔断、降级、隔离)?各个机制分别解决什么问题、怎么配置?
【参考答案】
- 调用前:设置合理的超时时间(连接超时+读超时,按 B 的 P99 设定,如 500ms/1s),超时即失败,避免无限等待;
- 调用失败处理策略:
- 重试:仅对“幂等操作”重试,且限制重试次数+退避(1~2 次指数退避),防止重试风暴;写操作默认不重试;
- 降级:返回兜底数据(缓存/默认值/上一次结果);
- 熔断:B 连续失败(错误率>50%)时打开熔断器,快速失败不再调用 B,半开试探恢复;
- 异步化:非核心调用转 MQ,失败进重试队列;
- 隔离:线程池隔离(B 慢只占自己的线程池,不拖垮其他调用);
- 兜底:失败记录+定时补偿;告警人工介入;
- 设计原则:超时+重试(幂等限次)+降级+熔断+隔离五件套,且“调用失败不能阻塞主流程”。
- 容量/线程预算估算:A 线程池 200,B 超时 3s,则最坏情况下 A 的吞吐被拖到 200/3s≈67 QPS——这就是被拖垮的数学。若超时改为 500ms,同线程池理论恢复到 200/0.5=400 QPS(仍要看 CPU)。熔断打开后错误率上升但线程释放,整体可用。线程池隔离:给 B 分配 20 线程,B 挂了最多占 20,主链路 180 仍可服务其他依赖。重试放大:重试 2 次时,对 B 的最大调用流量≈3 倍输入,熔断与限流必须配合。
- 失败与降级:超时→快速失败;重试→仅幂等、1–2 次退避;熔断→错误率超阈值打开,半开试探;降级→缓存/默认值/只读;隔离→舱壁线程池/信号量。非核心调用转 MQ。失败补偿与人工预案。监控:依赖 RT/错误率、熔断状态、线程池水位、降级触发次数。
【原理溯源】
为什么 A 会被拖垮? 同步阻塞调用把下游延迟变成自己的线程占用。B 变慢→A 线程池耗尽→A 整体不可用。这是级联故障的经典形态。
超时为什么是第一道防线? 它限制单次调用最坏占用,把“无限等待”变成“有限失败”。没有超时,其他容错都无从谈起。
为什么重试必须限次且偏幂等? 无限重试会放大流量,把局部故障打成全局雪崩;非幂等重试可能重复扣款。重试是药,过量是毒。
熔断解决什么? 在 B 已经持续失败时,继续调用只是浪费线程并加重 B 负担。熔断快速失败,给 B 恢复时间;半开探测避免过早恢复流量。
隔离为什么必要? 即便有超时,多个下游共享大线程池时,一个慢下游仍可能占满池子。按下游拆线程池/信号量,故障域隔离。
【选型判断树】
调用特征?
├─ 核心同步路径
│ → 短超时 + 有限重试(仅幂等)+ 熔断 + 降级 + 线程池隔离
├─ 非核心通知/统计
│ → 异步 MQ + 重试队列,不占主链路线程
├─ 读多可缓存
│ → 降级读缓存/默认值
└─ 写且非幂等
→ 默认不重试;失败记录补偿/人工,而不是盲目重试
超时设置:
└─ 连接超时 < 读超时 < 上游总超时;超时要略大于下游 P99【口述骨架】(5 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)
| 时间 | 任务 | 内容 |
|---|---|---|
| 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 关系;能解释级联故障 |
【关联题】
【自测】
- 为什么超时是容错第一原则? 参考答案:限制单次调用最坏线程占用,避免无限等待拖垮自身。
- 什么操作可以安全重试? 参考答案:幂等操作(查询、带幂等键的写);非幂等写默认不重试。
- 熔断器半开状态做什么? 参考答案:放少量试探请求,成功则恢复,失败则继续保持打开。
附:本章知识簇索引(第五章)
| 知识簇 | 题号 |
|---|---|
| CAP / BASE 理论 | 97, 98 |
| 分布式事务 | 99, 100, 101, 102, 103 |
| 分布式锁 | 104, 105 |
| 分布式 ID | 106, 107, 121 |
| 分片与路由 | 108 |
| 数据/缓存一致性 | 109, 119 |
| 幂等与防重 | 110, 117 |
| 会话 | 111 |
| 注册/配置中心 | 112, 113, 120 |
| 分布式任务 | 114 |
| 可观测性 | 115 |
| 时钟 | 116 |
| 多实例并发 | 118 |
| 调用容错 | 122 |