Skip to content

第一章 高并发与性能优化(第1-22题) ​


1. 大促活动页查询接口预估 QPS 5 万+,单机扛不住(高并发接口设计) ​

【考察内容】高并发系统整体架构观(QPS 预估与全链路设计)

【题目】618 大促,营销活动页要上线一个查询接口,预估峰值 QPS 5 万,现有单机只能扛 2000。你作为负责人,从接到需求到上线,怎么设计才能保证大促当天不挂、不超时?

【参考答案】按“先估容量、再找瓶颈、分层治理、失败可降”的思路:

  1. 容量估算(步进):峰值 5 万 QPS,单机 2000 → 应用层理论需 25 台(按 0.8 系数约 32 台);但加机器解决不了数据层——MySQL 单机写约 2000–5000 TPS、读约 1–2 万,5 万全打 DB 必挂。假设缓存命中率 95%,回源 = 5万×5% = 2500 QPS,再叠加本地缓存与读写分离可压进单库可承受区间;写侧若峰值对应约 3000 TPS,则必须 MQ 削峰。
  2. 接入层:CDN/静态化扛活动页静态资源(占比常 >70%);Nginx limit_req + 网关全局限流,阈值按压测容量(32 台×2000≈6.4 万)的 80%≈5 万,超额返回 429 + 重试引导。注意别把「预估峰值」当容量:按 5 万×80%≈4 万设阈值,正常峰值就会有 20% 被拒。
  3. 应用层:无状态化水平扩容;热点 Caffeine + Redis;接口幂等(Token/唯一键);非核心依赖(推荐/日志)降级或异步,把线程与连接留给主链路。
  4. 数据层:SQL 走索引、读写分离;写路径 MQ 异步削峰,消费端按 DB 能力限速落库;乐观锁/条件更新控并发写。
  5. 失败与降级:Redis 故障 → 本地缓存 + 直连 DB 且限流阈值砍半;下游超时熔断返回兜底;DB 变慢启用排队页;监控 QPS/RT/错误率/命中率,压测验证 1.5–2 倍峰值(注意与本机数一致:本条按 0.8 系数配到 32 台×2000=6.4 万,只有峰值的 1.28 倍;要真做到 1.5–2 倍需 38–50 台,限流阈值也要按实配容量重算,不能沿用 6.4 万×80%),大促变更冻结。

【原理溯源】

  • 为什么“瓶颈通常在数据库”? 各层的单机能力差 1–2 个数量级:Nginx 约 5–10 万 QPS,Redis 约 10 万 QPS,Tomcat 约 2000–5000 QPS(RT 50ms 级),而 MySQL 单机写通常只有几千 TPS。所以当 QPS 从 2000 涨到 5 万,最先被打穿的一定是 DB。这就是“缓存 + 异步”必须成为重点的根因。
  • 为什么缓存能提升吞吐? 缓存把“每次请求一次 DB 往返(约 1–10ms)”变成“一次内存访问(约 0.1ms)”,缩短了单请求占用线程/连接的时间,于是同样资源能承载的并发量上升。本质是“让每个请求更快结束”,不是“让机器变快”。
  • 为什么限流是“保命”而不是“优化”? 系统容量有限(线程池、连接池、CPU)。一旦超过容量,请求排队 → RT 上升 → 客户端超时重试 → 流量被放大 → 雪崩。限流的作用是在入口挡掉超出容量的流量,牺牲部分请求,保住整体可用。
  • 为什么接口要无状态? 无状态才能任意水平扩容(加机器即加容量)。有状态则需要会话同步,扩容成本高且引入一致性问题。

【选型判断树】

接到“扛住 X QPS”的需求,按顺序做四件事:

1. 先估容量
   QPS × 单请求耗时 = 需要的并发数;对比现有单机能力 → 差几个数量级?
2. 找瓶颈层(按单机能力从低到高)
   DB(写千级)→ 应用(数千级,同题 Tomcat 2000–5000)→ 缓存(十万级)→ 网关(十万级)
3. 按瓶颈选手段
   ├─ 读多写少      → 缓存 + 读写分离 + 多级缓存
   ├─ 写多 / 瞬时尖峰 → MQ 异步削峰
   ├─ 单机不够      → 无状态化 + 水平扩容
   └─ 依赖不稳      → 限流 + 熔断 + 降级
4. 最后兜底
   压测验证容量 + 降级预案 + 监控告警 + 变更冻结

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“这是一道高并发接口设计题,我会按‘先估容量、再找瓶颈、分层治理’的顺序来答”
0:30–1:00估容量5 万 QPS ÷ 单机 2000 =理论 25 台,按 0.8 水位实配 32 台(6.4 万,只有峰值 1.28 倍;要压测验证 1.5–2 倍需 38–50 台);但加机器解决不了 DB 瓶颈,这是关键判断
1:00–3:30分层展开接入层(CDN/限流)→ 应用层(缓存/幂等/无状态)→ 数据层(索引/异步/读写分离),每层 2–3 个手段
3:30–4:30风险兜底限流熔断降级 + 全链路压测 + 监控告警 + 大促变更冻结
4:30–5:00收尾“一句话:高并发的本质是把请求挡在系统外、把压力从 DB 挪走、让系统能水平扩展”

【关键数字】

参数经验值说明
Tomcat 单机约 2000–5000 QPS简单接口,RT 50ms 级
Redis 单机约 10 万 QPS简单命令;pipeline 可更高
MySQL 单机写约 2000–5000 TPS;读约 1–2 万 QPS走索引的前提下
Nginx 单机约 5–10 万 QPS静态资源
缓存命中率目标> 90%低于 80% 说明缓存设计有问题
大促容量冗余峰值的 1.5–2 倍留出突发余量
限流阈值压测容量的 70%–80%留 20% 缓冲,避免排队雪崩
扩容触发CPU > 60% 或 RT > 目标值提前扩容,不要等打满
应用并发估算QPS × RT决定线程池与实例数

【追问链】(三层)

L1|“缓存和数据库不一致怎么办?” → 分场景:一致性要求不高用“先更新 DB 再删除缓存 + 过期时间兜底”;要求高用“延迟双删”或“订阅 binlog 异步刷新缓存”;资金类场景不用缓存做权威数据源。

L2|“限流用什么算法?各有什么问题?” → 计数器(有临界突变问题)、滑动窗口(平滑但有内存开销)、漏桶(恒定速率,无法应对突发)、令牌桶(允许突发,最常用)。分布式限流用 Redis + Lua 保证原子性。

L3|“加了缓存、限流、扩容,大促还是挂了,最可能是什么原因?” → 三类:① 缓存大面积失效(雪崩,或热 key 打满单节点);② 下游依赖被打挂(风控、短信等未做隔离);③ 发布变更引入故障。所以必须提前做全链路压测 + 依赖隔离 + 大促变更冻结。

【评分标准】

档位答案特征
60 分能说出“加缓存、加机器、限流”
80 分能按接入层/应用层/数据层分层展开,并点出“瓶颈在 DB”
95 分主动估容量并指出“加机器解决不了 DB 瓶颈”;给出限流阈值依据与降级预案;提到全链路压测与变更冻结;能说出缓存命中率下降的连锁后果

【关联题】

  • 同簇: 第 3 题(限流设计)、第 14 题(吞吐优化)、第 21 题(性能优化方法论)、第 22 题(降级兜底)、第 10 题(全链路压测)
  • 下游延伸: 第 17 题(多级缓存)、第 18 题(弹性伸缩)、第 15 题(读多写少架构)
  • 国企 / 金融版: 第 209 题(高可用登录系统)、第 217 题(金融级服务降级)

【自测】

  1. 5 万 QPS、单机 2000,加 25 台机器就够了吗?为什么? 参考答案: 不够。加机器只解决应用层,MySQL 单机写通常只有几千 TPS,会成为新瓶颈。必须先做缓存 + 异步削峰。
  2. 限流阈值设成容量的 100% 有什么问题? 参考答案: 没有缓冲。流量接近容量时 RT 会急剧上升(排队),实际有效容量低于理论容量,容易雪崩。应设在 80% 左右。
  3. 缓存命中率从 95% 掉到 70%,系统会怎样? 参考答案: 未命中请求全部打到 DB,DB 压力放大约 6 倍,很可能直接被打挂。需立即排查(大 key 集中过期、热 key、缓存集群故障)。

2. 明星官宣热搜,流量瞬间涨 10 倍(流量突增保障) ​

【考察内容】高可用与弹性设计思维

【题目】某明星突然官宣恋情,你们 App 的流量在 10 分钟内暴涨 10 倍,监控开始告警,部分接口已出现超时。作为负责人,此刻你如何保证系统不挂、用户还能正常用?

【参考答案】突发流量按“事前-事中-事后”,事中核心是限流→降级→扩容→削峰:

  1. 容量估算(步进):假设日常峰值 2 万 QPS,10 分钟涨 10 倍 → 约 20 万 QPS,持续约 10–20 分钟。现有容量按 3 万计,缺口约 6–7 倍。扩容从触发到接入流量要 约 3–5 分钟(容器 30s–2min+JVM 预热 1–3min,与本题【关键数字】同口径;重镜像/需预热缓存另说),期间必须靠限流止血;单实例扛 2000 则理论要 100 台,显然不现实,只能“限流 + 非核心降级 + 部分扩容”组合。
  2. 事中四步:① 网关/Nginx 先限流(阈值 = 当前容量 80%),超额返回 429;② 降级非核心(推荐/日志/通知)返回兜底,把资源留给浏览与交易;③ K8s HPA 自动扩容(CPU>60% 触发,注意镜像预热与注册耗时);④ 可延迟写入 MQ 削峰,保护 DB。
  3. 热点应对:突发往往集中在少数热搜 key → 本地缓存 + Redis 多副本打散,避免单节点被打满。
  4. 失败与降级:限流误杀可动态调阈值;扩容不及 → 核心接口优先、非核心全关;Redis 被热点打挂 → 本地缓存兜底 + 网关级静态响应;事后复盘 QPS/RT/错误率,把限流规则、扩容策略、降级开关沉淀为预案。
  5. 口径:先保可用再谈体验,核心链路(浏览可降、下单支付必须保)资源再分配,不是一刀切拒绝。

【原理溯源】

  • 为什么“先保可用、再谈体验”是第一原则? 系统容量是有限的(线程池、连接数、CPU、DB 连接)。流量突然 10 倍,超过容量后请求开始排队 → RT 拉长 → 客户端超时重试 → 流量被进一步放大 → 连接池/线程池耗尽 → 全面雪崩。这个正反馈环是“从局部超时到全站不可用”的根因。因此第一时间要在入口砍掉超出容量的流量,牺牲部分请求的即时体验,保住系统整体存活。
  • 为什么限流要放在最前面,而不是先扩容? 扩容从触发到新实例接入流量,需要分钟级(拉镜像、启动、注册、预热)。在这几分钟里,超额流量已经把现有实例打穿了。限流是秒级生效的止血手段,扩容是分钟级生效的容量手段——应急场景必须先止血再扩容。
  • 为什么要“核心与非核心分流”而不是一刀切限流? 一刀切限流会把支付、下单也挡掉,业务直接损失。正确做法是:识别核心链路(浏览可降、下单支付必须保),对非核心接口主动降级/返回兜底数据,把腾出来的线程、连接、CPU 留给核心链路。本质是资源再分配,不是简单拒绝。
  • 为什么 MQ 能扛住突增? 同步调用下,请求速率超过下游处理速率时会直接超时。MQ 把“接收”和“处理”解耦:接收端只做校验+入队(极快),处理端按自己的节奏消费。峰值流量被缓冲在队列里,下游不被打穿。代价是用户从“同步拿到结果”变成“异步查询结果”,因此只适合可延迟的场景(下单受理、积分发放),不适合必须同步返回的查询。
  • 为什么热点数据本地缓存能救急? 突发流量往往集中在少数热点(热搜明星相关的几个接口/几个 key)。本地缓存(进程内 Caffeine)把读请求挡在应用内,RT 从 Redis 的 0.5–1ms 降到微秒级,同时把 Redis 集群的压力打散到每个应用实例——这是“用水平扩展的本地内存挡集中读”的经典手法。

【选型判断树】

流量突增现场,按顺序决策:
1. 现在超没超容量?
   └─ 已超时/错误率上升 → 立即限流(网关/Nginx,先挡最外层)
2. 挡完后核心链路还稳吗?
   ├─ 稳 → 观察扩容是否需要(HPA)
   └─ 不稳 → 降非核心(推荐/日志/通知)→ 再看核心
3. 压力主要打在哪一层?
   ├─ 热点读打满 Redis → 本地缓存 + key 多副本打散
   ├─ DB 连接/慢 SQL → DB 限流 + 读写分离 + 缓存挡读
   └─ 应用线程池满 → 拒绝策略改 MQ 兜底 + 扩容
4. 事后
   └─ 复盘瓶颈 → 沉淀限流阈值/扩容策略/降级开关为预案

口诀:先限流止血,再降级保核心,然后扩容,最后复盘沉淀。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“这是突发流量应急题,我会按事前/事中/事后三段来讲;事中核心是限流→降级→扩容→削峰四步,先保可用”
0:30–1:00定界假设:流量 10 分钟涨 10 倍,集中在少数热搜相关接口;当前水位按本题第 1 条口径为日常峰值 2 万 ÷ 现有容量 3 万 ≈ 67%
1:00–3:00事中四步① 网关/Nginx 限流挡超额;② 降级非核心(推荐/日志);③ HPA 自动扩容;④ MQ 削峰保护 DB。每步一句机制+一句代价
3:00–4:00事前事后事前:压测定水位、预置限流规则、缓存预热;事后:看 QPS/RT/错误率复盘,找出真正的瓶颈层
4:00–5:00收尾“一句话:突发流量的本质是容量与需求的时间差,限流消除雪崩正反馈,扩容消除容量缺口,降级保证核心不陪葬”

【关键数字】

参数经验值说明
限流阈值系统容量的 70%–80%留排队缓冲,避免打满后 RT 指数上升
扩容触发CPU > 60% 持续 1–2 分钟提前扩,不要等 90%
扩容冷启动容器 30s–2min;含 JVM 预热可达 3–5min预热不足会先扛一波慢请求
缓存命中率目标> 90%突发时掉到 80% 以下 DB 危险
排队超时用户侧 3–10s超过则直接失败+引导,避免“排死队”
降级开关生效秒级(配置中心推送)必须预置好,不能现场写代码
HPA 冷却时间扩容 0–1min,缩容 3–5min防抖动
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量

【追问链】(三层)

L1|“限流会不会把真实用户也挡掉?” → 会,但可控。三层手段减少误杀:① 用户分级——登录用户/会员阈值高于匿名;② 排队而不是直接拒绝——超阈值进入排队页,按处理能力放行;③ 热点预扣——秒杀场景先发资格 token,拿不到 token 的根本不进核心链路。目标是“挡掉的大部分是脚本和重试,不是正常用户”。

L2|“加机器之前请求已经打满,机器起来也接不住了怎么办?” → 所以应急顺序是“限流先把进来的流量降到现有容量以下 → 扩容 → 逐步放宽限流阈值”。限流阈值要支持动态调整(Sentinel/配置中心),扩容完成后把阈值从 80% 提到 90% 再观察。另外可以预热:大促/热点事件前,如果能预判(明星官宣其实很难预判),提前扩容+缓存预热。

L3|“流量涨了 10 倍,但限流后用户体验全变差,产品投诉怎么办?” → 要把“变差”设计得有梯度:① 非核心功能优雅降级(推荐位变静态榜单),用户几乎无感;② 核心功能排队透明化(进度条/预计时间),比直接 502 好;③ 对被限流的请求给明确文案+重试引导;④ 事后对被限流用户做补偿(优惠券)。同时用数据说明:如果不做这些,系统会 100% 不可用,现在是 70% 功能可用——这是权衡不是失职。

【评分标准】

档位答案特征
60 分能说出“限流、扩容、加缓存”三个词
80 分按事前/事中/事后展开,事中能排出限流→降级→扩容→削峰的顺序并说明为什么
95 分主动讲雪崩正反馈机制;指出限流必须先于扩容的原因(时间差);给出核心/非核心分流的资源再分配逻辑;能回答“限流误杀”与“扩容前打满”的具体解法

【关联题】

  • 同簇: 第 1 题(高并发接口设计总览)、第 3 题(限流设计细节)、第 22 题(降级兜底)、第 18 题(弹性伸缩)
  • 应急实战版: 第 179 题(凌晨 QPS 暴涨应急)
  • 对比参照: 第 6 题(秒杀是有预知的突增,本题是无预知的突增——预案深度不同)
  • 国企 / 金融版: 第 217 题(金融级服务降级)

【自测】

  1. 流量涨 10 倍,第一动作是扩容还是限流?为什么? 参考答案: 限流。扩容需要分钟级才能生效,限流秒级生效;不先限流,扩容完成前系统已经雪崩。
  2. 判断对错:突发流量时应该所有接口一起限流,保证公平。 参考答案: 错。应核心链路(下单/支付)高阈值保用,非核心(推荐/日志)降级或低阈值,把资源让给核心。
  3. MQ 削峰适合本题的哪类流量? 参考答案: 可异步受理的写流量(如下单受理、发券),不适合必须同步返回的查询流量。

3. 下单接口被突发流量打爆,单实例与集群都要兜住(限流设计) ​

【考察内容】限流算法原理与工程落地

【题目】秒杀活动开始瞬间,下单接口涌入远超容量的请求,单台机器先被打爆,接着集群整体雪崩。请设计一套方案:既能保护单实例不被突发流量打爆,又能让整个集群的总流量可控。

【参考答案】限流分“算法选型 + 单机保护 + 集群协同 + 多层落地 + 失败语义”:

  1. 容量估算(步进):假设秒杀下单峰值 10 万 QPS,系统真实容量(P99<200ms、错误率<0.1%)压测得 2 万 QPS → 限流阈值日常 0.7C=1.4 万、大促 0.8C=1.6 万;超限约 8 万 QPS 必须在入口快速失败,不能排队硬扛。
  2. 算法:固定窗口有临界突刺(窗口切换瞬间可通过 2 倍阈值);滑动窗口(Redis ZSet 按时间戳)平滑但有内存开销;漏桶出口恒定、保护第三方最彻底;令牌桶允许突发(桶内累积令牌),业务接口最常用。生产常用 Guava 令牌桶 + 集群滑动窗口。
  3. 落地三层:① Nginx limit_req 挡洪水(本地、零网络开销);② 应用单机 Guava 令牌桶自保;③ 集群总量用 Redis + Lua 原子(读-判-写必须原子,否则 GET+SET 并发下总流量超标);Sentinel 支持按用户/接口/热点参数维度并可热更新。
  4. 失败与降级:被限流返回 HTTP 429 + Retry-After,快速失败引导退避,禁止无限等待;Redis 限流组件故障 → 降级为单机限流(阈值下修为 1/N 防集群超量);阈值必须配置中心动态可调,误杀秒级纠正。
  5. 分层理由:单层限流或使 Redis 成为瓶颈,或流量已打进应用;漏斗式多层截留,下层压力更小。

【原理溯源】

  • 为什么固定窗口有“临界突刺”问题? 固定窗口按自然时间片(如每秒)计数。在窗口切换的瞬间(如 09:59:59.999 与 10:00:00.000 交界),前一窗口的配额和后一窗口的配额可以被连续用掉,短时间实际通过量是阈值的 2 倍。因此固定窗口只能用于精度要求不高的粗限流。
  • 为什么滑动窗口能消除突刺? 滑动窗口不按固定边界切分,而是维护最近 N 秒内的请求时间戳集合(Redis 常用 ZSet:score=时间戳)。判断时只统计“当前时刻往前 N 秒”的请求,窗口随时间连续滑动,不存在边界可以钻。代价是要存时间戳,内存开销与窗口内请求数成正比。
  • 为什么令牌桶允许突发而漏桶不允许? 令牌桶:系统以固定速率 r 向桶中放令牌,桶容量为 b。请求到达时先取一个令牌,取到才放行。若前期流量低,桶内会累积最多 b 个令牌,突发到来时可一次性消耗掉累积令牌——因此允许瞬时速率超过 r,只要长期平均不超过 r。漏桶:请求进入桶排队,桶以固定速率漏水(处理),出口速率恒定为 r,无论入口多猛。桶满则溢出丢弃。所以漏桶保护下游最彻底,但会“误杀”合法突发。
  • 为什么分布式限流必须原子? 多实例共享计数时,若用“GET 当前值 → 本地 +1 → SET 回去”,两个实例可能同时 GET 到相同值,都认为自己未超限而放行,集群总流量实际已超标。Redis 是单线程执行命令的,把“读-判断-写”放进 Lua 脚本,就能在 Redis 侧原子完成,这是分布式限流的正确做法。
  • 为什么要做多层限流而不是一层? 单层限流有两个问题:① 集中式(如只在网关用 Redis)时,Redis 本身可能成为瓶颈或单点;② 只在应用层限流时,流量已经打到了应用。分层设计:Nginx/网关用本地算法挡掉明显超额(低成本),集群层用 Redis 精确控制总量,业务层按用户/商品维度做细粒度(防单用户刷爆)。每层漏斗截留一部分,下层压力更小。

【选型判断树】

要限什么流?
├─ 突发可接受、平均有上限     → 令牌桶(Guava RateLimiter / Sentinel)
├─ 必须严格匀速(调第三方)   → 漏桶(Nginx limit_req)
└─ 要精确统计窗口内总量       → 滑动窗口(Redis ZSet)

限流范围?
├─ 单机自保                   → 进程内令牌桶(无网络开销)
├─ 集群总量                   → Redis + Lua(原子计数/取令牌)
└─ 多维度(用户/接口/热点参数)→ Sentinel 规则引擎,支持热更新

放在哪一层?
├─ 第一道:接入层 Nginx/网关(挡洪水)
├─ 第二道:应用集群层(控总量)
└─ 第三道:业务层(按用户/商品精细化)

口诀:突选用令牌,匀速用漏桶,计数用窗口;单机内存做,集群 Lua 扛,多层漏斗叠。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“限流题,我会分算法原理、单机保护、集群协同、多层落地四块来讲”
0:30–1:30算法对比四种算法各一句机制+一句适用/代价;重点讲清令牌桶为什么允许突发(累积令牌)、固定窗口为什么有临界突刺
1:30–3:00工程落地单机 Guava;集群 Redis+Lua 说明为什么必须原子;Sentinel 多维限流+热更新
3:00–4:00多层设计Nginx → 网关 → 业务层漏斗;限流后快速失败 429 + 重试引导,不无限等待
4:00–5:00收尾“限流的本质是在入口保证进入系统的速率不超过处理能力,从而切断‘排队→超时→重试→更堵’的雪崩环”

【关键数字】

参数经验值说明
令牌桶桶容量 b平均速率的 1–2 倍过小吸不住突发,过大等于没限
Guava RateLimiter数万 QPS(单机内存)无网络 RTT
Redis + Lua 限流数万–十几万 QPS受 Redis 单节点限制,可分片
Nginx limit_req与 Nginx 本身能力相当(数万 QPS)漏桶思想
限流阈值压测容量的 70%–80%留缓冲
被限流响应HTTP 429 + Retry-After快速失败,引导退避
滑动窗口 ZSet 过期2 × 窗口时长保证窗口完整+自动清理
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量

【追问链】(三层)

L1|“令牌桶和漏桶到底差在哪?” → 差在对突发的态度。令牌桶的桶里可以存令牌,突发时一次取走存量,所以允许瞬时超过平均速率;漏桶出口速率钉死,入口再猛也只能按 r 出,桶满丢弃。业务接口通常选令牌桶(用户集中点击是合理的),调用严格限速的第三方用漏桶。

L2|“分布式限流为什么不能 GET+SET,必须 Lua?” → 因为读-判-写不是原子的。两个实例同时读到 count=99(阈值 100),各自 +1 写回,都认为放行合法,集群实际进了 101 个。Lua 在 Redis 单线程里串行执行整个脚本,其他命令排队等待,天然原子。或者用 RedisCell 模块的 CL.THROTTLE。

L3|“限流阈值设多少合适?设低了误杀,设高压不住,怎么定?” → 用压测数据定,不用拍脑袋。步骤:① 全链路压测找出系统在 SLA 内(如 P99<200ms、错误率<0.1%)能扛的最大 QPS,记为 C;② 日常阈值设 0.7C,大促设 0.8C;③ 上线后监控限流触发率:常态下若触发率 >5% 说明阈值偏低或容量不足,若长期 0 触发可以适当上调;④ 阈值必须支持动态配置(Sentinel/配置中心),发现误杀秒级调整,不能重启服务。

【评分标准】

档位答案特征
60 分能列出令牌桶/漏桶/滑动窗口的名字
80 分能讲清令牌桶与漏桶的机制差异;知道分布式限流要用 Redis+Lua 保证原子
95 分能解释固定窗口临界突刺;说清多层限流漏斗的理由;给出阈值的压测定法与动态调整;知道限流后应返回 429+重试引导

【关联题】

  • 同簇: 第 2 题(流量突增应急)、第 1 题(高并发总览)、第 39 题(分布式限流计数)、第 22 题(降级)
  • 实现细节延伸: 第 8 题(熔断,限流的兄弟手段)
  • 热点特化: 第 13 题(热 Key,参数限流的极端情况)
  • 国企 / 金融版: 第 217 题(金融级限流降级)

【自测】

  1. 固定窗口限流在整点切换时有什么问题? 参考答案: 临界突刺——前后两个窗口的配额可在切换瞬间连续使用,短时通过量达阈值 2 倍。
  2. 令牌桶为什么允许突发?漏桶为什么不行? 参考答案: 令牌桶桶内可累积令牌,突发时一次消耗存量;漏桶出口速率恒定,桶满即丢。
  3. 用 Redis INCR 做分布式限流,不写 Lua 有什么隐患? 参考答案: INCR 只保证加一原子,“读值-判断-放行”仍是多步,并发下总流量会超限;必须 Lua 原子。

4. 领券接口被脚本秒光,真实用户领不到(防刷风控) ​

【考察内容】风控与安全工程思维

【题目】你们平台发优惠券,每次放券 1 秒内就被脚本抢光,真实用户永远领不到,运营怀疑有人用程序高频调用领券接口。你如何设计防护,让脚本失效、真实用户能正常领到?

【参考答案】防刷要“识别-拦截-降级”闭环,核心是提高脚本成本、保护真实用户:

  1. 容量估算(步进):假设领券活动放出 10 万张,真实用户峰值约 2 万 QPS,脚本流量可能放大到 10–20 万 QPS。系统真实领券容量按 1 万 TPS(DB 扣减)设计——注意本库全章 MySQL 口径是单机写 2000–5000 TPS,1 万 TPS 必须自带前提(分库/批量落库/Redis 预扣后异步写),否则该数字无出处;且真人 2 万 QPS + 脚本放大后即使挡掉 90% 仍有约 1.7 万打到这 1 万的库上,DB 照样挂、真人照样领不到——所以这道题的主路径必须是 Redis 原子预扣 + MQ 异步落库,「挡量」只是把入口压到预扣层,不是把量压到 DB,则至少 90%+ 流量必须在风控/限流层被挡掉,否则真实用户与脚本一起把库打挂。
  2. 分层防护:① 前端:验证码/滑块、按钮置灰、接口加密与时间戳签名,防简单脚本;② 接入层:IP/设备维度限流(如单 IP 每分钟 10 次),异常 UA 黑名单;③ 风控层:同一账号/设备/手机号聚集行为检测,频次+轨迹+图特征,命中人机验证或直接拒绝;④ 业务层:一人一券、资格预检前置到 Redis,库存不足快速失败。
  3. 技术手段:领券 URL 动态化(带签名随机路径);Token 预取校验;对高频 IP 段封禁或挑战;关键接口走独立集群,与主站隔离。
  4. 失败与降级:风控服务超时 → 降级为“放宽校验但收紧频次限流”,避免误杀真实用户导致活动冷场;验证码服务挂 → 切备用厂商或本地图形码;识别算法误杀率监控(人工申诉通道);活动结束后沉淀黑白名单与设备画像。
  5. 口径:目标不是 0 脚本,而是脚本拿不到、真实用户能拿到,用成本(验证+限流)抬高攻击收益比。

【原理溯源】

  • 为什么脚本能抢过真人? 脚本与真人在速度上差 3–5 个数量级:真人从打开页面到点按钮约 1–3 秒,脚本可以做到毫秒级并发调用,且可以起成百上千个线程/进程。只要接口是“先到先得、无身份门槛”,速度就是唯一竞争维度,真人必输。所以防护的本质是“引入真人擅长、脚本不擅长的维度”:人机识别(验证码)、身份成本(实名/手机)、行为模式(轨迹)。
  • 为什么只做 IP 限流会被打穿? 代理池/秒拨 IP 可以提供海量出口 IP,成本极低(几元可买数千 IP)。IP 维度的“唯一性”太弱。设备指纹(IMEI/IDFA/设备特征组合)成本更高,账号维度(注册+实名+绑手机)成本最高。防刷的强度与身份绑定的成本成正比——所以要多维度叠加,提高攻击者的综合成本。
  • 为什么“一人一单”比“限频”更根本? 限频是限制速度(每分钟 3 次),攻击者仍可以用 1000 个账号各刷 3 次。一人一单是限制资格:无论多快、多少次,每个用户最多领一张。配合资格预审(先报名/先抽签),可以把“拼速度”变成“拼资格”,从根本上消灭脚本的速度优势。
  • 为什么要 Token 预发(先发资格再领)? 如果领券接口本身公开可直接调用,脚本可以不经过前端。Token 预发把链路拆成两步:① 活动前/点击时获取带签名的 token(有频控、有有效期、与用户绑定);② 领券时校验 token。脚本即使知道领券 URL,没有合法 token 也调不通。这与秒杀 URL 动态化是同一思想:隐藏并保护真正的业务入口。
  • 为什么要审计日志和事后分析? 攻击手段会升级,任何静态策略都会被绕过。必须记录每次请求的用户/设备/IP/行为特征,事后用离线模型或规则回溯识别新形态的作弊,再把新规则推到线上。防刷是持续对抗,不是一次配置。

【选型判断树】

脚本刷接口,防护分四层(成本从低到高):
1. 频控层(立刻上)
   按用户 > 设备 > IP 顺序限频;IP 只是兜底维度
2. 人机层(核心)
   低价值接口 → 图形验证码(出错再弹)
   高价值接口 → 滑块/短信(强校验)
3. 资格层(根本)
   一人一单 + 预发 token + 服务端时间窗
4. 风控层(持续)
   设备指纹 + 行为序列 + 黑名单动态拉黑

口诀:频控挡速度,验证码挡机器,一人一单挡资格,风控持续升级。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“这是防刷风控题。脚本赢在速度,所以防护要引入速度以外的维度”
0:30–1:00定界假设:优惠券总额 10 万张,公开接口,无需登录也可刷(最坏情况)
1:00–3:30四层展开频控(用户/设备/IP)→ 人机(验证码/滑块)→ 资格(一人一单/token 预发)→ 风控(指纹/黑名单)。每层说清挡什么、代价是什么
3:30–4:30误伤与成本提主动风险:验证码误伤老年用户、IP 限流误伤公司 NAT;给分层阈值+放行通道
4:30–5:00收尾“防刷本质是提高攻击者成本、降低真实用户成本;没有一劳永逸,靠日志+对抗迭代”

【关键数字】

参数经验值说明
单用户领券频次1–5 次/分钟按业务定,过松无效
同 IP 请求频次10–30 次/分钟考虑企业 NAT,不能太严
验证码触发异常时才弹(非每次都弹)降低正常用户摩擦
token 有效期30s–5min短有效期防转发囤积
设备指纹采集项10–30 个特征过少易伪造,过多伤性能
黑名单响应实时/秒级生效用 Redis 存黑名单集合
风控规则更新日级/小时级对抗是持续的
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“只做 IP 限流行不行?” → 不行。代理池成本极低,攻击者可换海量 IP。IP 只能当兜底维度,主维度应该是用户 ID 和设备指纹。正确优先级:用户 > 设备 > IP。

L2|“误杀了正常用户怎么办?比如公司网出口一个 IP。” → 分层处理:① IP 阈值放宽(考虑 NAT),用户/设备维度收紧;② 正常用户被限后弹验证码放行,而不是直接拒绝;③ 白名单机制(内部员工、合作渠道);④ 监控误伤率(验证码通过率、领券成功率)持续调阈值。

L3|“脚本也可以自动过滑块验证码,怎么办?” → 打码平台确实能过,所以不能单点依赖验证码。组合拳:① 验证码只是提高成本的一环,不是终点;② 引入行为特征——滑动轨迹、点击间隔、页面停留时间,机器行为与人差异明显;③ 设备指纹+账号体系,过验证码也要有高成本账号;④ 一人一单从资格上消灭收益;⑤ 事后对账,把异常领券的券作废+拉黑。目标不是 100% 拦截,而是让攻击成本 > 收益。

【评分标准】

档位答案特征
60 分能说出“加验证码、加限流”
80 分能分层(频控/人机/资格/风控),知道 IP 限流会被代理池绕过
95 分能解释“脚本赢在速度所以要引入其他维度”;说出一人一单比限频更根本;能讨论误伤处理;知道验证码会被打码平台绕过并给出组合拳

【关联题】

  • 同簇: 第 19 题(防提前/防脚本)、第 3 题(限流)、第 4 题本身是第 134 题(优惠券系统)的安全侧
  • 系统设计: 第 134 题(优惠券系统全貌)、第 147 题(风控系统)
  • 对比参照: 第 6 题(秒杀场景的防刷是子问题)
  • 国企 / 金融版: 第 211 题(理财抢购,公平性与可审计)

【自测】

  1. 脚本能抢过真人的根本原因是什么? 参考答案: 速度差 3–5 个数量级,且无身份门槛时速度是唯一竞争维度。防护要引入人机/身份/资格等脚本不擅长的维度。
  2. 为什么“一人一单”比“每分钟限 3 次”更根本? 参考答案: 限频只限制速度,攻击者可用大量账号各刷几次;一人一单限制资格,无论多快每人最多一张,消灭速度优势。
  3. 判断对错:上了滑块验证码就高枕无忧了。 参考答案: 错。打码平台可过,需叠加设备指纹、行为特征、一人一单、事后对账。

5. 618 大促 100 件库存被扣成负数,订单超卖了几百单(防超卖) ​

【考察内容】并发控制与数据一致性

【题目】618 大促,某爆款商品库存只有 100 件,10 万人同时抢购。活动结束后运营发现:成交订单 523 单,库存被扣成了负数——超卖了。问题已经发生,接下来怎么止损?以后如何从根源上杜绝超卖?

【参考答案】防超卖核心是原子扣减 + 最终一致 + 异常回补:

  1. 容量估算(步进):库存 100 件,假设秒杀峰值 5 万 QPS 持续 5 秒 = 25 万请求,最终成交仅 100 单 → 99.96% 请求必须尽早失败。DB 条件更新(WHERE num>0)扣减约 2000–5000 TPS,若全部打到 DB 必然拖垮;因此 Redis 预扣先把无效流量挡掉,DB 只处理预扣成功的极少数。
  2. 扣减路径:① Redis DECR/Lua 原子预扣,返回库存<=0 直接失败;② 成功者发送 MQ,消费端 DB 条件更新 UPDATE stock SET num=num-1 WHERE id=? AND num>0,影响行数=0 则失败;③ 一人一单(唯一业务键)防止重复扣。
  3. 为什么不能只用 Redis:Redis 预扣与 DB 可能不一致(宕机丢单、网络分区),DB 是库存权威源,Redis 只是闸门。
  4. 失败与降级(题干第一问「已经发生怎么止损」要走这条):先止损——立刻关下单/支付入口或改排队页(分钟内,否则损失继续扩大),已超卖的按业务决定保留(认赔发货)或批量取消并补偿(退款+发券+致歉),不要为了账面好看直接改库存数;随后以 DB 为准回补 Redis 并跑对账。止损动作要预置成开关,不依赖定位完成。后续常态:支付超时关单 → 回补库存(DB + Redis 同步或消息补偿);Redis 挂 → 降级为 DB 直接条件更新 + 调低限流阈值;消息丢失/消费失败 → 定时对账任务比对“Redis 库存、DB 库存、成交订单数”,差异自动补偿或告警人工处理。
  5. 关键控制点:所有扣减/回补路径都要幂等;压测必须覆盖扣减、回补、超时关单、对账修复全路径,不能只测正向。

【原理溯源】

  • 为什么“先查再扣”会超卖? 经典竞态:线程 A 与 B 同时 SELECT num 都读到 1,各自判断“还有货”,各自执行 UPDATE SET num=num-1。由于 UPDATE 是对当前值操作,最终 num=0,但卖出了 2 件。根因是“读-判断-写”不是原子的,中间窗口被其他事务插入。条件更新 WHERE num>0 把判断下推到 UPDATE 语句内部,由行锁保证同一行的更新串行,第二个事务执行时 num 已是 0,条件不满足,影响行数为 0——从而原子地完成了“检查+扣减”。
  • 为什么 Redis DECR 能防超卖? Redis 单线程执行命令,DECR 是原子操作。若库存 1,两个请求 DECR,第一个得 0(成功),第二个得 -1(失败,应回滚或拒绝)。单线程串行执行消除了竞态。但 DECR 后可能短暂为负,业务上要用“结果 >=0 则成功”判断,并且必须有 DB 条件更新兜底——因为 Redis 可能丢数据(宕机/持久化失败),库存的权威源必须是 DB。
  • 为什么分布式锁正确但吞吐低? 分布式锁把并发扣减变成串行:同一时刻只有一个请求能执行扣减。正确性有了,但吞吐 = 1/锁持有时间。若一次扣减含 DB 写要 5ms,单锁吞吐上限约 200 TPS,远低于无锁的条件更新。所以分布式锁只适合“逻辑复杂、无法用条件表达”的超热点场景,能用条件更新/原子命令就别用锁。
  • 为什么 MQ 削峰能帮助防超卖? 超卖往往发生在流量峰值——瞬间 10 万请求把 DB 连接/线程打满,导致部分请求重试或异常路径执行了错误的扣减逻辑。MQ 把峰值流量缓冲下来,消费端按 DB 能承受的速率串行/低并发扣减,消除了“过载导致的异常路径”。注意:削峰解决的是压力问题,原子性仍要靠条件更新。
  • 为什么库存要以 DB 为准而不是 Redis? Redis 是内存存储,存在数据丢失风险(主从异步复制丢数据、持久化间隔丢数据)。库存是资金相关的核心资产,权威数据源必须是支持持久化与事务的 DB。Redis 只做预扣的“快速闸门”,真正确认订单时要再走一次 DB 条件更新;两者不一致时,以 DB 为准,通过定时对账把 Redis 校正。

【选型判断树】

库存扣减怎么选?
├─ 并发不高(< 几百 TPS)
│   └─ 直接 DB 条件更新(UPDATE ... WHERE num>0),最简单正确
├─ 并发高(秒杀级,万级请求)
│   ├─ Redis DECR/Lua 预扣(挡掉 99% 无效请求)
│   └─ 成功请求再走 DB 条件更新确认(权威扣减)
├─ 扣减逻辑复杂(组合优惠/多仓库存)
│   └─ 可考虑分布式锁串行化,或拆成多段库存
└─ 超热点单品(全站就一个 SKU)
    └─ 库存分片(num 拆成 N 份)+ 随机扣减 + 后台合并

口诀:低并发条件更新,高并发 Redis 预扣 + DB 兜底,超热点分片打散。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“防超卖的核心是扣减的原子性。我会先说根因,再给方案递进,最后讲止损”
0:30–1:30根因先查再扣的竞态:两事务读到相同库存 → 判断都有货 → 都更新成功。条件更新把判断下推到 SQL,行锁保证原子
1:30–3:00方案递进DB 条件更新(正确基准)→ Redis DECR 预扣(抗高并发)→ DB 兜底确认 → MQ 削峰 → 分布式锁(仅超热点)
3:00–4:00止损与兜底已超卖:关下单入口、按订单创建时间保留前 N 单、其余取消+补偿(券/道歉);对账任务修库存
4:00–5:00收尾“库存权威在 DB,Redis 只是闸门;原子性靠条件更新不靠锁;削峰解决的是过载,不是原子”

【关键数字】

参数经验值说明
Redis DECR单节点 10 万级 QPS原子命令,预扣主力
DB 条件更新单行更新约数千 TPS受行锁与连接池限制
分布式锁吞吐约 100–500 TPS取决于临界区时长
库存分片数10–100 片超热点打散,片间随机扣
对账周期分钟级~小时级只能当「纠偏」手段,不能当超卖的第一发现手段:题干要求「分钟内关入口」,靠小时级定时对账发现时窗口早已过去。第一发现必须是同步打点——预扣返回值 ≤0、UPDATE ... WHERE num>0 影响行数=0、或读到 num<0 立即告警并自动关闭入口(秒级);对账只做兜底核对与差异修复
超卖止损窗口分钟内必须关入口否则损失扩大
订单保留规则按支付时间/创建时间保留前 N需产品事先约定
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“Redis 和 DB 库存不一致怎么办?” → 以 DB 为准。Redis 只做预扣闸门,真正下单成功必须再执行 DB 条件更新。若 Redis 扣成功但 DB 失败,要回滚 Redis(INCR 回去)或靠过期。定时对账比对两侧数量,差异时校正 Redis 并告警。

L2|“为什么不用分布式锁一把梭?实现简单还正确。” → 吞吐太低。锁把并发变串行,TPS 上限 = 1/临界区时间。秒杀场景万级请求会大量排队超时。能用条件更新/原子命令表达的扣减,就不该用锁。锁只留给无法用条件表达的复杂逻辑。

L3|“DECR 后值变成 -1 了,是超卖吗?怎么处理?” → DECR 是原子的,出现 -1 说明前面已经有请求把 0 扣走了,这次是无效请求。正确处理:判断返回值,<0 则业务失败,并 INCR 回滚(或用 Lua 一步完成“若 >0 则减,否则失败”)。短暂的 -1 不是超卖,没有创建订单才是关键。超卖指的是“创建了超过库存的成交订单”。

【评分标准】

档位答案特征
60 分能说出“加锁”或“用 Redis”
80 分能写出条件更新 SQL 并解释行锁原子性;知道 Redis 预扣 + DB 兜底
95 分能讲清先查再扣的竞态根因;区分条件更新与乐观锁;知道分布式锁的吞吐代价与适用边界;能设计超卖后的止损与对账

【关联题】

  • 同簇: 第 6 题(秒杀全链路,本题是其中库存子问题)、第 36 题(分布式锁)、第 72 题(库存表设计)
  • 机制延伸: 第 67 题(防重复插入)、第 61 题(事务隔离级别,竞态的理论基础)
  • 对比参照: 第 162 题(计数系统,类似的高并发原子计数)
  • 国企 / 金融版: 第 211 题(理财抢购不超卖)

【自测】

  1. 为什么“先 SELECT 再 UPDATE”会超卖? 参考答案: 读-判-写非原子,并发下多个事务读到相同库存,各自判断有货后都更新成功。
  2. 防超卖的正确 SQL 是什么?它是乐观锁吗? 参考答案: UPDATE stock SET num=num-1 WHERE id=? AND num>0。不是乐观锁;乐观锁要用 version 字段比对。
  3. Redis 扣库存时 DECR 后得到 -1,业务上算超卖吗? 参考答案: 不算,只要没创建订单。应判 <0 为失败并回滚,或用 Lua 原子完成判断+扣减。

6. 百万用户同时抢 1 万件商品,系统要全链路扛住(秒杀系统设计) ​

【考察内容】秒杀是系统设计第一高频题,几乎必考

【题目】平台要做一次秒杀:1 万件商品,预计百万用户同时抢。要求:页面不能崩、抢购结果必须准确(不能超卖也不能少卖)、非秒杀业务不受影响。请给出从用户点击到订单落库的完整架构设计。

【参考答案】秒杀全链路按“层层削峰 + 最终准确”:

  1. 容量估算(步进):100 万用户 × 点击约 1–2 次 → 峰值约 200 万请求/分钟量级(秒级可达数十万 QPS),库存仅 1 万 → 99% 请求注定失败。网关不必放行百万,限流可设 5–10 万(略高于真实抢购吞吐)——放行量之后要接着给实例数与预扣层分片数:按本库单机 2000 QPS 口径,5–10 万放行量对应应用层 25–50 台;Redis 预扣按单实例约 10 万 ops 估,10 万级 QPS 至少 1–2 个分片再加读副本,热点 key 还要按 stock:{tid}:{0~N} 拆段;只说「网关挡量」而不接这两笔数,等于把压力原封不动挪到下一层;Redis 预扣 10 万级 QPS 挡掉绝大多数;MQ 按 DB 写入能力 几千 TPS 消费,把到达 DB 的流量衰减 2–3 个数量级。
  2. 五层设计:前端 CDN+静态页+按钮置灰+防提前;接入层网关限流+动态签名 URL+Token;应用层 Redis 预扣+MQ 异步(用户先收“排队中”);数据层条件更新扣库存+订单分库分表+对账;保障层秒杀服务独立部署(独立集群/缓存/连接池,隔离爆炸半径)。
  3. 结果准确:不超卖靠原子扣减(Redis DECR 判结果 + DB WHERE num>0)+ 一人一单;不少卖靠超时关单回补、支付取消回补、对账修复。
  4. 失败与降级:Redis 挂 → DB 条件更新+调低限流+库存快照粗过滤;消息积压 → 扩消费者+批量落库,超时订单自动关单回补;对账发现 Redis/DB/订单三方不一致 → 补偿或人工;大促关闭推荐等非核心,监控 QPS/库存/积压/失败率。
  5. 收尾:本质是用异步与缓存把百万流量衰减到 DB 能处理的量,用原子扣减+对账保证“不多卖不少卖”。

【原理溯源】

  • 为什么要“层层削峰”而不是一层扛住? 百万请求瞬间涌入,任何单层都会被打穿:网关扛不住百万连接,应用扛不住百万线程,Redis/DB 更不行。削峰的逻辑是每层漏斗截留一部分无效流量:CDN 挡静态资源请求,网关限流挡超额,Redis 预扣挡“无库存的请求”(1 万库存,百万请求,99% 在 Redis 层就能失败返回),MQ 缓冲剩下的有效请求按 DB 能力消费。最终到达 DB 的可能只有几千 TPS。
  • 为什么秒杀 URL 要动态化? 若秒杀接口 URL 固定公开,脚本可以不点页面直接高频调用。动态 URL(活动开始前才下发带签名的随机路径)把“知道地址”变成门槛,配合 token 与服务端时间校验,脚本必须先走正常入口获取资格——把攻击面从“开放接口”收窄为“带凭证的业务流程”。
  • 为什么用户先收到“排队中”而不是同步下单结果? 同步路径下,用户等待时间 = 全链路耗时,峰值时排队+重试会把连接/线程占满,引发雪崩。异步受理:接口只做校验+入队(毫秒级返回),用户轮询/推送拿结果。把“用户等待”和“系统处理”解耦,系统可以按自己的节奏处理,用户体验从“转圈”变成“可查询的排队”。
  • 为什么必须独立部署秒杀服务? 秒杀流量是平时的百倍,若与主站共用应用集群、缓存、DB 连接池,秒杀会耗尽共享资源,主站其他业务跟着挂——这违背“非秒杀业务不受影响”的要求。独立部署+独立限流+独立缓存,把爆炸半径限制在秒杀域内。
  • 为什么要有对账任务? 异步链路多(Redis 预扣 → MQ → DB),每一环都可能失败:MQ 丢消息、消费者失败、Redis 宕机丢预扣。对账定时比对“Redis 库存、DB 库存、已成交订单数”,发现差异自动补偿或告警人工介入。异步系统的最终一致靠对账兜底,不靠单点可靠。

【选型判断树】

秒杀链路分五段,每段一个主要手段:
前端     → CDN + 静态化 + 按钮置灰(挡无效请求、防脚本)
接入层   → 网关限流 + 动态 URL + token(挡洪水、挡脚本)
应用层   → Redis 预扣 + MQ 异步(挡无库存、削峰)
数据层   → DB 条件更新 + 分库分表 + 对账(保准确、保容量)
保障层   → 独立部署 + 压测 + 降级(保隔离、保预案)

库存扣减选型见第 5 题判断树。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“秒杀系统设计,我会按用户点击到落库的全链路来讲,核心思想是层层削峰+最终准确”
0:30–1:00定界100 万请求、1 万库存,意味着 99% 请求注定失败——要在尽量早的层失败
1:00–3:30五层展开前端→接入→应用(Redis+MQ)→数据(DB+对账)→保障(隔离+压测)。每层讲手段+为什么
3:30–4:30风险兜底Redis 挂了怎么办(DB 直接限流+条件更新);消息积压怎么办(扩消费者);对账怎么做
4:30–5:00收尾“秒杀的本质是用异步和缓存把百万流量衰减成 DB 能处理的量,用原子扣减+对账保证结果准确”

【关键数字】

参数经验值说明
网关限流略高于库存的若干倍(如 5–10 万)不必放行百万
Redis 预扣 QPS10 万级挡掉绝大多数无效请求
MQ 消费速率按 DB 写入能力定(几千 TPS)削峰核心参数
用户轮询间隔0.5–2s避免轮询自己成洪峰
排队超时30s–2min超时给明确结果
库存对账分钟级~准实时发现不一致
静态化页面 CDN承载 90%+ 浏览流量详情页不回源
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“Redis 挂了怎么办?秒杀还能进行吗?” → 能,降级方案:① 库存以 DB 条件更新为准直接扣(限流阈值调低保护 DB);② 本地缓存缓存库存快照做粗过滤;③ 简化链路关闭非核心。关键是预演过 Redis 故障预案,而不是现场手忙脚乱。

L2|“消息积压了几十万怎么办?” → ① 先确认是生产太快还是消费太慢;② 消费端扩容(增加实例,注意分区数限制);③ 消费逻辑优化(批量写库、异步落库);④ 若是临时故障导致,恢复后允许短时积压慢慢消化;⑤ 业务上可对超时未处理的订单自动关单+回补库存,避免用户一直等。

L3|“怎么保证既不超卖也不少卖?” → 不超卖:原子扣减(Redis DECR 判结果 + DB WHERE num>0)+ 一人一单。不少卖:① 预扣失败要回滚(用户放弃支付时库存要还回去);② 支付超时关单自动回补库存;③ 对账发现“库存剩余但无人下单”要检查是否回滚逻辑有 bug;④ 压测验证扣减+回补+超时关单全路径。

【评分标准】

档位答案特征
60 分能说出 Redis、MQ、限流几个名词
80 分能按前端/接入/应用/数据/保障分层展开,知道 Redis 预扣+MQ 削峰
95 分能解释“99% 请求要尽早失败”的削峰逻辑;说出独立部署隔离的理由;设计对账与回补;能处理 Redis 挂/消息积压/少卖三类追问

【关联题】

  • 同簇: 第 5 题(防超卖,本题库存子问题)、第 3 题(限流)、第 4 题(防刷)、第 19 题(防提前)、第 2 题(流量突增)
  • 系统设计完整版: 第 134 题(优惠券系统)、第 157 题(预约资格)、第 211 题(金融抢购)
  • 一致性延伸: 第 20 题(全链路一致性)

【自测】

  1. 为什么 99% 的无效请求要尽量在 Redis 层失败? 参考答案: 层层削峰,越早失败成本越低;Redis 单节点 10 万 QPS,能把百万请求衰减到 DB 能处理的量级。
  2. 秒杀服务为什么要独立部署? 参考答案: 隔离爆炸半径,防止秒杀洪峰耗尽共享的连接池/线程/缓存,导致主站其他业务挂掉。
  3. 用户收到“排队中”后,如何保证最终结果准确? 参考答案: 异步消费落库用原子扣减;支付超时关单回补库存;定时对账修复差异。

7. 核心链路要求 99.99% 可用,全年停机不许超过 1 小时(高可用设计) ​

【考察内容】高可用设计体系

【题目】公司核心交易链路要求可用性达到 99.99%(全年不可用时间不超过约 53 分钟)。目前系统是单机部署、数据库单点。你如何改造架构,让任何一台机器、一个组件挂掉都不影响核心交易?

【参考答案】99.99% 可用 ≈ 全年停机 ≤ 52.6 分钟(按 365×24×60×0.01%),需按错误预算设计:

  1. 容量与冗余估算(步进):核心链路假设峰值 2 万 QPS,单实例扛 2000 → 至少 N+2 冗余(常态 10+2,故障一台仍满足 SLA);多机房部署时同城双活,单机房故障可承担 100% 流量;存储层主从+自动切换,RTO 分钟级、RPO 接近 0(半同步)——但题干要求「任一组件挂都不影响核心交易」,分钟级 RTO 达不到该标准(99.99% 只有 52.6 分钟/年,按 DB 主从切换 10–60 秒算仅够 5–50 次)。两种达标路线要分开说:① 连接层自愈(客户端 1s 内重连 + 读写分离快速摘除故障实例)把单实例故障影响压到秒级,这才是「不影响」;② 机房级切换本就是分钟级 RTO,只能承诺「影响可控」而非「不影响」——答题时先把「不影响」定义清楚(是否允许秒级抖动、是否需人工介入)。
  2. 高可用四板斧:① 冗余:应用无状态水平扩展,存储主从/集群,避免单点;② 隔离:线程池/连接池隔离,核心与非核心拆分部署;③ 弹性:限流熔断降级 + HPA,过载时保核心;④ 变更可控:灰度、金丝雀、一键回滚,变更仍是最大故障源。
  3. 故障应对:依赖故障 → 熔断(快速失败)+ 兜底数据;单机房断网 → 流量切换预案与演练;DB 主库挂 → 切换从库,业务侧读写降级。
  4. 失败与降级:没有 100% 可用,要把预算花在刀刃上——监控错误预算消耗速度,超速则冻结非必要变更;非核心接口可牺牲体验保核心交易;定期故障演练(Chaos)验证预案是否真可用。
  5. 口径:高可用不是“从不挂”,而是挂了影响可控、可检测、可恢复,并用错误预算管理团队投入。

【原理溯源】

  • 为什么高可用的第一性原理是“消灭单点”? 单点意味着“该组件挂 = 系统挂”,可用性上限就是该组件的可用性。应用单机 99.9%,系统最多 99.9%。多实例让任一实例挂掉时其他实例接管,系统可用性变成“全部实例同时挂”的概率,远高于单机。这就是冗余的数学本质:并联提高可靠性。
  • 为什么无状态化是前提? 有状态(Session 在本地内存)时,请求必须回到原实例,实例挂掉后该用户的会话丢失,负载均衡器无法随意切换。无状态(Session 外置到 Redis,或用 Token)后任意实例都能处理任意请求,才能做到任意替换、随意扩容、故障秒级切换。这是水平扩展和高可用共同的前提。
  • 为什么数据库是高可用改造最难的一环? 应用无状态可随意加机器,数据库有状态——数据不能丢、写不能分叉。主从复制解决读扩展和故障切换,但引入主从延迟(从库可能落后主库数秒)和切换一致性(主挂时未同步的写可能丢失)。半同步复制(至少一个从库确认收到 binlog)降低丢数据概率,但增加写延迟。组复制/Paxos(MySQL Group Replication、TiDB)更强一致但更复杂。数据库高可用是“一致性、延迟、复杂度”的三角取舍。
  • 为什么必须有熔断和隔离,光有冗余不够? 冗余解决“自己挂”,不解决“依赖挂”。下游服务故障时,上游若同步等待会耗尽线程池,故障沿调用链向上传染(雪崩)。熔断在错误率超阈值时快速失败,不再调用故障依赖;隔离给每个依赖独立线程池,一个依赖耗尽不影响其他。冗余是提高自身存活,熔断隔离是防止被别人拖死——两者缺一不可。
  • 为什么要有混沌工程/故障演练? 高可用架构的价值只在故障时体现,但生产很少主动验证。若不演练,切换脚本可能是坏的、监控可能是盲的、预案可能没人会执行。故障演练主动注入故障(杀实例、断网、模拟延迟),在可控环境验证高可用是否真的成立。没有演练的高可用只是“纸面架构”。

【选型判断树】

可用性目标怎么达成?
├─ 先算账:99.99% = 全年约 53 分钟
│   含发布、故障、演练窗口——发布也要算进去
├─ 消灭单点(应用)
│   ├─ 无状态化 → 多实例 + LB
│   └─ 跨可用区部署(不要全放一个机架)
├─ 数据高可用
│   ├─ 读多 → 主从 + 读写分离(注意延迟)
│   ├─ 写要高可用 → 半同步/组复制 + 自动切换
│   └─ 极致 → 同城双活 / 异地多活(成本高)
├─ 防止被依赖拖死
│   └─ 超时 + 熔断 + 隔离 + 降级
└─ 验证
    └─ 监控告警 + 故障演练 + 预案 SOP

口诀:无状态多副本消灭单点,主从切换保数据,熔断隔离防传染,演练验证真高可用。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“高可用设计题。核心是消灭单点、自动切换、防止故障扩散、可验证”
0:30–1:00定界99.99% ≈ 53 分钟/年,包含计划内发布——所以发布策略也在范围内
1:00–3:30四块展开冗余与无状态;故障转移(应用/DB/Redis);自我保护(限流熔断隔离);数据可靠性
3:30–4:30验证与预案监控三件套;混沌工程;把预案写成 SOP 并演练
4:30–5:00收尾“高可用不是‘多部署几台’,而是任一组件挂掉时系统行为可预期、可自动恢复”

【关键数字】

参数经验值说明
99.9%约 8.76 小时/年可接受小时级故障
99.99%约 53 分钟/年核心交易常见目标
99.999%约 5 分钟/年成本急剧上升,慎提
主从切换时间10s–1min(自动)MHA/Orchestrator/云 RDS
半同步延迟增加约 1–10ms比异步复制略慢
健康检查失败判定3–5 次 × 间隔避免网络抖动误摘
故障演练频率至少季度一次验证预案有效性
多实例最小规模每服务 ≥2,建议 ≥32 台时一台挂只剩 1 台
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“只提多部署几台,算高可用吗?” → 不够。还要回答:① 实例是否无状态(否则挂了会丢会话);② 是否跨故障域(跨机架/AZ);③ 挂了之后流量是否自动切换(健康检查+自动摘除);④ 数据是否也有冗余和切换。应用只是高可用的一层。

L2|“数据库主挂了,从库数据可能不是最新的,怎么办?” → 这是复制延迟问题。方案按一致性要求选:① 异步复制:快但可能丢最后几秒写;② 半同步:至少一个从库收到 binlog 才提交,丢数据概率大降,写延迟略增;③ 组复制/共识:强一致但复杂。另外切换后应用要能处理短暂的复制中断,必要时降级只读或排队写。

L3|“如何证明高可用是真的,而不是 PPT 上的?” → 演练。定期注入故障:随机杀一个应用实例、模拟 Redis 主从切换、模拟 DB 主库不可用、模拟下游延迟。观察:业务错误率是否在可接受范围、切换是否在目标时间内完成、监控是否及时告警、预案是否可执行。把演练结果写入复盘,形成闭环。没有演练的高可用不可信。

【评分标准】

档位答案特征
60 分能说出“多部署、主从、负载均衡”
80 分正文四板斧说得全:冗余 / 隔离 / 弹性 / 变更可控,并能补上切换预案与可观测;知道无状态化是前提
95 分能解释主从延迟与切换一致性的取舍;区分冗余(防自己挂)与熔断隔离(防被拖死);强调故障演练;能算 99.99% 的时间账

【关联题】

  • 同簇: 第 8 题(雪崩防护,高可用的“防传染”部分)、第 2 题(流量应急)、第 22 题(降级)
  • 数据侧: 第 35 题(Redis 高可用集群)、第 78 题(备份恢复)、第 71 题(迁移)
  • 理论基础: 第 97 题(CAP)
  • 国企 / 金融版: 第 222 题(同城双活)、第 209 题(高可用登录系统)

【自测】

  1. 99.99% 一年最多停多久?这个时间要不要算上发布? 参考答案: 约 53 分钟。要算——所以需要滚动发布、蓝绿/灰度,把发布停机压到接近 0。
  2. 为什么无状态化是高可用和扩容的共同前提? 参考答案: 无状态后任意实例可处理任意请求,才能随意替换、扩容、故障切换;有状态则需要会话同步,切换和扩容都困难。
  3. 光有应用多实例,数据库单点,能到 99.99% 吗? 参考答案: 不能。数据库是单点,DB 挂则全挂,可用性上限就是 DB 的可用性。数据层必须主从/多副本+自动切换。

8. 一个下游服务挂了,整个系统跟着全瘫(雪崩防护) ​

【考察内容】微服务容错设计(故障隔离与快速失败)

【题目】微服务架构下,某天支付服务响应变慢,调用它的订单服务线程迅速被占满,订单又拖垮了上游网关……几分钟内整个系统全部不可用。这类“一个服务故障引发全链路瘫痪”如何从架构上预防?

【参考答案】雪崩防护 = 熔断 + 限流 + 降级 + 隔离 + 超时:

  1. 容量估算(步进):假设核心服务线程池 200,下游 RT 从 50ms 涨到 5s → 单请求占用线程时间放大 100 倍,有效 QPS 变为原来的 1/100。若入口仍放进原始流量,线程池秒级耗尽,自己也被拖死。因此下游超时必须短(如 200–500ms)且失败快速返回,防止连接与线程被慢下游“吸住”。
  2. 熔断:错误率/慢调用比例超过阈值(如 50%)自动熔断,直接快速失败,隔一段时间半开放行探测恢复;Sentinel/Hystrix/Resilience4j 均可。
  3. 隔离:线程池隔离(核心业务独立池)或信号量隔离;不同依赖独立连接池,避免一个下游耗尽共享资源。
  4. 降级:熔断后返回兜底数据(缓存快照/默认值/关闭非核心功能),保证主流程可走完;读多场景可本地缓存兜底。
  5. 失败与降级预案:依赖全挂时核心链路仍可用(付款依赖风控挂 → 降级为限额放行+异步补检);监控熔断触发率、下游 RT、线程池活跃数;预案要演练。本质:不让一个下游的失败沿调用链放大成全站不可用,快速失败优于慢速排队。

【原理溯源】

  • 为什么下游变慢会拖垮上游? 同步 RPC 下,上游线程会阻塞等待下游响应。下游变慢 → 上游线程等待时间变长 → 同样 QPS 需要更多并发线程 → 线程池打满 → 新请求排队 → 上游 RT 也上升 → 上游的上游也慢……故障沿调用链向上传染,这是雪崩的传播机制。关键点:不是下游“挂了”才会传染,“变慢”就够了,而且变慢比挂更隐蔽。
  • 为什么超时是第一道防线? 没有超时(或超时过长)时,一个慢下游能让上游线程无限期挂住,线程池迅速耗尽。合理超时(连接超时 + 读超时)保证单次调用占用线程的时间有上界,即使下游完全无响应,上游线程也会在超时后释放,去处理其他请求。超时是防止“线程被拖死”的基本手段。
  • 为什么熔断比单纯超时更进一步? 超时只限制单次调用时长,但若下游持续慢,大量请求仍然超时,错误率上升,下游被打得更惨(它还在处理这些注定超时的请求)。熔断在错误率/慢调用率超阈值后直接不再发起真实调用,快速失败返回,给下游喘息时间。半开状态定期放少量请求试探,恢复后自动闭合。
  • 为什么需要隔离? 若所有下游共用一个线程池,支付服务耗尽线程后,订单服务调用库存、积分等健康服务也没线程可用——一个依赖故障,全部依赖陪葬。线程池隔离给每个依赖独立的线程池,支付的池耗尽不影响库存调用。信号量隔离开销更低(不切换线程),但隔离粒度较粗。
  • 为什么重试会加剧雪崩? 下游已经过载时,上游重试相当于把 1 倍流量放大成 N 倍,让下游雪上加霜。正确做法:重试必须有上限次数、必须有退避间隔、只对幂等接口重试、只对可重试错误(网络超时,非业务错误)重试。无限制的盲目重试是雪崩的常见帮凶。

【选型判断树】

防止雪崩,按调用失败的阶段选手段:
调用前 → 超时设置(必须有,连接+读超时)
调用中 → 熔断(错误率/慢调用率超阈值快速失败)
调用后 → 降级返回兜底数据(缓存/默认值/空)
资源侧 → 线程池/信号量隔离(防单依赖耗尽全局)
入口侧 → 限流(保护自己不被上游打垮)
重试侧 → 有限次+退避+幂等(防止流量放大)

熔断器三态:
CLOSED(正常)→ 错误率超阈值 → OPEN(快速失败)
OPEN → 超时后 → HALF_OPEN(放少量试探)
HALF_OPEN → 试探成功 → CLOSED;试探失败 → OPEN

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“雪崩防护题。我会先讲雪崩怎么传播,再讲四板斧:超时、熔断、隔离、降级,最后讲重试陷阱”
0:30–1:30传播机制下游变慢→上游线程阻塞→池满→向上传染。强调“变慢就够了,不必挂”
1:30–3:30四板斧超时(有上界)→ 熔断(三态)→ 隔离(独立池)→ 降级(兜底数据)。每板斧一句机制+适用
3:30–4:30重试与限流重试放大流量的反面案例;有限重试+退避;入口限流自保
4:30–5:00收尾“雪崩的本质是故障沿调用链的正反馈;手段本质都是切断这个反馈”

【关键数字】

参数经验值说明
连接超时100ms–1s内网服务宜短
读超时按下游 P99 × 1.5–2过长会拖死线程池
熔断错误率阈值50% 左右(可调)业务可接受范围内
熔断打开时长5s–30s 后半开试探太短恢复不了,太长影响业务
重试次数1–2 次禁止无限重试
重试退避指数退避,如 100ms→200ms→400ms避免重试风暴
线程池隔离每个关键依赖独立池,核心数按依赖容量定舱壁模式
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“熔断和降级有什么区别?经常搞混。” → 熔断是被动切断:检测到下游故障后停止调用,快速失败,是保护机制。降级是主动砍功能:返回兜底数据(缓存/默认值/友好提示),是体验机制。通常熔断触发后要接降级——熔断了不能白屏,要给用户兜底内容。一句话:熔断管“调不调”,降级管“返回什么”。

L2|“熔断器的三种状态怎么转换?” → CLOSED:正常放行,统计错误率;错误率超阈值 → OPEN:直接快速失败,不发真实请求;OPEN 持续一段时间后 → HALF_OPEN:放少量请求试探下游;试探成功 → 回到 CLOSED;试探失败 → 回到 OPEN。这样下游恢复后能自动复联,又不会在下游未恢复时把它再次打垮。

L3|“我已经加了超时和熔断,为什么还会雪崩?” → 检查这几类残留问题:① 超时设置过长或没设连接超时,线程仍被拖住;② 熔断阈值太高或统计窗口太长,熔断前池已满;③ 所有依赖共用一个线程池,没隔离;④ 上游重试放大了流量;⑤ 非核心调用没降级,和核心调用抢资源;⑥ 网关/接入层没限流,入口流量无限。要系统检查整条链路的每一段,而不是只改一处。

【评分标准】

档位答案特征
60 分能说出“加超时、加熔断”
80 分能讲清雪崩传播机制;区分熔断与降级;知道隔离的作用
95 分能解释“变慢也会传染”;说清熔断三态转换;指出重试放大流量的陷阱;能系统列出“加了还会雪崩”的残留原因

【关联题】

  • 同簇: 第 7 题(高可用总览)、第 3 题(限流)、第 22 题(降级)、第 12 题(异步化解耦)
  • 实现延伸: 第 91 题(MQ 故障降级)、第 115 题(链路追踪,定位慢在哪)
  • 对比参照: 第 9 题(接口变慢排查,雪崩的前兆诊断)
  • 国企 / 金融版: 第 217 题(金融级降级)

【自测】

  1. 下游服务没挂,只是变慢,上游会不会雪崩?为什么? 参考答案: 会。上游线程阻塞等待,池满后排队,RT 上升向上传染。变慢比挂更隐蔽。
  2. 熔断和降级分别是什么?关系是什么? 参考答案: 熔断是被动切断调用(保护),降级是返回兜底数据(体验)。熔断触发后通常接降级。
  3. 为什么无限制重试会加剧雪崩? 参考答案: 下游已过载时,重试把流量放大 N 倍,让下游更惨。重试必须限次数、退避、幂等。

9. 详情接口 P99 从 50ms 涨到 2s,用户投诉变多(接口变慢排查) ​

【考察内容】线上问题定位方法论

【题目】线上监控显示:商品详情接口的 P99 延迟从 50ms 涨到了 2s,用户开始投诉页面转圈。你手上只有监控大盘,怎么沿着调用链一步步定位到底慢在哪一环,并完成优化?

【参考答案】接口变慢按“现象确认 → 分层排查 → 针对性处置”:

  1. 现象与容量背景(步进):P99 从 50ms 到 2s,放大 40 倍。先看是否流量同步上涨:若 QPS 未变而 RT 变慢,多半是依赖/资源问题;若 QPS 同步上涨,则是容量不足。估算:单实例线程 200,RT=2s 时理论吞吐 ≈ 200/2 = 100 QPS,而 RT=50ms 时可达 4000 QPS——RT 变长本身就是吞吐杀手,必须先止血。
  2. 排查顺序(分层):① 接入层:网关/Nginx 是否限流排队、TLS/网络;② 应用层:线程池是否打满、GC 是否频繁(Full GC 停顿)、日志同步写;③ 依赖层:下游 RT、超时、熔断状态、连接池等待;④ 数据层:慢 SQL、锁等待、主从延迟、连接池获取耗时。
  3. 常用工具:有链路追踪时 APM 看哪一跳变慢;题干说「手上只有监控大盘」,要先分清有没有 trace——没有就改分段计时(网关 RT/应用 RT/DB 慢日志/Redis RT 逐项相减),必要时临时开采样或用 Arthas trace 类.方法 看方法内耗时;SHOW PROCESSLIST/慢查询日志看 SQL;监控 CPU、线程池队列、GC、连接池 active/wait。
  4. 处置与失败降级:定位到慢 SQL → 紧急加索引/限流该接口;下游变慢 → 调短超时+熔断+降级兜底数据;GC 问题 → 先扩容/重启,再查对象泄漏;流量突增 → 限流+扩容。期间对用户侧可返回降级页或异步化。
  5. 口径:先回答“哪一层变慢、RT 被谁拉长”,再给优化;优化后用压测与灰度验证,避免“头痛医头”。

【原理溯源】

  • 为什么 P99 比平均值更重要? 平均值是全体请求的均值,少量极慢请求会被大量快请求稀释——平均 60ms 看起来正常,但 1% 的用户在等 2 秒。P99 表示“99% 的请求都比这个值快”,直接反映尾部用户体验。用户投诉往往来自这最慢的 1%。只看平均值会漏掉尾部劣化,这是监控的常见盲区。
  • 为什么要先确认“是单接口还是全局”? 若全接口都变慢,更可能是公共依赖(DB、Redis、网络、GC、宿主机)出问题;若只有详情接口慢,更可能是该接口特有的逻辑(慢 SQL、大 Key、下游详情服务)。这个判断决定了排查方向,能避免一上来就瞎查。同样,要对比发布变更时间点——很多问题是发布引入的。
  • 为什么 Full GC 会导致全局变慢? Full GC(或长时间的 Mixed GC)会触发 Stop-The-World,所有业务线程暂停,直到 GC 完成。若 Full GC 耗时 1–2 秒且频繁发生,这段时间内所有请求都卡住,P99 自然飙到秒级。判断方法:看 GC 停顿曲线与 RT 曲线是否时间相关——RT 高峰与 GC 停顿重合,基本可锁定 GC。
  • 为什么锁竞争会导致偶发慢而不是一直慢? 锁竞争只在冲突时阻塞。平时无冲突,RT 正常;流量升高或出现热点时,线程排队等锁,部分请求慢、部分请求快,表现为 P99/P999 恶化而平均值可能还好。用 jstack 多次采样,若多个线程 BLOCKED 在同一把锁上,即可确认。
  • 为什么要按“应用→中间件→DB→下游→机器”顺序查? 这是按“从最可控到最不可控、从最常见到最罕见”的经验排序。大部分慢是应用自身(慢 SQL、锁、GC)或中间件(Redis 大 Key、MQ 积压)引起;机器资源饱和相对少见。有顺序才不会东一榔头西一棒子。调用链追踪(SkyWalking)能直接显示每一跳的耗时,是定位的最快路径。

【选型判断树】

P99 变慢,按这个决策树排查:
1. 看监控形态
   ├─ 全局都慢 → 公共依赖:GC / DB / Redis / 网络 / 宿主机
   ├─ 单接口慢 → 该接口特有:慢 SQL / 大 Key / 下游服务
   └─ 与发布时间点重合 → 先回滚或 diff 发布内容
2. 看调用链(有链路追踪就直接看 SkyWalking 每一跳耗时;**但题干说「手上只有监控大盘」,要先分清有没有 trace**:没有 APM 时改用分段计时——网关 RT、应用 RT、依赖调用打点、DB 慢日志、Redis RTinfo 逐项相减找最大那一跳,必要时临时开采样或用 Arthas `trace 类.方法` 看方法内耗时分布;别默认自己有全链路追踪)。
   └─ 直接看到哪一跳耗时最长
3. 应用层
   ├─ GC 停顿与 RT 相关 → 查 GC 原因(堆、对象分配)
   ├─ 线程池满 → 查阻塞原因(锁/下游/IO)
   └─ 锁竞争 → jstack 看 BLOCKED
4. 中间件
   ├─ Redis 慢 → 大 Key / 热 Key / 持久化阻塞
   └─ MQ 积压 → 消费能力不足
5. 数据库
   └─ 慢日志 + EXPLAIN:全表扫描/索引失效/锁等待
6. 机器
   └─ CPU / 内存 / 磁盘 IO / 带宽

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“线上变慢排查题。我会按‘确认现象→分层定位→优化验证’的方法论来答”
0:30–1:00确认现象单接口还是全局?P99 还是均值?有没有对应发布/流量变化?
1:00–3:30分层定位应用(GC/线程池/锁)→ 中间件(Redis/MQ)→ DB(慢 SQL)→ 下游 → 机器。有 trace 就讲调用链,只有大盘就讲分段计时(先表态手上是什么工具,别说漏)
3:30–4:30优化手段对应根因给手段:SQL/索引、缓存、异步化、参数调整
4:30–5:00收尾“排查的本质是缩小范围:先分全局/单接口,再用调用链锁定环节,最后用工具确认根因”

【关键数字】

参数经验值说明
P99 目标业务定,详情页常见 100–300ms超过用户体感明显变差
Full GC 停顿通常 >100ms,频繁时秒级与 RT 曲线对照
慢 SQL 阈值>100ms 记录,>1s 必须处理按业务调整
Redis 慢命令>10ms 即警惕单线程会阻塞
大 keyString >10KB;集合元素 >5000需拆分
线程池打满活跃线程 = 最大线程数且队列增长查阻塞原因
连接池打满活跃连接接近 max查下游/慢 SQL
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“P99 和平均值有什么区别?为什么只看平均不行?” → 平均值会被大量快请求稀释,尾部慢请求看不出来。P99 表示 99% 请求都比它快,直接反映最差体验。用户投诉通常来自最慢的 1%,所以要盯 P99/P999。

L2|“怎么确认是 GC 导致的变慢?” → 看时间相关性:GC 监控的停顿时间曲线与接口 RT 曲线的高峰是否重合。再看 GC 日志:Full GC 频率、每次停顿时长、堆使用情况(是否接近满、是否存在内存泄漏导致对象晋升异常)。必要时用 jstat/jmap 或 GCEasy 分析。

L3|“调用链显示 DB 那一跳 1.5s,但 EXPLAIN 看起来走了索引,怎么回事?” → 走了索引不代表快。可能原因:① 索引区分度低,扫描行数仍然很多;② 回表太多(非覆盖索引);③ 锁等待(其他事务持有行锁);④ 连接池等待(不是执行慢,是等连接);⑤ 服务器 IO/网络问题。要再看 rows examined、是否 lock wait、连接池活跃数,不能只看 type=ref 就下结论。

【评分标准】

档位答案特征
60 分能说出“看日志、看监控、加缓存”
80 分有分层排查框架;知道 P99 含义;会用调用链(无 trace 时能改用分段计时)、慢日志、jstack 等工具
95 分能解释 GC/锁竞争导致尾部慢的机制;能处理“EXPLAIN 有索引仍慢”的追问;先确认全局/单接口再动手

【关联题】

  • 同簇: 第 21 题(性能优化方法论)、第 14 题(吞吐优化)、第 8 题(雪崩,变慢的严重后果)
  • 工具延伸: 第 51 题(慢 SQL)、第 52 题(索引失效)、第 30 题(大 Key)、第 13 题(热 Key)、第 115 题(链路追踪)、第 166 题(接口超时排查)
  • JVM: 第 181 题(内存上涨 GC 频繁)

【自测】

  1. 为什么只看平均 RT 可能漏掉问题? 参考答案: 平均值被大量快请求稀释,尾部慢请求被掩盖;P99/P999 才反映真实尾部体验。
  2. 全接口都变慢 vs 只有详情接口变慢,排查方向有何不同? 参考答案: 全局慢查公共依赖(GC/DB/Redis/网络);单接口慢查该接口特有逻辑(慢 SQL/大 Key/下游)。
  3. 调用链显示 DB 慢且走了索引,还可能是什么原因? 参考答案: 回表多、扫描行数大、锁等待、连接池等待、机器 IO/网络问题。

10. 大促前要证明系统扛得住峰值流量(全链路压测) ​

【考察内容】容量工程与性能测试方法

【题目】双 11 前一个月,老板要求“用压测证明系统扛得住峰值流量”。压测怎么组织?压测中会不会误伤线上真实用户?压测结果怎么解读、怎么推动整改?

【参考答案】全链路压测目标是用数据证明峰值容量,并找出瓶颈层:

  1. 容量估算(步进):大促目标峰值假设 5 万 QPS,按冗余 1.5–2 倍,压测要打到 7.5–10 万 QPS 并观察是否出现 RT 恶化/错误率上升。业务模型按线上比例:浏览:下单:支付 ≈ 100:10:1,不能只压浏览接口。判定标准示例:P99 < 200ms、错误率 < 0.1%、核心中间件 CPU < 70%。
  2. 方案要点:① 影子库/影子表/透传压测标,避免脏数据污染生产;② 流量从入口按真实链路打满,包含网关、缓存、MQ、DB、第三方 mock;③ 阶梯加压观察拐点(找到“容量悬崖”);④ 故障注入(杀实例、断网)验证降级预案。
  3. 必须压的场景:峰值突发、缓存失效后回源、依赖超时、限流触发、扩容后恢复。
  4. 失败与降级:压测可能误伤生产 → 必须在低峰/全链路隔离环境进行,准备一键停止;压测数据与线上不一致 → 模型校准;发现容量不足 → 先限流阈值下调保大促,再排期扩容/优化。
  5. 产出:瓶颈清单(哪一层先挂)、容量曲线(N 台机器 vs QPS)、限流阈值与扩容预案、未闭环风险项。压测不是走形式,要能回答“峰值挂在哪、余量还有多少”。

【原理溯源】

  • 为什么要全链路压测而不是单接口压测? 单接口压测只能证明“该接口在理想条件下能扛多少”。真实场景是多个接口并发、共享连接池/缓存/DB,瓶颈往往在共享资源(DB 连接池被多个接口耗尽、Redis 被多个热点打满)。全链路压测按真实流量比例组合施压,才能暴露这些共享瓶颈。这是“单点容量”与“系统容量”的区别。
  • 为什么压测流量必须打标隔离? 若压测流量与真实流量混在一起写同一个库,会产生脏数据(假订单、假库存扣减),污染生产数据。打标(请求头带压测标记)后,中间件识别标记,把压测流量路由到影子库/影子表,或者在写入时丢弃/隔离。这是“能在线上压测”的前提,否则只能在隔离环境压,又测不出生产真实问题。
  • 为什么要“逐步加压”而不是直接压到目标? 直接打到 10 倍流量,系统可能瞬间雪崩,你只能看到“挂了”,看不到“挂在哪个水位”。逐步加压(如 2000→5000→10000→20000)能观察:哪个 QPS 水位开始 RT 上升?哪个水位错误率抬头?哪个资源先饱和?找到拐点比知道极限更重要——生产要运行在拐点以下。
  • 压测结果怎么解读? 不是“能扛 X QPS”一句话,而是:① 拐点在哪(RT/错误率开始恶化的 QPS);② 先饱和的资源是什么(CPU?DB 连接?Redis 网络带宽?);③ 是否符合 SLA(P99 目标内);④ 容量冗余是否足够(建议峰值的 1.5–2 倍)。瓶颈资源决定优化方向:DB 饱和就缓存/读写分离/分库分表,应用 CPU 饱和就优化计算/扩容。
  • 为什么要推动整改闭环而不只是出报告? 压测的价值在“发现问题并修好”。流程:压测报告 → 列出瓶颈与风险 → 排优先级 → 优化 → 回归压测验证 → 沉淀基线。没有闭环的压测只是表演。还要把压测变成常态化(大促前必压、重大架构变更后必压),而不是一年一次。

【选型判断树】

压测怎么组织?
环境选择:
├─ 预发/隔离环境 → 安全,但与生产差异大(数据量、依赖)
└─ 生产环境     → 真实,但必须打标+影子库+随时可停(推荐大厂做法)

压测层次:
├─ 单机接口压测 → 验证单点能力、找代码级瓶颈
├─ 链路压测     → 验证某条业务链
└─ 全链路压测   → 验证系统容量、共享资源瓶颈(大促前必做)

加压策略:
└─ 逐步加压找拐点,不要直接打爆

结果驱动:
└─ 拐点 QPS × 0.7 = 限流阈值参考;先饱和资源 = 优化方向

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“容量工程题。我会按准备→执行→分析→风险控制→推动整改来讲”
0:30–1:00目标与模型明确峰值 QPS 与 SLA;按真实流量比例构造模型
1:00–3:00执行与隔离打标+影子库;逐步加压;监控全链路指标
3:00–4:00结果解读找拐点、找先饱和资源、算容量冗余
4:00–5:00整改闭环报告→优先级→优化→回归验证→沉淀基线

【关键数字】

参数经验值说明
容量冗余峰值的 1.5–2 倍应对模型偏差与突发
压测拐点RT 开始陡升的 QPS生产应运行在拐点 70% 以下
影子库隔离全链路打标,写入影子表防脏数据
压测终止条件错误率 >1% 或核心 RT 超 SLA 2 倍防止打爆生产
压测流量标记请求头 X-Pressure-Test: true中间件识别路由
大促前压测时间至少提前 1 个月留整改窗口
回归验证每次优化后重压同水位证明有效
应用并发估算QPS × RT决定线程池与实例数
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“压测会误伤线上真实用户吗?” → 若直接用生产数据不隔离,会(脏数据)。正确做法:压测流量打标,写入走影子库/影子表;读压测可以接受(只读无害);设置压测开关,异常立即终止。另外压测时间避开业务高峰,或用小流量逐步验证。

L2|“压测发现 DB 先扛不住,怎么办?” → 按读写分类处理:读多 → 加缓存挡读、读写分离、热点本地缓存;写多 → MQ 削峰、批量写、分库分表;SQL 本身慢 → 索引/EXPLAIN/改写。同时设置 DB 限流保护,避免压测把生产 DB 打挂。

L3|“压测数据好看,上线还是挂了,为什么?” → 常见差异原因:① 压测模型与真实流量比例不符(真实热点更集中);② 压测环境数据量小于生产(索引、缓存命中率不同);③ 压测时下游依赖是 mock 或低负载,真实下游更慢;④ 发布引入了新问题;⑤ 缓存冷启动(压测时热,上线时冷)。所以要全链路生产压测、用真实数据比例、灰度发布+实时监控。

【评分标准】

档位答案特征
60 分能说出“用 JMeter 压一下”
80 分知道全链路压测、影子库/打标隔离、逐步加压
95 分能解读拐点与先饱和资源;设计终止条件;讲清整改闭环;能分析“压测好上线挂”的原因

【关联题】

  • 同簇: 第 21 题(性能优化方法论)、第 1 题(容量评估)、第 173 题(大促保障)
  • 优化落地: 第 14 题(吞吐优化)、第 11 题(线程池)、第 55 题(分库分表)
  • 国企 / 金融版: 第 211 题(理财抢购压测)

【自测】

  1. 为什么压测流量要打标? 参考答案: 隔离压测写入,避免污染生产数据(假订单/假库存),是线上压测的前提。
  2. 逐步加压的目的是什么? 参考答案: 找到 RT/错误率开始恶化的拐点,观察哪个资源先饱和;直接打爆只能看到“挂了”。
  3. 压测通过就能保证大促不挂吗? 参考答案: 不能。还要模型真实比例、生产环境验证、整改闭环、变更冻结、实时监控与降级预案。

11. 任务接口高峰大量请求排队超时(线程池调优) ​

【考察内容】并发编程工程化

【题目】你们有个处理任务型请求的接口,高峰时大量请求排队、纷纷超时,低谷时线程又大量空闲。领导让你把线程池参数(核心线程数、最大线程数、队列长度、拒绝策略)调合理,你依据什么来定?

【参考答案】线程池排队超时按“参数-队列-拒绝策略-隔离”系统化处理:

  1. 容量估算(步进):假设高峰 QPS=2000,单请求处理耗时 100ms,则理论需要并发 ≈ QPS×RT = 2000×0.1 = 200 线程。若池只有 50,队列很快堆满。CPU 核数 8 时,计算密集型一般配 8–16 线程,IO 密集可放大到核数×5–10——但这只是起点不是上限:本条已算出需要 200 线程,8 核×5–10 只给 40–80,照公式配池仍会排队超时。max 要按压测放大到 ≥200,IO 型池的真正上限是下游连接数与下游 RT(池 80×100ms 只能出 800 QPS),不是核数倍数(再大上下文切换吃 CPU)。
  2. 问题根因:核心参数不合理(线程太少/队列过长)、慢任务占用线程、池未隔离(一个慢接口耗尽全局池)、拒绝策略不合理(CallerRuns 反而拖垮调用方)。
  3. 优化步骤:① 压测获取 RT 与目标 QPS,按 QPS×RT 定线程;② 有界队列(如 200–500),防止无限堆积 OOM;③ 拒绝策略选快速失败(429)或降级,不选无限等待;④ 核心/非核心、不同依赖分池隔离;⑤ 监控活跃线程、队列长度、拒绝次数、任务耗时。
  4. 失败与降级:队列积压 → 临时扩容+对该接口限流;慢下游 → 调短超时+熔断,避免线程被吸住;紧急时可下调阈值保核心。业务上排队超时要给用户明确失败语义,不能一直转圈。
  5. 口径:线程池不是越大越好,过大导致上下文切换与连接池连带打满;用容量公式与监控定参数,而不是拍脑袋。

【原理溯源】

  • 为什么 CPU 密集型线程数 ≈ 核数+1? CPU 密集时,线程大部分时间在计算,几乎不等待。线程数超过核数后,多出来的线程只能排队等 CPU,上下文切换本身成为额外开销,吞吐反而下降。+1 是为了当某线程因缺页等原因短暂暂停时,另一个线程能顶上,不让 CPU 空转。
  • 为什么 IO 密集型线程数要远大于核数? IO 密集时,线程大部分时间在等(网络、磁盘、下游响应),不占 CPU。若线程数 = 核数,大量线程在等待,CPU 空闲。增大线程数让 CPU 在部分线程等待时处理其他线程,提高利用率。经验公式:核数 × (1 + 等待时间/计算时间)。若阻塞系数 0.9(90% 时间在等),则线程数 ≈ 核数 × 10。
  • 为什么必须用有界队列? 无界队列(如 LinkedBlockingQueue 默认 Integer.MAX_VALUE)下,任务来多少存多少,最大线程数形同虚设——因为线程池的逻辑是“核心线程满 → 入队 → 队列满才扩到最大线程”。无界队列永远不会满,所以永远只用核心线程,多余任务无限堆积,最终 OOM。有界队列让“队列满”成为扩线程的触发条件,并在真正过载时触发拒绝策略,把过载显式暴露出来而不是默默吃内存。
  • 为什么队列长度 = 可接受排队延迟 × 处理速率? 队列太长:任务排队时间超过业务超时,用户已超时但任务还在队列里白白处理。队列太短:稍有波动就触发拒绝,误伤正常请求。设 L = 可接受额外延迟(如 500ms)× 每毫秒处理数,就能保证队列中的任务最坏等待时间不超过业务能接受的延迟。
  • 为什么生产禁止 Executors.newFixedThreadPool? 它用无界 LinkedBlockingQueue,任务堆积会 OOM;newCachedThreadPool 最大线程数是 Integer.MAX_VALUE,突发时会创建海量线程导致 OOM 或线程切换风暴。必须用 ThreadPoolExecutor 显式指定所有参数,让容量边界清晰、可控、可观测。

【选型判断树】

线程池参数怎么定?
第一步:判断任务类型
├─ CPU 密集(计算/序列化/压缩)→ core ≈ N+1,max ≈ N+1
├─ IO 密集(网络/DB/下游调用)→ core ≈ N×2,max ≈ N×(5~10) 只是**起点**:本条已算出需 200 线程,8 核×5–10 只给 40–80,最终 max 按压测放大到 ≥200,上限受下游连接数与下游 RT 约束
└─ 混合型 → 拆成两个池,或按实测等待比例估算

第二步:定队列
├─ 有界(必须)→ ArrayBlockingQueue
└─ 长度 = 可接受排队延迟 × 处理速率

第三步:定拒绝策略
├─ 不能丢、可异步 → 自定义:告警 + 入 MQ 重试
├─ 要反压、不能丢 → **仅限非请求路径的内部提交线程**用 CallerRunsPolicy(让提交者自己跑、降速);跑在 Tomcat/RPC 请求线程上时它会占用业务线程,过载期禁用(见正文根因),改落 MQ+快速失败
├─ 可丢弃(日志)→ DiscardOldest
└─ 快速失败 → AbortPolicy(默认)

禁用:Executors 快捷方法(无界队列/无界线程)

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“线程池调优题。我会按任务类型定线程数、按延迟定队列、按业务定拒绝策略”
0:30–1:30线程数CPU 密集 N+1 的原因(切换开销);IO 密集放大线程数的原因(等待不占 CPU);公式本质
1:30–3:00队列与拒绝无界队列的 OOM 陷阱;有界队列长度的估算;四种拒绝策略适用场景;推荐自定义策略
3:00–4:00生产规范禁用 Executors;监控队列长度与拒绝次数;压测校准
4:00–5:00收尾“线程池本质是资源池化:用有限线程服务无限请求,过载时必须显式拒绝而不是默默堆积”

【关键数字】

参数经验值说明
CPU 密集核心线程CPU 核数 + 1避免切换开销
IO 密集核心线程CPU 核数 × 2 起步按等待/计算比放大
IO 密集最大线程CPU 核数 × 5–10 为起点,按压测放大(本题需 ≥200)真正的上限是下游连接数×下游 RT 换算的吞吐,不是核数倍数
队列长度可接受延迟 × 处理速率如 500ms × 100/s = 50
拒绝告警必须有拒绝 = 过载信号
活跃线程监控接近 max 即告警打满前预警
队列积压监控持续增长即告警说明消费跟不上
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“线程池满了,是先扩线程还是先入队?为什么?” → 取决于队列类型。LinkedBlockingQueue(无界)先入队,永远不会扩到 max 线程——这是陷阱。SynchronousQueue(无容量)直接尝试扩线程,到 max 后拒绝。ArrayBlockingQueue(有界)先入队,队列满再扩线程,到 max 后拒绝。所以有界队列才能让 max 线程数真正生效。

L2|“CallerRunsPolicy 有什么问题?为什么说它能反压?” → 它让提交任务的线程自己执行任务,提交线程被占用,提交速度自然下降——这就是反压(背压)。优点是简单、不丢任务、自动减速。缺点:① 若提交者是 Tomcat 线程,会阻塞 HTTP 线程,拖慢接口;② 任务执行时间不可控。适合内部任务提交,不适合用户请求路径上的同步提交。

L3|“参数配好后上线还是超时,怎么继续查?” → 看监控定位:① 活跃线程是否达到 max 且队列增长 → 真过载,要么扩容要么优化任务本身;② 活跃线程远低于 max 但队列长 → 可能任务被锁或下游阻塞,查 jstack;③ 有拒绝告警 → 看拒绝是否合理,阈值是否过低;④ 任务本身变慢 → 查下游 RT、GC、锁竞争。参数是容量边界,超时根因往往在任务执行本身。

【评分标准】

档位答案特征
60 分能背出 CPU/IO 密集的线程数公式
80 分能解释公式背后的原理;知道无界队列陷阱;会选拒绝策略
95 分能解释 max 线程何时生效(有界队列);设计队列长度的延迟估算;推荐自定义拒绝策略;能处理“配好仍超时”的排查

【关联题】

  • 同簇: 第 169 题(线程池打满排查)、第 12 题(MQ 异步,另一种解耦方式)
  • 对比参照: 第 63 题(连接池打满,类似的池化问题)
  • 延伸: 第 8 题(隔离用的独立线程池)

【自测】

  1. 为什么 CPU 密集型线程数不是越多越好? 参考答案: 超过核数后线程排队等 CPU,上下文切换成为额外开销,吞吐反而下降。
  2. 无界队列有什么隐患? 参考答案: max 线程数永远不生效(队列永远不满),任务无限堆积最终 OOM;过载被默默掩盖。
  3. 拒绝策略选什么最合适? 参考答案: 按业务:不能丢且可异步→告警+入MQ;要反压且提交方不是业务请求线程→CallerRuns(过载期在请求路径上会拖垮调用方,正文已把它列为病根);可丢→DiscardOldest;快速失败→Abort。生产推荐自定义策略。

12. 下单链路同步调用 800ms,用户等得不耐烦(MQ 异步化) ​

【考察内容】异步化与解耦设计(同步/异步边界判断)

【题目】现在下单链路是同步串行:下单→扣库存→发短信→送积分,一次下单要 800ms,其中短信和积分要占 500ms,用户流失严重。你怎么改造这条链路?哪些步骤可以异步、怎么保证异步后不丢不错?

【参考答案】MQ 异步化的核心是把同步长链路拆成“快速受理 + 异步处理”:

  1. 容量估算(步进):下单同步 800ms,假设峰值 TPS=500,则应用并发占用 ≈ 500×0.8 = 400 线程;若异步化后受理接口只做校验+入队(RT=20ms),并发占用降到 500×0.02=10 线程,释放约 97% 线程资源。用户可感知 RT 从 800ms 降到 <50ms(返回“已受理”)。
  2. 拆分原则:适合异步的——积分、短信、通知、物流、部分库存/履约、对账;不适合——必须同步返回结果的支付扣款确认、用户立即要看的状态(可同步落库+异步后续)。
  3. 方案:入口事务内只落“订单受理记录 + 发消息”(或本地消息表保证发消息与业务同事务);消费者按 DB 能力限速处理;结果通过查询接口/推送告知用户。
  4. 失败与降级:MQ 挂 → 降级为同步处理并调低限流,或落本地表异步补发;消息丢失 → 本地消息表+对账补偿;重复消费 → 消费端幂等(唯一业务键);消息积压 → 扩消费者+批量处理,超时自动关单。
  5. 代价:体验从“同步确定”变成“受理中”,必须设计查询与超时策略;最终一致依赖对账与补偿。本质是用一致性换吞吐与稳定性。

【原理溯源】

  • 为什么短信/积分可以异步,扣库存不行? 判断标准是一致性强弱与业务容忍度。扣库存直接影响“能不能卖”,必须与订单强一致——异步扣库存会出现“用户下单成功但库存没扣=超卖”。短信/积分是“下单成功后的通知/权益”,晚几秒到达用户可接受,属于最终一致。异步化的边界 = 强一致操作与最终一致操作的分界。
  • 为什么异步化能降低 RT? 同步串行时,RT = 所有步骤耗时之和(800ms)。异步后主流程只做必须同步的部分(约 300ms),短信/积分移到后台。用户等待时间从“全链路”缩短到“关键路径”。注意:系统总工作量没变,只是把非关键路径从用户等待时间里挪走了。
  • 为什么异步化能削峰? 峰值时,短信服务可能跟不上。同步调用下,短信慢会把下单线程拖住,下单接口跟着慢。异步后,MQ 充当缓冲:下单接口快速入队返回,短信消费者按自己的节奏处理。把“接收速率”与“处理速率”解耦,峰值被缓冲在队列里。
  • 为什么异步化能故障隔离? 同步调用下,短信服务挂了会导致下单失败——非核心依赖拖垮核心业务。异步后,短信服务挂了只是消息积压,下单不受影响;短信恢复后继续消费。把非核心依赖的可用性从核心链路中剥离。
  • 为什么异步会引入“丢、重、乱”三个新问题? 同步调用的成败是即时可知的;异步经过 MQ,多了网络和存储环节:① 丢:生产者发了但 broker 没收到,或消费者还没处理就 ack 了;② 重:网络抖动导致重复投递;③ 乱:多分区/多消费者导致顺序打乱。因此必须:生产端 confirm、broker 持久化、消费端处理成功再 ack、消费端幂等、同订单消息进同分区。异步化的代价是把“同步的简单”换成“分布式的可靠”。

【选型判断树】

哪些步骤该异步?
判断标准:失败后能否补偿?用户能否接受延迟?
├─ 失败不可接受、必须立刻知道结果 → 同步
│   (扣库存、支付扣款、鉴权)
├─ 失败可补偿、用户可接受秒级延迟 → 异步
│   (发短信、送积分、发优惠券、更新统计、写日志)
└─ 失败可丢弃 → 异步 + 可丢
    (埋点、非关键通知)

异步可靠性三件套:
生产端 confirm → 不丢生产
消费端幂等   → 不重复生效
死信+对账    → 失败有兜底

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“异步化边界判断题。核心是分清哪些必须同步、哪些可异步”
0:30–1:30边界判断扣库存强一致必须同步;短信/积分最终一致可异步。给出判断标准
1:30–3:00改造方案主流程精简 + MQ 发事件 + 消费者异步处理;讲 RT、削峰、隔离三个收益
3:00–4:00可靠性引入的丢/重/乱问题;confirm、幂等、死信、对账四件套
4:00–5:00收尾“异步化本质是把非关键路径从用户等待中挪走,代价是必须自己保证消息可靠”

【关键数字】

参数经验值说明
改造前 RT800ms(含短信积分 500ms)用户流失临界约 1s
改造后主流程完成 RT<300ms只保留核心路径;受理接口 RT 见上文 <50ms,两者口径不同
MQ 确认机制生产 confirm + 消费手动 ack防丢消息
消费重试3–5 次后进死信防无限重试堵队列
幂等实现唯一索引 / 状态机 / Redis SETNX防重复
消息延迟容忍秒级短信积分可接受
死信告警必须有人工介入信号
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“能不能把扣库存也异步化?用户下单后后台慢慢扣。” → 不能轻易做。异步扣库存存在窗口:用户看到“下单成功”但库存还没扣,若此时库存不足,会出现“下单成功却无货”。除非业务接受这种可能性并有补偿(自动取消订单+通知),否则库存必须同步扣。这是强一致与最终一致的边界。

L2|“异步任务失败了怎么办?” → 分层处理:① 消费失败自动重试(限次数+退避);② 重试耗尽进死信队列,告警人工;③ 定时对账任务扫描“已下单但未发短信/未送积分”的记录,补发;④ 关键业务(如积分)要有业务状态机记录处理进度,可重入。

L3|“怎么保证消息不丢、不重复?” → 不丢:生产端开启 confirm(broker 确认后才算发送成功);broker 开持久化(多副本);消费端处理成功后再 ack,失败不 ack。不重复:MQ 只能保证 at-least-once,消费端必须幂等——用唯一业务键做唯一索引,或状态机判断“已处理则跳过”。一句话:不丢靠 ack 策略,不重靠业务幂等。

【评分标准】

档位答案特征
60 分能说出“把短信积分改成异步”
80 分能分清同步/异步边界并说明理由;知道 MQ 可靠性三件套
95 分能解释异步化对 RT/削峰/隔离的三个收益;指出扣库存不能轻易异步;设计死信+对账兜底;说清 at-least-once 与幂等的关系

【关联题】

  • 同簇: 第 80 题(消息不丢)、第 81 题(重复消费)、第 92/93 题(事务消息/本地消息表,业务与发消息的一致性)
  • 对比参照: 第 79 题(MQ 价值与代价)、第 83 题(消息积压)
  • 系统设计: 第 20 题(全链路一致性,本题是其中一环)

【自测】

  1. 判断标准是什么:一步操作该同步还是该异步? 参考答案: 看一致性强弱与失败能否补偿。强一致、失败不可补偿的必须同步;最终一致、可补偿的可异步。
  2. 异步化会引入哪三个新问题? 参考答案: 丢(消息丢失)、重(重复消费)、乱(顺序打乱)。
  3. 如何保证消息不丢? 参考答案: 生产 confirm + broker 持久化 + 消费端处理成功再 ack。

13. 某爆款商品的数据请求把 Redis 单节点打满(热 Key 治理) ​

【考察内容】Redis 性能治理实战

【题目】某商品突然上了热搜,它的数据在 Redis 里是同一个 key,所有请求都打到这一个节点上,该节点 CPU 打满、其他业务跟着遭殃。怎么快速缓解?长期怎么治理热 key?

【参考答案】热 Key 治理 = 发现、分散、就近、限流:

  1. 容量估算(步进):假设爆款商品详情读 QPS 峰值 10 万,集群 10 个 Redis 节点均匀假设每节点 1 万 QPS(单节点能力约 10 万,看似安全);但若 ****按题干的「所有请求都打这一个 key」估:10 万 QPS×1KB(1024B)≈1.024e8 B/s≈819Mbps(按 1MB=10^6B 折算约 102MB/s),千兆网卡 81% 且单线程满载;若按经验值「50% 流量集中在 1 个热 Key」则只有 41%、CPU 半载,解释不了题干说的打满——所以告警阈值要按集中比例分档(100% 集中 → 5 万 QPS/key 就该告警,50% 集中 → 阈值放宽一倍),则该节点要扛 5 万 QPS——按第 32 题口径单值约 1KB 算即 50MB/s ≈ 400Mbps(千兆网卡的 40%),叠加其他 key 的流量就会把网卡/CPU 余量吃光,产生连接抖动与全集群超时(若单值到 2KB 以上则是直接打满)。
  2. 发现:Redis HOTKEYS(生产慎用)、monitor/代理层统计、客户端埋点、云 Redis 热 key 分析;监控单分片 CPU/QPS 倾斜。
  3. 分散:① 本地缓存(Caffeine)挡最热数据,TTL 秒级;② Key 多副本:item:{id}:{random%N} 读时随机副本,写时广播失效;③ 拆结构:大 hash 拆分,避免单 key 过大。
  4. 就近与保护:数据尽量只读静态化/推到 CDN 或网关缓存;对热 key 单独限流;业务可排队/排队页。
  5. 失败与降级:本地缓存兜底陈旧数据可接受的场景;Redis 单节点过载 → 读从库/只读副本;极端时网关直接返回缓存快照。监控命中率与热分片负载。
  6. 口径:热 Key 本质是流量分布不均,解法不是简单加机器,而是把读请求打散并把最热部分挪到更靠近用户、成本更低的层。

【原理溯源】

  • 为什么热 Key 会打满单节点? Redis Cluster 按 key 的 hash 分片,一个 key 只会落在一个分片上。该 key 的所有请求都由这个节点处理,其他节点帮不上忙。若该 key QPS 达到 10 万,这个节点就要扛 10 万 QPS(含网络 IO 和单线程 CPU),而集群其他节点可能很闲。热 Key 的本质是访问分布极度倾斜,单点成为瓶颈——这不是集群扩容能解决的。
  • 为什么本地缓存能立刻止血? 热点读请求集中在少数数据上。在应用进程内用 Caffeine 缓存这几份数据(TTL 1–5 秒),大部分读请求在应用内就命中返回,根本不走 Redis。10 台应用 × 每台挡掉 90% 读 = Redis 压力降到原来的 1/10 以下。这是成本最低、生效最快的止血手段。代价是多实例各自缓存,数据变更后有秒级不一致(热 key 通常可接受)。
  • 为什么“多副本 key”能打散? 把 product:123 复制成 product:123:0、product:123:1…product:123:N,请求随机选一个副本读。不同副本的 hash 值不同,落到不同分片节点,单节点压力被均摊。这是“用空间换单点负载”的经典手法。写时要更新所有副本(或用短 TTL 让其自然过期后回源)。
  • 为什么写热点(库存)要“分片存储+异步合并”? 写热点无法用本地缓存(要保证一致性),也无法简单多副本(写要原子)。做法:把库存 1000 拆成 stock:1~10 各存 100,请求随机扣减一个分片;扣到 0 则试下一个;后台任务定期把各分片汇总到 DB。把“一个 key 的写热点”变成“N 个 key 的分散写”,每个分片压力是原来的 1/N。代价是逻辑变复杂,需要处理分片间不均、汇总延迟。
  • 热 Key 和大 Key 的区别是什么? 热 Key:单 key 访问频率过高,打的是节点 CPU/网络带宽。大 Key:单 key 存储体积过大(如百万元素的 Hash),打的是单命令耗时和带宽,一次 HGETALL 就能卡住。两者可能叠加,但治理手段不同:热 Key 靠本地缓存+打散,大 Key 靠拆分结构。

【选型判断树】

热 Key 治理选型:
读热点(最常见)
├─ 应急 → 应用本地缓存(Caffeine,TTL 秒级)
├─ 加固 → key 多副本随机读,打散到多分片
└─ 长期 → 热点探测 + 自动推送到本地缓存

写热点(如库存)
├─ 低并发 → 分布式锁串行(简单但吞吐低)
├─ 高并发 → 分片库存(stock:1~N)+ 随机扣减 + 异步汇总
└─ 兜底 → DB 条件更新 + 对账

预防
└─ 压测找热点 + 监控单 key QPS + 限流保护 Redis

口诀:读热用本地缓存+多副本,写热用分片打散,预防靠压测+监控。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“热 Key 治理题。我会先讲根因,再分应急止血和长期根治”
0:30–1:00根因一个 key 只落一个分片,访问倾斜导致单节点瓶颈;区别于大 Key
1:00–2:30应急止血本地缓存挡读(最快);key 多副本打散;限流保护
2:30–4:00长期根治读热点:多级缓存+热点探测;写热点:分片库存+异步汇总
4:00–5:00收尾“热 Key 本质是访问分布倾斜,集群扩容无效,必须在应用层打散或缓存”

【关键数字】

参数经验值说明
本地缓存 TTL1–10 秒热点数据可容忍秒级不一致
本地缓存容量限制条目数(如 1000)+ 过期防止内存撑爆
key 副本数5–20按热点程度,打散到多分片
库存分片数10–100超热点场景
单 key QPS 告警按「集中到该 key 的流量比例」分档:100% 集中(题干口径)→ 5 万 QPS/key 就该告警(≈819Mbps、单线程满载);50% 集中 → 阈值可放宽一倍不能给一个与集中比例无关的固定档
Redis 单节点能力约 10 万 QPS简单命令
热点探测窗口滑动 10s–1min及时发现新兴热点
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“热 Key 和大 Key 有什么区别?治理手段一样吗?” → 不一样。热 Key 是访问频率过高,打的是节点 CPU/带宽;大 Key 是单 value 过大,打的是单命令耗时。热 Key 治理靠本地缓存+打散;大 Key 治理靠拆分数据结构(Hash 分页、ZSet 分片)。两者可能同时出现,要分别处理。

L2|“本地缓存多实例不一致怎么办?” → 用短 TTL 容忍秒级不一致(热点数据通常可接受)。若需要更快失效:① 更新时发广播(Redis Pub/Sub)通知各实例删除本地缓存;② 版本号比对,版本不一致则回源;③ 订阅 binlog 异步失效。注意广播可能丢消息,短 TTL 仍是兜底。

L3|“写热点(扣库存)能用本地缓存吗?为什么?” → 不能。写要保证一致性,本地缓存是各实例独立的,无法做原子扣减。写热点正确做法:① 分片库存打散写压力;② 或 Redis 预扣 + DB 兜底;③ 或分布式锁(吞吐低,仅超热点复杂逻辑)。本地缓存只适合读。

【评分标准】

档位答案特征
60 分能说出“加本地缓存”
80 分区分热 Key 与大 Key;知道本地缓存+多副本打散;了解写热点分片
95 分能解释“一个 key 只落一个分片所以集群无效”;设计热点探测机制;能处理写热点的分片+汇总方案

【关联题】

  • 同簇: 第 30 题(大 Key 治理)、第 17 题(多级缓存)、第 47 题(缓存预热)
  • 对比参照: 第 32 题(Redis 单线程,热 Key 为何致命)、第 162 题(计数系统的热点打散)
  • 系统设计: 第 134 题(优惠券,热点商品)

【自测】

  1. 为什么热 Key 扩 Redis 集群节点没用? 参考答案: 一个 key 只落在一个分片,所有请求仍打到该节点;扩容不改变单 key 的分片位置。
  2. 本地缓存为什么能止血热 Key? 参考答案: 热点数据在应用进程内缓存,大部分读不走 Redis,多实例各自拦截,Redis 压力大幅下降。
  3. 写热点(库存)如何打散? 参考答案: 拆成多个分片 key,请求随机扣减,后台汇总;或 Redis 预扣+DB 条件更新兜底。

14. 下单接口单机 TPS 只有 200,大促要 2000(吞吐优化) ​

【考察内容】性能优化的系统性方法

【题目】压测发现下单接口单机 TPS 只有 200,而大促目标要求单机 2000+。你从应用层、数据库、中间件、架构几个层面,分别能做什么把吞吐提上去?

【参考答案】吞吐优化目标:单机 TPS 200 → 2000,放大 10 倍,要找“耗时花在哪”:

  1. 容量估算(步进):TPS=200 时,单请求系统耗时约 5ms(1/200s 线程占用)。若单请求在 DB 花 20ms、缓存/远程再花 30ms,同步串联可达 50–100ms,则单机受 IO 等待限制,TPS 很难上 2000。目标公式(Little 定律:并发 = TPS × RT):要 2000 TPS,单请求占用系统的耗时必须 ≤ 线程数 ÷ 2000 秒——10 个线程即 ≤5ms,单线程则要 ≤0.5ms(10 只是演示量纲、不是本题硬约束:线程数本身可以加,能加到 100 就只需 ≤50ms,不必把 RT 压到 5ms;真正卡住的是下游连接池与 DB 并行度(见第 64 题),所以「加线程」的前提是下游扛得住,否则只是把排队从应用挪到 DB);常见路径是把重 IO 挪走或并行化,而非盲目加线程。
  2. 优化手段(题干要「应用层/数据库/中间件/架构」四层分别给;下面 ①–⑥ 只覆盖前三层,架构层见句末补充):① 缓存热点,减少 DB 往返;② SQL 优化/索引,把 DB RT 从 20ms 压到 2–3ms;③ 串行 RPC 改并行/合并批量;④ 同步改异步(可延迟步骤);⑤ 减少序列化、日志同步写、锁竞争;⑥ 垃圾回收与对象分配优化。架构层:读写分离与分库分表(把单库压力拆开)、多级缓存(本地+Redis,把读挡在应用外)、热点接口拆独立服务并单独扩容(隔离故障域)、无状态化水平扩容+MQ 削峰——只答前三层会被判「单机 200→2000 十倍差距靠调参补不平」。
  3. 不要做的事:线程池无脑调大(上下文切换、连接池连带打满);只加机器不改架构(DB 还是瓶颈)。
  4. 失败与降级:优化过程中灰度验证,监控错误率与 RT 分布;回滚预案;优化后限流阈值同步上调前要压测确认。
  5. 验证:对比优化前后压测曲线;分层看应用/缓存/DB 各层耗时占比。本质是降低单请求资源占用时间,同样硬件承载更高并发。

【原理溯源】

  • 为什么必须“先测量再优化”? 不测量就优化是瞎猜。TPS 上不去可能是 CPU 满、可能是锁等待、可能是下游慢、可能是连接池耗尽——原因不同,手段完全不同。盲目加缓存可能浪费,盲目加线程可能更糟(切换开销)。压测+监控定位到瓶颈资源,才能对症下药。这是性能优化的第一原则。
  • 为什么瓶颈在“等待”时 CPU 会很低? TPS 上不去但 CPU 低,说明线程在等(IO/锁/下游),不在算。这时加 CPU 或加线程(若等待是共享资源限制则无效)都没用,要去掉等待源:优化下游、减少锁竞争、增加连接。CPU 满是计算瓶颈,CPU 空是等待瓶颈——这两种的优化方向截然相反。
  • 为什么串行改并行能提吞吐? 若一次下单要串行调用库存(50ms)、风控(30ms)、积分查询(20ms),总耗时 100ms,单线程 TPS=10。改成三个调用并行,总耗时 ≈ max(50,30,20)=50ms,TPS 翻倍。并行不减少总工作量,但缩短关键路径。前提:这些调用之间没有依赖,且下游能承受更高并发。
  • 为什么异步化能提吞吐? 非核心逻辑(发短信、写日志)从同步路径挪到 MQ,主流程更快结束,线程/连接占用时间缩短,同样资源能服务更多请求。本质是减少每个请求占用共享资源的时间。与并行的区别:并行是“同时做多件事”,异步是“这件事不做/晚点做”。
  • 为什么缓存能提吞吐? 缓存把 DB 查询(1–10ms)变成内存读取(0.1ms),单请求耗时下降 10–100 倍,吞吐反比上升。更关键的是:DB 是共享瓶颈(连接池、磁盘 IO),缓存挡掉读流量后,DB 有余力处理写。所以读多写少场景,缓存是最有效的吞吐手段。

【选型判断树】

TPS 上不去,先看哪个资源先饱和:
├─ CPU 满 → 计算密集
│   ├─ 优化算法/序列化(JSON→Protobuf)
│   ├─ 减少重复计算(缓存计算结果)
│   └─ 水平扩容
├─ CPU 低但 TPS 低 → 等待瓶颈
│   ├─ 锁竞争 → 减少临界区/分段锁/无锁
│   ├─ 下游慢 → 并行化/异步化/熔断
│   └─ 连接池满 → 调大池/优化慢查询/扩 DB
├─ DB 饱和
│   ├─ 读多 → 缓存 + 读写分离
│   ├─ 写多 → 批量 + 异步 + 分库分表
│   └─ SQL 慢 → 索引 + EXPLAIN 优化
└─ IO/网络满 → 压缩、减少传输、本地缓存

口诀:先看谁先满,满了才有方向;CPU 满算得慢,CPU 空等得久。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“吞吐优化题。我会先定位瓶颈,再按层给手段”
0:30–1:00定位方法压测看 CPU/IO/线程/DB 谁先饱和;强调“不测量不优化”
1:00–3:30分类优化DB 慢→索引/缓存;串行→并行/异步;锁→减临界区;计算→算法/序列化
3:30–4:30架构层多级缓存、接口合并、无状态扩容
4:30–5:00收尾“吞吐优化本质是减少单请求占用关键资源的时间,或增加资源数量”

【关键数字】

参数经验值说明
当前 TPS200起点
目标 TPS2000+10 倍提升
串行改并行收益关键路径缩短到 max(各步)前提是无依赖
缓存收益读 RT 从 5–10ms → 0.1ms命中率 >90% 时显著
异步化收益非核心逻辑移出主路径RT 和吞吐同时改善
连接池核心线程的 0.5–1 倍(DB)过大压垮 DB
单机扩容线性度无状态服务接近线性有共享瓶颈时递减
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“优化了 SQL、加了缓存,TPS 还是上不去,怎么办?” → 回到定位:现在是哪个资源先饱和?可能是应用 CPU(计算逻辑)、可能是线程池(下游等待)、可能是连接池、可能是锁。每优化一步要重新压测,看瓶颈是否转移。瓶颈是会移动的——解决了 DB,可能应用变成新瓶颈。这是迭代过程,不是一步到位。

L2|“压测 TPS 上不去但 CPU 很低,说明什么?” → 瓶颈在等待不在计算。可能原因:锁竞争、下游服务慢、IO 等待、连接池耗尽、GC 等待。用 jstack 看线程状态(BLOCKED/WAITING),用调用链看哪一跳慢。这时加机器或加线程可能无效(如果等待的是共享资源),要消除等待源。

L3|“并行化改造要注意什么?有什么坑?” → ① 确认无依赖,有依赖不能并行;② 下游要能承受更高并发(并行会放大对下游的压力);③ 线程池隔离,避免并行任务耗尽全局线程;④ 异常处理:一个并行分支失败怎么办(全部失败?部分成功?);⑤ 超时设置:并行的总超时要覆盖最慢分支;⑥ 测试:并发路径的 bug 更难复现。

【评分标准】

档位答案特征
60 分能罗列“加缓存、加索引、加机器”
80 分先定位瓶颈再优化;能按 CPU 满/等待/DB 分类给手段
95 分能解释“CPU 低 TPS 低=等待瓶颈”;知道瓶颈会转移需迭代;能讨论并行化的注意事项;给出量化验证方法

【关联题】

  • 同簇: 第 21 题(性能优化方法论)、第 9 题(变慢排查)、第 14 题与第 10 题(压测验证)
  • 具体手段: 第 51 题(慢 SQL)、第 12 题(异步化)、第 17 题(多级缓存)、第 11 题(线程池)
  • 架构层: 第 15 题(读写分离)、第 55 题(分库分表)

【自测】

  1. TPS 上不去,第一步应该做什么? 参考答案: 压测定位瓶颈资源(CPU/IO/锁/DB/下游),不测量不优化。
  2. CPU 低但 TPS 低,说明什么问题? 参考答案: 瓶颈在等待(锁/下游/IO/连接池),不在计算;优化方向是消除等待。
  3. 串行改并行能提吞吐的原理是什么?前提是什么? 参考答案: 缩短关键路径耗时;前提是各步骤无依赖且下游能承受更高并发。

15. 商品详情页读 QPS 10 万、写 QPS 100(读多写少架构) ​

【考察内容】读优化架构

【题目】商品详情页每天被大量浏览:读 QPS 10 万、写 QPS 只有 100(价格、库存偶尔变动)。要求详情页秒开、数据库不能被打挂。你怎么设计这套读写极不对称的系统?

【参考答案】读 10 万 / 写 100 的读多写少架构:缓存抗读、DB 保写、多级分担:

  1. 容量估算(步进):读写比约 1000:1。DB 单机读 1–2 万 QPS,10 万读直接打 DB 必挂;Redis 单机约 10 万 QPS,一台/一组 Redis+本地缓存即可承接。写 QPS=100,单库毫无压力。缓存命中率目标 ≥95% 时,回源约 10万×5%=5000 QPS,读写分离后主库几乎只扛写。
  2. 架构:① 本地缓存(Caffeine,热点详情)→ ② Redis 集群 → ③ MySQL 读写分离(读走从库);页面/接口可 CDN 或静态化。
  3. 一致性:写后删除缓存+过期兜底;详情类允许短暂不一致;价格/库存等强一致字段不以缓存为权威,或短 TTL+主动失效。
  4. 失败与降级:Redis 挂 → 本地缓存+从库限流读,或返回兜底模板;缓存击穿 → 热点互斥重建/逻辑过期;从库延迟 → 关键读可走主库或提示延迟。
  5. 扩展:多级缓存一致性(广播失效)、热点 key 打散、监控命中率(低于 90% 要查)。本质:用内存层级吃掉读洪峰,DB 专注少量写与权威数据。

【原理溯源】

  • 为什么读多写少要优先“静态化”? 读 10 万 QPS、写 100 QPS,意味着请求里 99.9% 是读(10万 ÷ 10.01万),而每条数据两次写之间被反复读很多次。这类几乎不变的数据最适合静态化:渲染成 HTML/JSON 放 CDN,请求在边缘节点就返回,根本不回源。这是把“动态计算”变成“静态分发”,成本最低、速度最快。只有数据变更时才重新生成静态页。
  • 为什么 CDN 能扛掉绝大部分读流量? CDN 节点分布在各地,用户就近访问。同一商品详情页被百万用户浏览,CDN 命中后源站只收到回源请求(通常 <1%)。10 万 QPS 的浏览,到源站可能只有几百 QPS。这是“用边缘节点的分布式缓存对抗集中读”的极致形态。
  • 为什么还要多级缓存(本地+Redis)? CDN 适合完全静态的内容;但详情页可能有个性化元素(用户级别优惠、库存实时数),不能完全静态化。本地缓存(微秒级)挡热点,Redis(毫秒级)共享缓存,DB 兜底。多级缓存是用“一致性递减、速度递增”的层级结构,让 99%+ 的请求在最便宜的层被满足。
  • 为什么写时要“先更新 DB 再删缓存”而不是“先删缓存再更新 DB”? 先删缓存:删完后、更新 DB 前,有并发读把旧值读回缓存,旧数据被写回且长期存在(直到下次过期)。先更新 DB 再删缓存:并发读可能短暂读到旧缓存,但下次读会回源拿到新值。虽仍有极小概率不一致,但概率远低于前者,且可用延迟双删/ binlog 订阅进一步降低。这是 Cache Aside 模式成为主流的原因。
  • 为什么缓存挂了必须 DB 限流而不是直接放行? 正常时 10 万读中 99% 被缓存挡掉,DB 只扛几百。缓存全挂后 10 万读全部打到 DB,DB 瞬间被打死,写也跟着挂。所以缓存故障时必须立即启用 DB 限流(把进入 DB 的读压到其容量以下),并返回兜底数据(可能不完整)或排队。宁可部分降级,不能 DB 挂掉。

【选型判断树】

读多写少架构选型:
数据是否可完全静态?
├─ 是(无个性化)→ 静态化 + CDN(最优)
└─ 否(有个性化/实时字段)
    ├─ 个性化字段独立加载(静态部分 CDN,动态部分接口)
    └─ 多级缓存:本地 → Redis → DB

写路径
└─ 先更新 DB → 再删缓存(Cache Aside)
    兜底:延迟双删 / 订阅 binlog

缓存故障
└─ DB 限流 + 降级返回

读写分离
└─ 主写从读,注意主从延迟(第 46/59 题)

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“读多写少架构题。核心思路:能静态就静态、能缓存就缓存、读写分离”
0:30–1:00定界读 10 万、写 100,比例 1000:1,数据变化极少
1:00–3:00分层设计静态化+CDN → 多级缓存 → 读写分离;写路径 Cache Aside
3:00–4:00一致性与故障先更 DB 再删缓存的原因;延迟双删/binlog 兜底;缓存挂了 DB 限流
4:00–5:00收尾“读多写少的本质是用各种缓存层把读流量挡在 DB 外面,写路径保持简单正确”

【关键数字】

参数经验值说明
读写比1000:1极不对称
CDN 命中率目标>95%低于 90% 要查回源原因
多级缓存命中率>95%(Redis+本地)低于 80% 设计有问题
本地缓存 TTL30s–5min热点数据
Redis TTL10min–24h视数据变更频率
主从延迟通常 <1s,峰值可能秒级读写分离要注意
缓存挂后 DB 限流按 DB 读容量设定(如 2000 QPS)保命
静态页重新生成写后秒级~分钟级视业务容忍
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“Cache Aside 为什么是‘先更 DB 再删缓存’,不是‘先删缓存’?” → 先删缓存有更大的不一致窗口:删后更新 DB 前,并发读把旧值回填缓存,旧数据长期存在。先更 DB 再删:即使并发读短暂读到旧缓存,下一次读会回源新值。虽非完美(仍有极小概率不一致),但概率低一个数量级,是工程上的最优实践。

L2|“主从延迟导致用户刚改完就查到旧数据,怎么办?” → 分场景:① 读己之写——用户自己的读走主库;② 关键读走主库,非关键走从库;③ 等待复制位点(读从库时检查复制进度是否已包含本次写);④ 降低延迟用半同步复制。详情页展示类读通常可接受秒级延迟。

L3|“缓存和 DB 不一致了,怎么发现和修复?” → 发现:① 定时对账任务抽样比对缓存与 DB;② 监控缓存更新失败率;③ 业务侧兜底短 TTL。修复:① 手动/自动删除缓存 key 强制回源;② 订阅 binlog 异步刷新;③ 延迟双删。预防:写路径失败要有重试与告警,不能静默。

【评分标准】

档位答案特征
60 分能说出“加 Redis 缓存”
80 分知道静态化+CDN、多级缓存、读写分离;了解 Cache Aside
95 分能解释先更 DB 再删缓存的原因;设计缓存挂了的 DB 限流降级;处理主从延迟的读己之写;有对账意识

【关联题】

  • 同簇: 第 17 题(多级缓存设计)、第 27 题(缓存一致性)、第 28 题(延迟双删)、第 29 题(缓存策略选型)
  • 读写分离细节: 第 46 题(主从延迟)、第 59 题(读写分离延迟)
  • 对比参照: 第 16 题(写多读少,完全相反的架构)
  • 系统设计: 第 123 题(短链,也是读多写少)

【自测】

  1. 读 10 万写 100,最优的第一层手段是什么? 参考答案: 静态化+CDN。数据几乎不变,最适合静态分发,绝大部分读在边缘返回。
  2. Cache Aside 的写路径是什么?为什么这个顺序? 参考答案: 先更新 DB 再删缓存。先删缓存会有更大窗口把旧值回填,不一致概率更高。
  3. 缓存全挂了怎么办? 参考答案: 立即 DB 限流把读压到 DB 容量以下,返回兜底数据,防止 DB 被打挂导致写也挂。

16. 埋点日志每秒写入 10 万条、几乎不读(写多读少架构) ​

【考察内容】写优化与存储选型

【题目】你们要做一套埋点日志系统:用户行为日志每秒产生 10 万条,几乎只写不读(只有事后分析才查)。直接写 MySQL 肯定不行,你怎么设计存储与写入链路?

【参考答案】写 10 万条/秒、几乎不读:追加写、异步批、少索引、后置算:

  1. 容量估算(步进):10 万条/s,假设每条 200B → 约 20MB/s 峰值写入;一天约 86 亿条量级,必须提前做容量与生命周期。MySQL 行级插入远达不到 10 万/s(通常几千 TPS 量级),单靠 DB 同步写必挂。
  2. 架构:① 应用 → MQ/Kafka(高吞吐日志通道)快速 ACK;② 批量消费写入:按时间/条数攒批(如 500 条或 200ms)写入;③ 存储选型:顺序写友好的系统——Kafka 本身可短期存、ClickHouse/ES/HBase/对象存储归档;④ 强索引延后建,或只建时间+少量过滤字段。
  3. 写入优化:按天/小时分表分区,冷热分离;压缩;避开随机小 IO;可异步双写校验。
  4. 失败与降级:MQ 积压 → 扩消费者,积压可接受(日志类允许延迟);下游存储挂 → 堆积在 MQ,恢复后追赶;丢失可接受的埋点可降采样(抽样 10%–100% 可调);必须不丢的走同步+本地缓冲+重试。
  5. 口径:写多读少的核心是把随机同步写变成顺序批量写,并把“查询分析”与“采集写入”解耦。

【原理溯源】

  • 为什么 MySQL 不适合海量写? MySQL InnoDB 是 B+ 树结构,写入时要维护索引(随机 IO)、可能触发页分裂、要写 undo/redo、要维护事务。每条写入的成本远高于“追加到文件末尾”。单机写入通常几千 TPS,面对 10 万/s 的日志写入差 1–2 个数量级。而且日志几乎不读,B+ 树为快速点查优化的结构完全浪费了。
  • 为什么“顺序写+追加”是写密集场景的正解? 磁盘顺序写速度可达几百 MB/s,随机写只有几 MB/s(差 100 倍)——这组数是机械盘 HDD 口径;NVMe SSD 随机写能到数百 MB/s~GB/s,顺序/随机差距缩到几倍~十几倍,但「顺序追加仍优于随机写」的结论不变,引用这组数要先说盘型。Kafka 把消息追加到分区日志文件末尾(顺序写),充分利用磁盘顺序带宽。LSM 树(HBase/RocksDB)也是类似思想:先写内存,批量刷盘成有序文件,把随机写变成顺序写。写密集场景选存储,先看“是否支持顺序追加”。
  • 为什么要客户端本地缓冲+批量上报? 每秒 10 万条,若每条一个 HTTP 请求,网络 RTT 和连接开销会成为新瓶颈。客户端先把日志写本地文件/内存缓冲,攒够一批(如 100 条或 1 秒)再上报,把“百万次小请求”变成“万次批量请求”,网络效率提升 10–100 倍。代价是客户端崩溃可能丢最后一批(日志通常可接受)。
  • 为什么用 Kafka 而不是直接写存储? Kafka 是为高吞吐日志场景设计的:顺序写、多副本、水平扩展。它作为“写入缓冲层”接收所有日志,下游消费者按自己的节奏处理入仓。把“写入速率”与“处理/存储速率”解耦,峰值不会打垮下游。若直接写 ClickHouse,高峰可能把 CK 打挂。
  • 为什么日志系统可以“几乎不读”却还要设计查询? “几乎不读”指在线业务不读,但事后分析(用户行为分析、故障排查、合规审计)必须能查。查询模式是“按时间范围+条件过滤的批量扫描”,与在线交易的点查完全不同。所以选列式存储(ClickHouse)——列式压缩比高、适合聚合扫描,而不是为点查优化的行存。

【选型判断树】

写多读少架构选型:
先问:写入速率与数据特征
├─ 海量写、几乎不读、允许丢一点 → Kafka + 列式数仓
│   (埋点、日志、监控指标)
├─ 海量写、要按主键查 → LSM 存储(HBase/RocksDB)
│   (设备状态、时序数据)
└─ 写多但也要在线查 → Kafka 缓冲 + 可查询存储(ES/CK)

存储选择:
├─ 聚合分析为主 → ClickHouse / Hive / Presto
├─ 全文检索为主 → Elasticsearch
├─ 时序数据 → InfluxDB / TDengine
└─ 冷数据 → 对象存储(OSS/S3),成本极低

口诀:写多先看是否顺序追加;日志用 Kafka+列存;点查用 LSM;冷数据归档。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“写多读少架构题。我会按写入链路、存储选型、可靠性来讲”
0:30–1:00定界10 万条/s,几乎只写;查询是事后分析,非在线点查
1:00–3:00链路设计客户端缓冲批量 → Kafka 顺序写 → 流式消费入数仓 → 冷热分层
3:00–4:00为什么不用 MySQLB+树随机写慢;日志几乎不读,点查优化浪费;顺序写+追加才对
4:00–5:00收尾“写多读少的本质是选为写优化的存储(顺序追加),用 MQ 解耦写入与处理速率”

【关键数字】

参数经验值说明
日志写入速率10 万条/s本题规模
Kafka 单机写入百万级 TPS(小消息)顺序写的优势
MySQL 单机写入数千 TPS差 1–2 个数量级
客户端批量100 条或 1s 一次平衡延迟与吞吐
Kafka 保留策略按天(如 3–7 天)防磁盘打满
冷数据归档T+7 或 T+30 迁 OSS成本降一个数量级
ClickHouse 写入批量写入,单行写性能差必须攒批
压缩比列存压缩 10:1 常见节省存储成本
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“日志也要强一致事务吗?” → 不需要,这是过度设计。埋点日志允许丢失少量、允许乱序、允许秒级延迟。强一致(事务、同步写)会把吞吐拉到 MySQL 级别,完全没必要。按数据价值选一致性:资金>订单>行为日志>调试日志。

L2|“Kafka 数据量膨胀怎么控制?” → ① 设置保留时间(retention.ms)或大小(retention.bytes),到期自动删除;② 按 topic 分离不同日志,分别设保留;③ 压缩(lz4/zstd)减少存储;④ 冷数据转存对象存储;⑤ 监控磁盘水位,提前扩容。不设保留策略是生产事故的常见原因。

L3|“ES 写入慢怎么办?” → ① 批量写入(bulk),别单条;② 降低副本数(分析场景可临时 0 副本);③ 按天建索引,删除旧索引比删文档高效;④ 调大 refresh_interval(默认 1s,可改 30s 或手动 refresh);⑤ 关闭不需要的字段索引;⑥ 写入量太大时前面再加 Kafka 缓冲。

【评分标准】

档位答案特征
60 分能说出“用 Kafka,不要用 MySQL”
80 分能讲清 MySQL 为何不适合海量写;设计客户端缓冲+Kafka+数仓链路;知道列式存储
95 分能解释顺序写 vs 随机写的性能差异;设计冷热分层与保留策略;讨论 ES/CK 写入优化;知道日志不需要强一致

【关联题】

  • 同簇: 第 86 题(Kafka 高吞吐原理)、第 79 题(MQ 价值)、第 84 题(MQ 选型)
  • 对比参照: 第 15 题(读多写少,完全相反)
  • 存储延伸: 第 65 题(数据归档)、第 146 题(BI 看板,读侧)

【自测】

  1. 为什么 MySQL 不适合 10 万条/s 的日志写入? 参考答案: B+树随机写+事务开销,单机几千 TPS;日志几乎不读,点查优化浪费;应选顺序追加存储。
  2. 为什么客户端要本地缓冲批量上报? 参考答案: 减少网络请求次数,把小请求变批量,网络效率提升 10–100 倍。
  3. 日志系统为什么不需要强一致事务? 参考答案: 数据价值低,允许少量丢失、乱序、延迟;强一致会严重牺牲吞吐。

17. 详情页要秒开,单层缓存还不够(多级缓存设计) ​

【考察内容】缓存分层架构

【题目】商品详情页要求 P99 30ms 内返回。只加一层 Redis 缓存后,网络开销仍然偏大、QPS 上不去。你打算引入本地缓存(进程内)与 Redis 配合,怎么设计多级缓存的读取与更新?各自缓存什么数据?

【参考答案】多级缓存目标:详情页秒开(题干口径 P99 ≤30ms;本地缓存未命中回源 Redis 时控制在 ≤20ms 内为可接受回退线)——为什么不是 50ms:题干 P99 目标是 30ms,而 L1 只挡 30%–50%,若回退线取 50ms,P50–P99 就落在回源集合上,P99 必然击穿 30ms;只有把未命中占比压到 <1%(让 P99 不再落在回源路径)时,回退线才可以放宽到 50ms。

  1. 容量估算(步进):假设详情页峰值读 5 万 QPS,单实例 50 台则每台约 1000 QPS。若全部打 Redis:集群仍可扛,但 RT 与带宽仍有压力;若命中本地缓存(进程内)RT 可到 <1ms,Redis 约 0.1–1ms(同机房,含网络与序列化),回源 DB 约 1–10ms(走索引)。命中率目标(口径:「占全部读请求的比例」,与下面【关键数字】里「热点数据 >90%」不是同一个分母):本地 30%–50% + Redis 40%–60% + 总命中 ≥95%,回源 ≈2500 QPS。
  2. 分层设计:L1 本地缓存(Caffeine,短 TTL 1–5s,热点详情)→ L2 Redis(TTL 分钟~小时,全量热点)→ L3 DB。静态片段/整个页可再加 CDN/网关缓存。
  3. 一致性:本地缓存变更不敏感的展示数据用短 TTL;写路径更新 DB 后发缓存失效广播(Redis Pub/Sub 或消息)清理各实例本地缓存;价格/库存等准实时字段单独短 TTL 或旁路读。
  4. 失败与降级:本地缓存放大陈旧风险 → TTL 收短+主动失效;Redis 挂 → 本地缓存兜底+DB 限流;重建风暴 → 互斥重建/逻辑过期;全链路失败时返回静态模板或“稍后刷新”。
  5. 监控:各级命中率、RT、重建次数、陈旧投诉率。本质:用空间换时间,把读压力从远端挪到离 CPU 更近的地方,同时用失效协议控制陈旧。

【原理溯源】

  • 为什么单层 Redis 不够? Redis 访问仍有网络 RTT(同机房 0.1–1ms)和序列化开销。P99 30ms 的目标下,1ms 的 Redis 访问占比不大,但高并发时 Redis 单节点可能成为带宽/CPU 瓶颈——所有实例都来打同一个 Redis。本地缓存把热点数据放在进程内存,访问是纳秒–微秒级(与本题【关键数字】L1818 同口径),且压力被分散到每个应用实例,把 Redis 单点热点的压力大幅摊掉(说「彻底消除」过头:Redis 仍要扛非热点读与写,且引入多实例间一致性成本)。
  • 为什么多级缓存是“一致性递减、速度递增”的层级? L1 本地:最快(微秒)、最不一致(多实例各自为政)、容量最小(受实例内存限制)。L2 Redis:较快(毫秒)、较一致(共享)、容量中等。L3 DB:最慢(毫秒+)、最一致、容量大。让 90%+ 的热点请求在 L1 命中,剩下的去 L2,极少数回源 DB——用速度换一致性的分层妥协。
  • 为什么本地缓存的多实例一致性是大坑? 每个应用实例有独立的本地缓存副本。写操作更新 DB 后,如何让 10 个实例的本地缓存都失效?① 短 TTL:到期自然过期,容忍秒级不一致(最简单);② 失效广播:更新时 Pub/Sub 通知所有实例删除(实时但可能丢消息);③ 版本号:读时比较版本,不一致则回源。没有完美方案,都是在一致性与复杂度间取舍。
  • 什么数据适合放本地缓存? 适合:读多写少、可容忍短暂不一致、数据量小(能放进实例内存)。如:商品基础信息、配置项、字典表、热点用户信息。不适合:更新频繁(库存、余额——本地缓存会脏)、数据量大(撑爆内存)、强一致要求(资金)。
  • 为什么回源时要“回填两级”而不是只填 L1? 只填 L1:其他实例下次访问还要回源 DB,且 L1 过期后又要回源。回填 L1+L2:L2 是共享的,其他实例可以先从 L2 拿,减少 DB 压力。L2 充当“实例间的共享层”,降低整体回源率。

【选型判断树】

要不要加本地缓存?
数据特征:
├─ 热点、读多写少、可容忍秒级不一致 → 加 L1(推荐)
├─ 更新频繁或强一致 → 不加 L1,只用 Redis
└─ 数据量大 → 只缓存热点子集到 L1,全量在 L2

L1 失效策略:
├─ 简单场景 → 短 TTL(1–30s)
├─ 要较快失效 → Redis Pub/Sub 广播
└─ 要精确 → 版本号比对

容量控制:
└─ L1 限制最大条目数+权重,防撑爆堆内存

口诀:热且可脏放本地,更新频繁别放,失效广播或短 TTL,容量一定要限。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“多级缓存设计题。我会讲为什么分层、怎么读写、一致性怎么处理”
0:30–1:00为什么分层Redis 有网络 RTT 且可能单点热点;本地微秒级分散压力
1:00–2:30读写设计读:L1→L2→DB,回填两级;写:先 DB 后删缓存,L1 失效
2:30–3:30一致性多实例 L1 不一致是核心坑;短 TTL / 广播 / 版本号三种方案
3:30–4:30适用边界什么数据放 L1,什么不放;容量限制
4:30–5:00收尾“多级缓存是用一致性换速度的分层妥协,核心难点在 L1 多实例失效”

【关键数字】

参数经验值说明
L1 本地访问微秒级(纳秒–微秒)无网络
L2 Redis 访问0.1–1ms含网络+序列化
L3 DB 访问1–10ms走索引
L1 TTL1s–5min热点可短,配置可长
L1 容量限制条目数+权重上限防 OOM
L1 命中率目标热点数据 >90%口径是「热点集合内部」命中率,不是全站请求占比(全站口径见正文第 1 条的 30%–50%)
整体命中率>95%回源率控制
P99 目标30ms本题
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“本地缓存用什么组件?Caffeine 和 Guava Cache 有什么区别?” → 推荐 Caffeine。相比 Guava Cache:① 更高性能(W-TinyLFU 淘汰算法,命中率更高);② 真异步加载与刷新(buildAsync/AsyncCache,刷新不占读线程;Guava 也有 refreshAfterWrite,但其重载在调用线程同步执行);③ 更好的统计与监控。Guava Cache 已停止推荐,新项目用 Caffeine。

L2|“本地缓存更新时,怎么通知其他实例失效?” → 三种:① 短 TTL 自然过期(最简单,容忍秒级不一致);② Redis Pub/Sub 广播失效消息(较快,但 Pub/Sub 可能丢消息,需短 TTL 兜底);③ 版本号/时间戳比对(读时校验,精确但增加一次 Redis 访问)。生产常用 ①+② 组合。

L3|“本地缓存会不会把堆内存撑爆?” → 会,必须限制。① 设置最大条目数(maximumSize)和权重(maximumWeight);② 设置过期时间;③ 只缓存热点子集,不是全量数据;④ 监控缓存占用内存,设告警;⑤ 大对象不要放本地(序列化后仍大)。Caffeine 的 size-based eviction 必须配,不能只设 TTL。

【评分标准】

档位答案特征
60 分能说出“本地缓存+Redis”
80 分知道读写路径、回填两级、先更 DB 再删缓存;了解多实例失效问题
95 分能解释分层的一致性/速度权衡;给出三种失效方案;讨论容量限制与适用边界;知道 Caffeine 优于 Guava

【关联题】

  • 同簇: 第 15 题(读多写少总架构)、第 27 题(缓存一致性)、第 38 题(更新顺序)、第 119 题(多级缓存一致性)
  • 组件延伸: 第 45 题(缓存放进程内还是 Redis)
  • 故障: 第 26 题(缓存雪崩,多级缓存的过期策略)

【自测】

  1. 为什么单层 Redis 不够,要加本地缓存? 参考答案: Redis 有网络 RTT 且高并发下可能单点热点;本地缓存微秒级,且压力分散到每个实例。
  2. 本地缓存最大的坑是什么? 参考答案: 多实例各自独立,更新时要通知所有实例失效;方案有短 TTL、广播、版本号。
  3. 什么数据不该放本地缓存? 参考答案: 更新频繁(库存/余额)、强一致要求(资金)、数据量大撑爆内存的。

18. 流量高峰要自动加机器、低谷要省成本(弹性伸缩) ​

【考察内容】云原生与容量工程

【题目】你们的服务白天高峰 CPU 90%、夜里低谷只有 10%,固定机器数要么浪费要么不够用。如何实现“高峰自动加机器、低谷自动减机器”?扩容的依据、步骤、风险分别是什么?

【参考答案】弹性伸缩要“指标驱动 + 冷却可控 + 成本平衡”:

  1. 容量估算(步进):假设白天峰值 2 万 QPS、夜间 2000,单实例健康容量 2000 QPS(利用率目标 60%–70%)→ 峰值理论实例 ≈ 2万/2000×1.4 ≈ 14–15 台,低谷 ≈ 2–3 台。伸缩目标是在 SLA 内尽量贴近曲线,节省约 50%–70% 闲置成本。
  2. 指标与触发:不只看 CPU,还要看 RT、线程池使用率、队列长度、自定义业务指标(如 QPS);CPU>60% 或 RT>阈值持续 2–3 分钟触发扩容;缩容要更保守(更长观察期、更低阈值),防止抖动。
  3. 冷却与预热:冷却要分两侧——扩容冷却 0–1 分钟(要能快速跟上),缩容冷却 3–5 分钟(防抖动),与本题【关键数字】同口径;实例启动含拉镜像、建连接、缓存预热,预热完成才接流;大促可定时预测扩容(不完全靠反应式)。
  4. 失败与降级:云 API 失败 → 告警人工介入,保留最小实例数下限;扩出来的实例不健康 → 自动摘除;缩容误杀 → 快速回滚;依赖(DB/Redis)容量不够时扩应用反而加速打挂——下游容量是伸缩上限。
  5. 成本:低谷缩容、竞价实例/闲置计划;无状态服务才能横向伸缩;本地缓存实例变多可能降低命中率,需权衡。

【原理溯源】

  • 为什么无状态化是弹性伸缩的前提? 有状态服务(本地 Session、本地文件、本地队列)扩容后,新实例没有旧状态,请求路由到新实例会出错;缩容时旧实例的状态丢失。无状态后任意实例可替换,加机器就是纯增加处理能力,减机器就是纯减少成本——这是弹性伸缩能成立的基础。
  • 为什么扩容有“冷启动”问题? 新容器启动需要:拉取镜像(可能几十秒到几分钟)、进程启动、JVM 预热(JIT 编译、连接池建立、缓存加载)。在预热完成前,新实例处理慢,甚至可能超时。所以:① 预置“温备”实例(提前启动好但不接流量);② 镜像预拉取到节点;③ 使用较小的基础镜像加速拉取。扩容不是点击即生效,必须算上冷启动时间。
  • 为什么缩容必须“优雅下线”? 直接杀进程,正在处理中的请求会中断。优雅下线:① 先从负载均衡摘除该实例(不再接新请求);② 等待存量请求处理完(graceful period,如 30s);③ 再销毁进程。K8s 的 preStop hook + terminationGracePeriodSeconds 就是干这个的。保证缩容不中断用户请求。
  • 为什么要冷却时间(cooldown)? 指标有波动:CPU 瞬时到 90% 可能是 GC 或突发,马上扩了,1 分钟后又降下来,缩容——抖动扩容浪费资源且增加不稳定。冷却时间:扩容后 N 分钟内不再触发缩容,缩容后 M 分钟内不再触发扩容,给系统稳定期。另外用“持续超过阈值一段时间”而不是“瞬时超过”作为条件。
  • 为什么数据库不能随意弹性伸缩? 应用无状态可随意加减;数据库有状态,扩容意味着分片(数据迁移、路由改造)、主从切换,不能秒级完成。所以数据库通常按峰值容量规划,不参与秒级弹性;或者用云数据库的只读副本扩展读,写扩展仍需分库分表提前设计。

【选型判断树】

弹性伸缩怎么落地?
前提检查:
├─ 应用是否无状态?否 → 先无状态化
└─ 是否容器化/镜像化?否 → 先容器化

指标选择:
├─ CPU 为主(通用)
├─ QPS/自定义指标(业务感知更准)
└─ 多指标:CPU+RT+队列长度

平台选择:
├─ K8s → HPA(Pod 级)
├─ 云主机 → 弹性伸缩组(AS)
└─ Serverless → 自动(但冷启动更明显)

关键配置:
├─ 扩容阈值:CPU 60–70% 持续 2–3 分钟(与正文同一口径,别一处写 1–2 分钟一处写 2–3 分钟)
├─ 缩容阈值:CPU 30% 以下持续 5–10 分钟
├─ 冷却时间:扩 0–1min,缩 3–5min
└─ 最大/最小实例数:防无限扩/缩到 0

有状态组件:
└─ DB 不参与秒级弹性,按峰值规划

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“弹性伸缩题。我会讲前提、指标、流程、风险”
0:30–1:00前提无状态化 + 容器化;数据库等有状态组件不参与秒级弹性
1:00–3:00实现HPA/AS;指标选择;扩容流程(启动→注册→接流量);缩容优雅下线
3:00–4:00风险冷启动、抖动扩容、缩容中断、DB 瓶颈
4:00–5:00收尾“弹性伸缩的本质是让计算资源随负载变化,前提是无状态,难点在冷启动与抖动控制”

【关键数字】

参数经验值说明
扩容触发CPU >60% 或 RT >阈值,持续 2–3min提前扩;观察窗口与正文第 5 条同值
缩容触发CPU <30% 持续 5–10min谨慎缩
冷启动时间容器 30s–2min;JVM 预热另加 1–3min预置温备可掩盖
扩容冷却0–1 分钟允许快速扩
缩容冷却3–5 分钟防抖动
优雅下线等待15–60s等存量请求完成
最小实例数≥2(高可用)不能缩到 1
镜像拉取10s–几分钟取决于大小与网络
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“有状态服务能做弹性伸缩吗?” → 很难直接做。有状态服务扩容需要数据迁移/会话同步,不是秒级操作。变通方案:① 把状态外置(Session→Redis)后变无状态再弹性;② 有状态服务按固定容量规划,不参与秒级弹性;③ 数据库用只读副本扩读,写用分库分表提前规划。

L2|“扩容了但流量还是扛不住,为什么?” → 检查:① 冷启动未完成,新实例还没真正处理能力;② 扩的是应用,瓶颈在 DB/Redis(共享资源没扩);③ 注册中心/负载均衡还没把流量切过去;④ 扩容数量不够(阈值设太松);⑤ 限流把新流量挡住了。扩容只解决应用层 CPU,不解决共享资源瓶颈。

L3|“怎么防止抖动扩容?” → ① 冷却时间:扩容后一段时间不缩容,缩容后一段时间不扩容;② 用“持续超过阈值”而不是瞬时值;③ 多指标综合(CPU+RT+队列),避免单一指标误判;④ 设置最大/最小实例数边界;⑤ 缩容用更保守的阈值(缩容比扩容更谨慎,因为缩错了直接影响用户)。

【评分标准】

档位答案特征
60 分能说出“用 K8s HPA”
80 分知道无状态化前提、指标选择、优雅下线
95 分能解释冷启动问题与预置温备;设计冷却时间防抖动;指出 DB 不参与秒级弹性;能处理“扩了还扛不住”的排查

【关联题】

  • 同簇: 第 7 题(高可用,冗余)、第 1 题(水平扩容)、第 2 题(突发流量应急)
  • 前提: 第 111 题(分布式 Session,无状态化)
  • 瓶颈转移: 第 55 题(分库分表,DB 扩展)
  • 国企 / 金融版: 第 222 题(同城双活,资源规划)

【自测】

  1. 为什么无状态化是弹性伸缩的前提? 参考答案: 无状态后任意实例可替换,加减机器纯增减处理能力;有状态则需要数据迁移/会话同步,无法秒级弹性。
  2. 缩容时直接杀进程有什么问题? 参考答案: 正在处理的请求会中断。必须优雅下线:先摘流量,等存量完成,再销毁。
  3. 什么是抖动扩容?怎么防? 参考答案: 指标波动导致频繁扩了又缩。用冷却时间、持续阈值、多指标、缩容更谨慎来防。

19. 秒杀开始前 1 秒请求已打满,脚本绕过前端直接抢(防提前/防脚本) ​

【考察内容】防刷与业务防提前

【题目】秒杀 10 点开始,9:59:59 服务器就已经被打满(有人提前请求);同时有脚本绕过前端按钮直接调后端接口抢购,真人用户根本点不进去。这两个问题怎么从接口层面解决?

【参考答案】防提前/防脚本:时间同步 + 资格凭证 + 服务端裁决:

  1. 容量估算(步进):假设 10 万资格用户,开抢前 1 秒恶意请求可能达 10 万+ QPS。真实库存 1 万,预期成功请求仅 1 万次。若开放接口无凭证,脚本可打满连接池,真实用户进不来。目标:开抢前 90%+ 请求应在“资格校验”层被快速拒绝,只放行有效凭证流量(「发多少资格」与「放行多少 QPS」是两个数,别混:本条前提是有 10 万资格用户,若按【关键数字】「token ≤ 库存或略多」只发 1 万个 token,则另外 9 万资格用户会被自己的资格校验挡掉;若给 10 万资格用户各发一个,则放行阈值要 ≈10 万 QPS 量级,此时真正收口在库存预扣层(1 万)而不是这一层。答法:资格校验层只负责「无凭证一律拒」,把有凭证的量全放到预扣层,由预扣层 + 库存决定成功数——10 万+ 请求若放行 2–5 万,只挡掉五到八成,达不到上面 90%+ 的拒绝目标)。
  2. 防护设计:① 统一时间:以服务端时间倒计时,前端仅展示;接口开抢前一律拒绝(服务端判断,不信客户端);② 动态 URL/签名:活动前才下发随机短时路径;③ 预取 Token/资格码:需登录+风控通过才能领,开抢时校验 Token 未用;④ 一码一用、绑定用户/设备。
  3. 脚本对抗:验证码/滑块在领 Token 阶段消耗脚本;频次限流+异常设备特征;对无 Token 请求不进核心逻辑,直接 403。
  4. 失败与降级:时间服务异常 → 以配置中心/本机单调时间兜底;风控过严误杀 → 申诉通道与白名单;Token 服务挂 → 降级为登录态限流+临时关闭活动入口,避免裸奔。
  5. 口径:把“能否参与”提前到开抢前完成核验,开抢瞬间只做极轻的 Token 与库存判定。

【原理溯源】

  • 为什么前端倒计时防不了脚本? 前端 JS 控制的是“真人看到的按钮”,脚本根本不经过浏览器渲染,直接发 HTTP 请求。前端校验对自动化攻击完全无效——这是安全的基本原则:永远不信任客户端。所有关键校验(时间、资格、库存)必须在服务端做。
  • 为什么要服务端时间校验? 即使有人提前发请求,服务端检查 now < startTime 则拒绝。看似简单,但要注意:① 时间必须是服务端时间,不能用客户端传的时间;② 集群多机器时钟要同步(NTP),否则 A 机认为未开始、B 机认为开始了;③ 时间窗口边界要精确(用毫秒时间戳比较)。
  • 为什么 URL 动态化有效? 若秒杀接口 URL 是固定的(如 /api/seckill/buy?id=123),脚本可以提前写好直接打。动态 URL:活动开始前才下发带签名的随机路径(如 /api/seckill/xxx?sign=abc&ts=...),脚本在活动前不知道真实地址。配合签名校验(防篡改)和有效期(防囤积),把“知道地址”变成门槛。本质是隐藏并保护业务入口。
  • 为什么要资格预审/token? 动态 URL 仍可能泄露(用户分享、抓包)。资格 token 把链路拆成两步:① 先获取资格 token(有频控、与用户绑定、SETNX 防重复领取);② 秒杀时凭 token 下单。脚本即使知道秒杀 URL,没有合法 token 也调不通。把“拼速度”变成“拼资格”,从根本上削弱脚本优势。
  • 为什么 SETNX 防 token 重复使用? token 应该一人一次。用 SET token_key 1 NX EX ttl(不能写成 SETNX token_key 1 EX ttl——SETNX 只接受 key 与 value 两个参数,带不了过期时间):第一个请求返回 OK 设置成功,后续请求因 NX 不成立返回 nil,直接拒绝。原子性由 Redis 单线程保证,避免“先查后用”的竞态。

【选型判断树】

防提前+防脚本,分层设防:
前端(防小白)
└─ 按钮置灰 + 倒计时(可被绕过)

接口(防直接调用)
├─ 服务端时间窗口校验(必须)
├─ URL 动态化 + 签名(隐藏入口)
└─ 资格 token 预发(SETNX 防重用)

风控(防专业脚本)
├─ 用户/设备/IP 频控
├─ 验证码(滑块/图形)
└─ 设备指纹 + 行为分析

兜底
└─ 网关限流,超阈值排队/拒绝

口诀:前端只防小白,服务端时间必须校验,URL 动态+token 预发是关键,风控兜底。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“防提前+防脚本题。核心原则:永远不信任客户端,校验必须在服务端”
0:30–1:00两个问题提前请求:服务端时间校验;绕过前端:URL 动态化+token
1:00–3:00分层方案前端(弱)→ 接口(时间/URL/token)→ 风控(频控/验证码)→ 限流兜底
3:00–4:00实现细节token 用 SETNX 防重复;时钟同步;签名有效期
4:00–5:00收尾“安全的本质是让攻击成本高于收益;前端校验只是体验,服务端校验才是安全”

【关键数字】

参数经验值说明
token 有效期30s–5min防转发囤积
token 与用户绑定必须防分享
时钟同步NTP,偏差 <100ms集群时间一致
URL 签名有效期与活动窗口一致或更短防提前使用
频控阈值每用户 1–5 次/秒按业务
时间校验精度毫秒级时间戳避免秒级边界问题
资格 token 数量按「资格人数」发放(题干 10 万资格就发 10 万个),「≤库存」只适用于资格数≈库存的场景否则另外 9 万资格用户会被自己的资格校验挡掉;真正收口在库存预扣层(1 万),不在发放层
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“只靠前端倒计时行不行?” → 不行。脚本直接发 HTTP,不经过前端。前端倒计时只能防普通用户手滑提前点,对自动化攻击无效。所有关键校验(时间、资格、库存)必须服务端做。

L2|“token 泄露了怎么办?用户分享给脚本。” → 多重绑定:① token 与用户 ID 绑定,换了用户无效;② 与设备指纹绑定(可选);③ 短有效期;④ SETNX 保证一人一次;⑤ 使用频控。即使泄露,也只能用一次且绑定原用户,脚本无法批量使用。

L3|“服务端时间校验,集群机器时间不一致怎么办?” → ① 所有机器必须 NTP 对时,监控时钟偏差;② 时间判断统一用中心时间服务(或 Redis TIME 命令)而不是本机时间;③ 缓冲只能放在「开始后」一侧:开始后极小宽容窗口内到达的请求照常受理,开抢前一律拒绝(把宽容期放在开抢前,等于把本题题干投诉的提前抢合法化);④ 活动开始/结束用配置中心统一下发,不要各机器独立判断。

【评分标准】

档位答案特征
60 分能说出“加验证码、加时间校验”
80 分知道前端校验可绕过;了解 URL 动态化和 token 预发
95 分能解释“永远不信任客户端”原则;设计 token 的 SETNX 防重用与多维绑定;处理集群时钟一致问题

【关联题】

  • 同簇: 第 4 题(防刷风控)、第 6 题(秒杀系统全貌)、第 3 题(限流)
  • 安全原则: 第 192 题(重放攻击,时间戳+签名)、第 197 题(越权,服务端校验)
  • 系统设计: 第 157 题(预约资格系统)

【自测】

  1. 为什么前端倒计时防不了脚本? 参考答案: 脚本直接发 HTTP 不经过前端;安全原则是永远不信任客户端,关键校验必须服务端做。
  2. URL 动态化是怎么防脚本的? 参考答案: 活动前不发真实接口地址,脚本不知道打哪里;配合签名和有效期防止泄露后滥用。
  3. token 为什么用 SETNX? 参考答案: 原子性保证一人一次,避免“先查后用”的并发竞态导致 token 被多次使用。

20. 从点击下单到支付回调,全链路数据要“不丢、不错、不乱”(下单全链路一致性) ​

【考察内容】下单全链路一致性设计(数据不丢、不错、不乱)

【题目】用户从点击“下单”到支付成功,中间经过订单落库、库存扣减、支付回调、状态流转多个环节。任何一个环节出问题,就会出现“扣了钱没订单”“扣了库存没订单”“订单状态永远处理中”。你如何保证全链路数据不丢、不错、不乱?

【参考答案】下单全链路一致性要求不丢、不错、不乱:

  1. 容量与链路估算(步进):假设下单峰值 5000 TPS,链路:校验→扣库存→创建订单→计价优惠→支付单→履约。同步串联 RT 可能 >500ms,且任一步失败都要补偿。采用“本地消息表/TCC/可靠消息”组合,按步骤可否回滚选择策略;资金相关步骤强一致,履约类最终一致。
  2. 不丢:业务库操作与消息发送同事务(本地消息表)或使用事务消息;下游消费成功要 ACK;超时与未知状态要有对账任务扫描补齐。
  3. 不错:幂等(订单号/请求 Token 唯一键);状态机合法迁移(只能创建→支付→发货,不能乱跳);金额与优惠计算以服务端为准,防篡改;分布式事务失败要回滚库存/优惠券占用。
  4. 不乱:顺序要求高的状态变更用同一分片键串行或单线程消费;支付回调乱序时以状态机幂等合并;重复回调只生效一次。
  5. 失败与降级:支付渠道超时 → 查询订单状态再决定,禁止盲目重扣;库存回滚失败 → 补偿任务+对账;MQ 故障 → 同步降级并限流;人工差错处理台处理长尾不一致。监控:补偿次数、对账差异、状态机违规告警。

【原理溯源】

  • 为什么“扣了钱没订单”会发生? 典型场景:支付回调先到,但订单创建消息还在 MQ 里没消费;或订单服务写库失败但支付已成功。根因是跨系统的操作无法用本地事务包住。解法:① 支付回调与订单状态更新要幂等且可重试;② 定时对账任务比对“支付流水”与“订单状态”,发现有支付无订单则补单或退款;③ 超时主动查单(不只依赖回调)。
  • 为什么订单状态机很重要? 若没有状态机,任何代码都可能把“待支付”直接改成“已发货”,或把“已取消”改成“已支付”。状态机定义合法流转(待支付→已支付→已发货→已完成;待支付→已取消),非法跳转直接拒绝。这从机制上防止“状态错乱”,而不是靠开发者自觉。
  • 为什么 MQ 要“同订单同分区”保证顺序? 订单的“创建→支付→发货”是有序事件。若乱序消费,可能“发货消息”先于“支付消息”处理,导致未支付就发货。同一订单的所有消息路由到同一分区,分区内天然有序,消费者按序处理。代价是热点订单可能分区倾斜,但对正确性是必要的。
  • 为什么支付回调必须幂等? 支付平台可能重试回调(网络超时、处理慢都会重试)。若不幂等,重复回调会导致重复发货、重复加积分。幂等手段:用支付流水号做唯一键,已处理则直接返回成功。幂等是应对“至少一次投递”的必备防御。
  • 为什么需要本地消息表/事务消息? “订单落库成功”和“发消息通知库存/积分”是两个动作,无法在一个本地事务里完成。先写库后发消息:写库成功但发消息前宕机,消息丢了。本地消息表:把“订单数据”和“待发消息”放进同一个本地事务,后台任务扫描未发送的消息投递。事务消息(RocketMQ)是类似思想的中间件实现。消除“业务成功但消息丢失”的窗口。

【选型判断树】

全链路一致性分层保障:
接入层 → 限流 + 幂等键(防重复提交)
业务层 → 原子扣减 + 状态机(防超卖、防乱流转)
异步层 → confirm + 幂等 + 同分区(不丢、不重、有序)
数据层 → 分布式 ID + 备份(不重、不丢)
兜底层
├─ 本地消息表/事务消息(业务与消息同事务)
├─ 定时对账(订单 vs 支付 vs 库存)
└─ 支付超时主动查单(不只依赖回调)

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“全链路一致性综合题。我会按接入/业务/异步/数据/兜底五层来讲”
0:30–1:00定界链路:下单→扣库存→支付→回调→状态流转;风险:不丢、不错、不乱
1:00–3:30五层展开每层讲“风险+对策”:幂等、原子扣减、状态机、MQ 可靠、对账
3:30–4:30典型故障“扣了钱没订单”怎么补;“状态永远处理中”怎么查单
4:30–5:00收尾“一致性没有银弹,核心是幂等+状态机+对账兜底,接受最终一致”

【关键数字】

参数经验值说明
支付超时15–30 分钟超时关单+回补库存
查单补偿超时后主动查,不只等回调回调可能丢
对账频率分钟级~T+1资金类建议准实时
幂等键支付流水号/订单号+版本唯一索引
状态机禁止非法跳转机制强制
MQ acksall(至少)防丢
消费重试3–5 次后死信防堵
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“Redis 预扣了但 MQ 消息丢了,库存不一致怎么办?” → DB 是权威。流程上:Redis 预扣成功 → 创建订单(DB)→ 发消息。若消息丢,订单已落库,对账任务发现“有订单未扣 DB 库存”则补扣。另外:本地消息表保证业务与消息同事务;Redis 预扣要有过期回补。

L2|“支付回调一直没来,订单永远‘处理中’怎么办?” → 不能只依赖回调。① 设置支付超时(如 15 分钟),超时未回调则主动调支付平台查单接口;② 查单确认已支付则补推进订单状态;③ 确认未支付则关单+回补库存;④ 定时任务兜底扫描长时间处理中的订单。回调是通知,查单是核实,两者都要。

L3|“怎么设计订单状态机?” → 明确定义状态集合与合法流转:待支付→已支付→已发货→已完成;待支付→已取消;已支付→退款中→已退款。非法流转(如已取消→已支付)在代码层拒绝并告警。状态变更要用条件更新(UPDATE ... WHERE status=?)保证并发下只有一次成功。状态机是防错乱的机制保障,不是文档约定。

【评分标准】

档位答案特征
60 分能说出“加事务、加锁、加对账”
80 分能分层展开(接入/业务/异步/数据/兜底);知道幂等、状态机、MQ 可靠
95 分能设计“扣钱没订单”的补偿;解释支付超时主动查单;说明本地消息表的作用;状态机用条件更新实现

【关联题】

  • 同簇: 第 5 题(防超卖)、第 12 题(MQ 异步化)、第 99 题(分布式事务选型)
  • 消息可靠: 第 80 题(不丢)、第 81 题(不重)、第 82 题(有序)、第 92/93 题(事务消息/本地消息表)
  • 幂等: 第 110 题(接口幂等)、第 117 题(重复下单)
  • 国企 / 金融版: 第 219 题(日终对账)、第 218 题(跨行转账一致性)

【自测】

  1. “扣了钱没订单”怎么发现和修复? 参考答案: 定时对账比对支付流水与订单状态;超时主动查单;发现有支付无订单则补单或退款。
  2. 为什么支付回调必须幂等? 参考答案: 支付平台会重试回调,不幂等会导致重复发货/重复加积分;用流水号唯一键去重。
  3. 订单状态机的作用是什么? 参考答案: 定义合法流转,非法跳转拒绝;用条件更新保证并发安全;防止状态错乱。

21. 大促前压测 TPS 上不去、RT 抖动,老板要优化方案(性能优化方法论) ​

【考察内容】性能优化方法论(先测量后优化、单变量、数据验证)

【题目】大促前压测,核心下单接口 TPS 卡在 3000 上不去,RT 忽高忽低。老板要一份系统性的优化方案。你从“目标定义→瓶颈定位→方案排序→实施验证→上线回归”按什么标准步骤推进,才能保证每步优化可量化、不回退?

【参考答案】性能优化方法论:定位瓶颈 → 分层优化 → 量化验证:

  1. 容量背景(步进):假设目标大促峰值 2 万 TPS,压测只到 3000 且 RT 抖动(与题干同一口径)。先算是否“线程被 RT 吃掉”:并发线程 200 时,理论 TPS≈200/RT;若 RT 从 20ms 抖到 200ms,TPS 从 1 万掉到 1000。抖动往往比均值更致命。
  2. 定位顺序:看监控分层——入口 QPS、应用 RT/GC/线程池、缓存命中、DB 慢 SQL/锁、网络与连接池等待;用压测阶梯找拐点,用链路追踪定位最慢一跳;区分计算密集 vs IO 密集 vs 锁竞争。
  3. 优化抓手(题干要「方案排序」,先给排序标准再列手段,否则只是罗列):排序标准=投入产出比(预期提升 ÷ 改动成本+风险),先用监控确认限制吞吐的那一层,低垂果实(索引/缓存命中率/批量/超时与池参数)先做,架构级(异步/分片/多活)后置。手段清单:SQL/索引与缓存命中率;串行改并行/批量;同步改异步;锁粒度与无锁结构;序列化与日志;JVM 参数与 GC;热点与大对象;连接池配置。
  4. 失败与降级:优化期保留回滚;灰度对照;限流保护线上;若短期不能优化完,先靠扩容+限流+降级保大促。
  5. 验证:每项优化有 before/after 压测数据与分层指标;建立容量模型(QPS-机器-RT)供下次预算。本质:用证据链替代经验主义,先解决限制吞吐的那一层。

【原理溯源】

  • 为什么必须“先测量再优化”? 不测量就优化是瞎猜。TPS 卡在 3000 可能是 CPU 满、锁等待、下游慢、连接池耗尽——原因不同,手段完全不同。盲目加缓存可能无效,盲目加线程可能更糟(切换开销)。压测+监控定位到瓶颈资源,才能对症下药。这是性能优化的第一原则,也是最容易被跳过的一步。
  • 为什么 RT“忽高忽低”比“稳定偏高”更难查? 稳定偏高通常是固定瓶颈(慢 SQL、下游 RT),找到就能修。RT 抖动往往来自间歇性争用:锁竞争(无冲突时快、有冲突时慢)、GC 停顿(周期性)、连接池排队(请求量波动时等待时间变化)、下游偶发慢。抖动说明瓶颈不是“一直存在”,而是“有时触发”,需要看 P99/P999 分位数和时间相关性,不能只看平均。
  • 为什么方案要按“投入产出比”排序? 优化资源有限(人力、时间),必须先做低成本高收益的:加索引可能 1 小时搞定提 30%;分库分表可能 2 个月提 3 倍。大促前时间窗口有限,先摘低垂果实,再攻架构级改造。排序还能降低风险——小改动容易验证和回滚。
  • 为什么“一次只改一个变量”? 同时改索引和线程池参数,压测结果变好了,你不知道是哪个起作用;结果变差了,不知道该回滚哪个。单变量控制是科学实验的基本方法,让每步优化的效果可归因。实践中可以用 A/B 压测组对比,但至少保证同一时间只有一个变量在变。
  • 为什么要“沉淀基线”? 没有基线,下次压测你不知道“3000 TPS 是好是坏”。基线记录了:当前硬件配置下的 QPS/RT/错误率/资源利用率。每次大促前压测对比基线,性能回退可以立即发现(如某次发布引入慢 SQL,TPS 从 3000 降到 2500)。基线是性能治理的锚点。

【选型判断树】

性能优化五步法:
1. 定目标 → 指标(QPS/RT/P99/错误率)+ 目标值
2. 找瓶颈 → 压测 + 监控 + 工具(谁先饱和)
   ├─ CPU 满 → 计算密集 → 优化算法/序列化/扩容
   ├─ CPU 空但 TPS 低 → 等待瓶颈 → 锁/下游/IO/连接池
   ├─ DB 饱和 → SQL/索引/缓存/读写分离/分库分表
   └─ GC 停顿 → 堆调优/对象分配/垃圾收集器
3. 排方案 → 按投入产出比(低成本高收益优先)
4. 逐步实施 → 一次一个变量,每步重压对比
5. 回归沉淀 → 全链路回归 + 基线记录 + 告警

口诀:先测量再优化,先低垂再架构,单变量可归因,有基线知回退。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“性能优化方法论题。核心原则:先测量后优化、单变量、数据验证”
0:30–1:00定界TPS 卡 3000、RT 抖动;先搞清目标是多少(大促峰值 × 冗余系数)
1:00–3:00五步展开定目标→找瓶颈(工具+方法)→排方案(投入产出比)→逐步实施→回归沉淀。每步说清“为什么”
3:00–4:00关键细节RT 抖动怎么查(分位数+时间相关性);单变量控制的原因;基线怎么建
4:00–5:00收尾“性能优化不是玄学,是可测量、可归因、可验证的工程过程”

【关键数字】

参数经验值说明
目标冗余峰值的 1.5–2 倍应对模型偏差
拐点定义RT 开始陡升的 QPS生产运行在拐点 70% 以下
优化对比基准同环境、同数据、同模型否则不可比
P99 vs 平均必须同时看平均会掩盖尾部
每步验证改一项→重压→记录单变量控制
基线更新每次大促前/重大发布后持续维护
瓶颈转移解决一个后要重新定位迭代过程
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“你优化了 SQL、加了缓存,TPS 还是上不去,怎么办?” → 回到测量:现在是哪个资源先饱和?瓶颈会转移——解决了 DB,可能应用 CPU 或线程池变成新瓶颈。每优化一步重新压测,看瓶颈是否转移。这是迭代过程,不是一步到位。关键是不要在旧瓶颈上继续投入。

L2|“压测 TPS 上不去但 CPU 很低,说明什么?” → 瓶颈在等待不在计算。可能原因:锁竞争(jstack 看 BLOCKED)、下游服务慢(调用链看哪一跳)、IO 等待、连接池耗尽、GC 等待。这时加机器或加线程可能无效(如果等待的是共享资源),要消除等待源。CPU 满是计算瓶颈,CPU 空是等待瓶颈——优化方向截然相反。

L3|“老板问'你怎么证明这次优化真的有效',怎么答?” → 同条件压测对比:① 优化前后的 QPS/RT 曲线(同一压测模型、同一环境);② 关键指标对比表(TPS、P99、错误率、CPU/内存);③ 如果可能,A/B 组对照。不能只说“感觉快了”,要有数据。同时说明:优化目标是否达成、瓶颈是否真正消除、有没有引入新问题。

【评分标准】

档位答案特征
60 分能说出“看监控、加缓存、加索引”
80 分有五步框架(目标→定位→方案→验证→回归);知道先测量再优化;会用工具定位瓶颈
95 分能解释 RT 抖动的排查方法(分位数+时间相关性);按投入产出比排序方案;坚持单变量控制;强调基线沉淀;能回答“怎么证明有效”

【关联题】

  • 同簇: 第 14 题(吞吐优化具体手段)、第 9 题(接口变慢排查)、第 10 题(全链路压测)
  • 工具延伸: 第 51 题(慢 SQL)、第 52 题(索引失效)、第 169 题(线程池打满)
  • 架构级: 第 12 题(异步化)、第 17 题(多级缓存)、第 55 题(分库分表)
  • 对比参照: 第 14 题是“具体怎么优化”,本题是“优化过程怎么组织”

【自测】

  1. 性能优化的第一步是什么?为什么? 参考答案: 测量定位瓶颈。不测量就优化是瞎猜;不同瓶颈需要完全不同的手段。
  2. RT“忽高忽低”和“稳定偏高”,排查思路有什么不同? 参考答案: 稳定偏高通常是固定瓶颈(慢SQL/下游);抖动来自间歇性争用(锁/GC/连接池排队),要查分位数和时间相关性。
  3. 为什么要“一次只改一个变量”? 参考答案: 让每步优化的效果可归因;同时改多项,结果变好/变差都不知道是哪项起作用。

22. 大促高峰资源紧张,要让用户“能用、不崩、不丢单”(降级兜底设计) ​

【考察内容】容量保障与体验设计的平衡

【题目】大促高峰期,系统资源已经逼近极限,继续硬扛所有功能必然全崩。你怎么通过降级、限流、兜底等设计,让用户在最坏情况下依然“能完成下单、页面不崩、订单不丢”?哪些功能可以牺牲、哪些必须保?

【参考答案】降级兜底设计:分级、可开关、保核心、可恢复:

  1. 容量估算(步进):大促峰值假设 5 万 QPS,降级后目标:核心链路(下单/支付/登录)容量优先保障,非核心(推荐/评论/积分明细/营销位)关闭后预计可释放 30%–50% 线程与连接——这句话只在「核心链路自身压测容量 ≥ 题干峰值」时够用:若核心链路压测只有 2 万 QPS 而峰值 5 万,把非核心全关也补不上缺口,必须同时扩容或把入口限流压到 2 万;所以先给核心链路的压测容量这个数,再说降级能保多少,否则「释放 30%–50%」是被追问打回的空口数字。限流阈值按核心链路压测容量 80% 设定;资源紧张时非核心 QPS 目标直接打到 0(兜底静态/默认值)。
  2. 降级层次:① 功能降级:关推荐、换静态页、简化查询字段;② 读降级:缓存陈旧数据/默认值;③ 写降级:可延迟操作入 MQ/异步任务;④ 依赖降级:第三方短信/风控超时走简化策略;⑤ 终极:整体排队/只读模式。
  3. 开关与预案:降级开关集中配置、秒级生效、按功能粒度;预案分级(L1 关非核心 → L2 限制读写 → L3 只读排队);明确触发条件(CPU/RT/错误率)。
  4. 失败与恢复:降级配置错误 → 快速回滚;过度降级影响业务 → 监控核心转化率;恢复时逐步放量观察,防止瞬间回流量打挂。
  5. 口径:降级不是故障后的手忙脚乱,而是平时设计好的资源再分配方案;目标是用户“能用、不崩、关键操作不丢”。

【原理溯源】

  • 为什么要“分级”而不是一刀切全降级? 资源有限时,如果所有功能一起降,核心链路(下单、支付)也被牺牲,业务直接损失。正确做法是识别核心与非核心:推荐、积分、通知可以降(用户几乎无感),下单、支付必须保(直接关系交易)。降级的本质是资源再分配——把非核心占用的线程、连接、CPU 腾出来给核心。
  • 为什么排队比直接拒绝好? 直接 502 用户体验极差,且不知道发生了什么。排队给用户明确反馈:“前方拥挤,预计等待 X 秒”,用户可以等待或离开。系统侧:排队把峰值流量缓冲下来,按处理能力匀速放行,避免瞬间打垮。关键是排队必须有超时——不能让用户“排死队”。
  • 为什么“能完成下单”比“页面完美”更重要? 大促的核心目标是成交。页面装饰性元素(推荐位、广告、积分弹窗)可以降级为静态内容或直接隐藏;但下单、支付、订单查询必须保。用户来大促是为了买东西,不是看推荐。降级的优先级应该跟着业务价值走,不是跟着技术难度走。
  • 为什么“不丢单”需要兜底+补偿? 资源紧张时,写操作可能失败(超时、连接池耗尽)。如果直接返回失败,用户可能放弃。正确做法:① 写操作先落 MQ(快速返回“已受理”),异步处理;② 失败进入重试队列;③ 定时对账任务扫描“受理了但未完成”的记录,补处理。用户看到的是“成功”,系统在后台保证最终成功——这就是最终一致在降级场景的应用。
  • 为什么降级开关必须“预置好”? 大促高峰时你不可能现场写降级代码。所有降级开关(配置中心控制)必须在大促前预置好,支持秒级打开/关闭。同样,降级策略要演练——不演练的降级开关可能本身有 bug。降级是应急预案,不是临时发挥。

【选型判断树】

降级怎么设计?按链路分级:
核心链路(必须保)
├─ 登录、浏览、下单、支付、订单查询
├─ 资源优先保障,限流阈值设高
└─ 降级手段:排队 + 简化流程(跳过风控异步校验)

非核心链路(可以降)
├─ 推荐、积分、通知、营销弹窗、个性化
├─ 资源紧张时主动降级
└─ 降级手段:返回兜底数据/关闭功能/延迟处理

降级层次:
├─ 接口级 → 返回默认值/缓存快照
├─ 页面级 → 静态化页面(去掉动态元素)
├─ 依赖级 → 弱依赖失败不阻断主流程
└─ 功能级 → 直接关闭非核心功能

排队机制:
├─ 超出容量 → 排队(MQ 缓冲)
├─ 排队超时 → 明确失败 + 引导重试
└─ 排队状态 → 用户可查询(进度条)

兜底保障:
├─ 写操作失败 → 重试队列
├─ 关键数据 → 先落 MQ 再异步
└─ 对账任务 → 扫描"受理未完成"补处理

口诀:核心保、非核心降;排队别拒绝;先受理后处理;对账兜底不丢单。

【口述骨架】(5 分钟)

时间任务内容
0:00–0:30定性“降级兜底设计题。核心目标:能下单、不崩、不丢单”
0:30–1:00定界资源逼近极限,必须取舍;先明确哪些是核心、哪些可牺牲
1:00–3:00四块展开分级保障(核心/非核心)→ 降级预案(接口/页面/依赖/功能)→ 排队机制(超时处理)→ 兜底补偿(MQ+对账)
3:00–4:00关键细节降级开关必须预置;排队超时怎么设;“不丢单”的最终一致怎么保证
4:00–5:00收尾“降级的本质是在容量不足时做资源再分配,优先保障核心交易链路,用异步+对账保证不丢单”

【关键数字】

参数经验值说明
核心链路登录、浏览、下单、支付必须保
非核心链路推荐、积分、通知、营销可降级
排队超时30s–2min超过给明确失败+引导
排队提示预计等待时间透明化比转圈好
降级开关生效秒级(配置中心)必须预置
写操作重试3–5 次后死信防无限重试
对账频率分钟级~T+1资金类建议准实时
降级演练大促前必须验证开关有效
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩

【追问链】(三层)

L1|“降级和限流有什么区别?” → 限流是控制进入系统的流量(挡掉超额请求),降级是简化系统处理逻辑(返回兜底数据/关闭功能)。通常组合使用:先降级非核心释放资源,再限流控制总量。一句话:限流管“进多少”,降级管“做多少”。

L2|“用户排队了,但处理失败了怎么办?” → 分层处理:① 排队时先返回“已受理”(用户看到成功);② 异步处理失败自动重试(限次数+退避);③ 重试耗尽进死信队列,告警人工;④ 定时对账扫描“受理了但未完成”的记录,补处理或通知用户。关键是用户侧的“成功”与系统侧的“最终成功”要对齐。

L3|“降级开关打开了,但用户投诉功能少了,产品有意见怎么办?” → 用数据说话:① 降级前后对比:不降级会全崩(0% 可用),降级后核心功能 100% 可用、非核心暂时关闭;② 降级时长:通常只持续峰值的几分钟到几十分钟;③ 用户体验数据:降级期间的下单成功率 vs 如果全崩的成功率(0)。降级是权衡不是失职,关键是提前和产品对齐“哪些可以降、降多久”。

【评分标准】

档位答案特征
60 分能说出“限流、降级、加机器”
80 分能分级(核心/非核心);知道排队比拒绝好;了解降级层次(接口/页面/依赖/功能)
95 分能解释“资源再分配”的本质;设计排队超时处理;保证“不丢单”的最终一致;强调降级开关预置与演练;能处理产品投诉

【关联题】

  • 同簇: 第 2 题(流量突增应急,降级是其中一步)、第 8 题(雪崩防护,降级是手段之一)、第 12 题(MQ 异步化,兜底的实现方式)
  • 系统设计: 第 20 题(全链路一致性,兜底补偿的完整方案)、第 6 题(秒杀系统,降级预案)
  • 对比参照: 第 2 题是“事中应急”,本题是“事前设计”
  • 国企 / 金融版: 第 217 题(金融级服务降级)

【自测】

  1. 降级和限流分别是什么?关系是什么? 参考答案: 限流控制进入系统的流量,降级简化系统处理逻辑。通常先降级释放资源,再限流控制总量。
  2. 为什么排队比直接 502 好? 参考答案: 排队给用户明确反馈,系统侧缓冲峰值流量按能力放行;但排队必须有超时,不能让用户“排死队”。
  3. 怎么保证“不丢单”? 参考答案: 写操作先落 MQ(用户看到成功),异步处理失败重试,对账任务扫描“受理未完成”补处理——最终一致+补偿。

持续学习,持续积累。