Skip to content

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


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

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

  • 本章真正考的那一个问题: 容量有限时,如何防止正反馈雪崩并保住核心链路?
  • 表面主题: QPS、秒杀、限流、压测
  • 题号与题量: 第 1–22 题,共 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/错误率/命中率;压测按实配容量验——32 台×2000=6.4 万,约为峰值的 1.28 倍,限流阈值取 6.4 万×80%≈5 万;若业务要求 1.5–2 倍峰值余量,需配到 38–50 台并同步按实配容量重算阈值。大促变更冻结。

【原理溯源】

  • 为什么“瓶颈通常在数据库”? 各层的单机能力差 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 上升 → 客户端超时重试 → 流量被放大 → 雪崩。限流的作用是在入口挡掉超出容量的流量,牺牲部分请求,保住整体可用。

  • 为什么接口要无状态? 无状态才能任意水平扩容(加机器即加容量)。有状态则需要会话同步,扩容成本高且引入一致性问题。

  • 元理论对应: 原理 1「资源稀缺、排队涌现与瓶颈漂移」(题群见《核心考点与元理论》§1.7)

【选型判断树】

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

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

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

时间任务内容
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 瓶颈”;给出限流阈值依据与降级预案;提到全链路压测与变更冻结;能说出缓存命中率下降的连锁后果

【关联题】

【自测】

  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 集群的压力打散到每个应用实例——这是“用水平扩展的本地内存挡集中读”的经典手法。

  • 元理论对应: 原理 2「过载动力学与雪崩正反馈」(题群见《核心考点与元理论》§2.7)

【选型判断树】

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

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

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

时间任务内容
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% 持续 2–3 分钟提前扩,不要等 90%;观察窗口与第 18 题同口径
扩容冷启动容器 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/配置中心),扩容完成后按新容量重算阈值(仍取压测容量的 70%–80%,与【关键数字】同口径),一档一档放宽、每放宽一档观察一轮。另外可以预热:大促/热点事件前,如果能预判(明星官宣其实很难预判),提前扩容+缓存预热。

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 精确控制总量,业务层按用户/商品维度做细粒度(防单用户刷爆)。每层漏斗截留一部分,下层压力更小。

  • 元理论对应: 原理 2「过载动力学与雪崩正反馈」(题群见《核心考点与元理论》§2.7)

【选型判断树】

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

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

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

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

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

时间任务内容
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+重试引导

【关联题】

【自测】

  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/行为特征,事后用离线模型或规则回溯识别新形态的作弊,再把新规则推到线上。防刷是持续对抗,不是一次配置。

  • 元理论对应: 原理 7「对抗理性与可观测的认识论」(题群见《核心考点与元理论》§7.7)

【选型判断树】

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

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

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

时间任务内容
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 分能解释“脚本赢在速度所以要引入其他维度”;说出一人一单比限频更根本;能讨论误伤处理;知道验证码会被打码平台绕过并给出组合拳

【关联题】

【自测】

  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 才判失败(=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,各自判断“还有货”,各自把读到的值减 1 后回填成绝对值 UPDATE stock SET num=0(两个事务都算出 1−1=0),后一次写覆盖前一次——最终 num=0 却卖出 2 件,这就是丢失更新;若写成相对更新 SET num=num-1,两次都执行成功则 num 被扣成 −1,即题干说的「扣成负数」。根因是“读-判断-写”不是原子的,中间窗口被其他事务插入。条件更新 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 校正。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

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

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

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

时间任务内容
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 片超热点打散,片间随机扣
对账周期分钟级~小时级只能当「纠偏」手段,不能当第一发现手段(完整口径见表下「对账周期口径」)
超卖止损窗口分钟内必须关入口否则损失扩大
订单保留规则按支付时间/创建时间保留前 N需产品事先约定
应用并发估算QPS × RT决定线程池与实例数
容量冗余峰值的 1.5–2 倍大促/突发留余量
限流阈值压测容量的 70%–80%避免排队雪崩
  • 对账周期口径: 只能当「纠偏」手段,不能当超卖的第一发现手段:题干要求「分钟内关入口」,靠小时级定时对账发现时窗口早已过去。第一发现必须是同步打点——预扣返回值 ≤0、UPDATE ... WHERE num>0 影响行数=0、或读到 num<0 立即告警并自动关闭入口(秒级);对账只做兜底核对与差异修复

【追问链】(三层)

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 万请求/分钟量级(按分钟摊只有 ≈3.3 万 QPS;开抢瞬间若 10%–30% 用户挤在同一秒点击,秒级才到数十万 QPS——这两个数量级别混着报),库存仅 1 万 → 99% 请求注定失败。网关不必放行百万,限流可设 5–10 万(略高于真实抢购吞吐)——放行量之后要接着给实例数与预扣层分片数:按第 1 题给的单机 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 条件更新+调低限流+库存快照粗过滤;消息积压 → 先定根因:下游未恢复前禁止盲目扩容(见第 83 题),确属消费能力不足才扩消费者+批量落库;超时订单自动关单回补;对账发现 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 库存、已成交订单数”,发现差异自动补偿或告警人工介入。异步系统的最终一致靠对账兜底,不靠单点可靠。

  • 元理论对应: 原理 5「不可靠信道上的正确性协议」(题群见《核心考点与元理论》§5.7)

【选型判断树】

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

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

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

时间任务内容
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 万 QPS),不是「库存件数×倍数」不必放行百万;件↔QPS 之间要按抢购窗口的秒数折算
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 挂/消息积压/少卖三类追问

【关联题】

【自测】

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

【原理溯源】

  • 为什么高可用的第一性原理是“消灭单点”? 单点意味着“该组件挂 = 系统挂”,可用性上限就是该组件的可用性。应用单机 99.9%,系统最多 99.9%。多实例让任一实例挂掉时其他实例接管,系统可用性变成“全部实例同时挂”的概率,远高于单机。这就是冗余的数学本质:并联提高可靠性。

  • 为什么无状态化是前提? 有状态(Session 在本地内存)时,请求必须回到原实例,实例挂掉后该用户的会话丢失,负载均衡器无法随意切换。无状态(Session 外置到 Redis,或用 Token)后任意实例都能处理任意请求,才能做到任意替换、随意扩容、故障秒级切换。这是水平扩展和高可用共同的前提。

  • 为什么数据库是高可用改造最难的一环? 应用无状态可随意加机器,数据库有状态——数据不能丢、写不能分叉。主从复制解决读扩展和故障切换,但引入主从延迟(从库可能落后主库数秒)和切换一致性(主挂时未同步的写可能丢失)。半同步复制(至少一个从库确认收到 binlog)降低丢数据概率,但增加写延迟。组复制/Paxos(MySQL Group Replication、TiDB)更强一致但更复杂。数据库高可用是“一致性、延迟、复杂度”的三角取舍。

  • 为什么必须有熔断和隔离,光有冗余不够? 冗余解决“自己挂”,不解决“依赖挂”。下游服务故障时,上游若同步等待会耗尽线程池,故障沿调用链向上传染(雪崩)。熔断在错误率超阈值时快速失败,不再调用故障依赖;隔离给每个依赖独立线程池,一个依赖耗尽不影响其他。冗余是提高自身存活,熔断隔离是防止被别人拖死——两者缺一不可。

  • 为什么要有混沌工程/故障演练? 高可用架构的价值只在故障时体现,但生产很少主动验证。若不演练,切换脚本可能是坏的、监控可能是盲的、预案可能没人会执行。故障演练主动注入故障(杀实例、断网、模拟延迟),在可控环境验证高可用是否真的成立。没有演练的高可用只是“纸面架构”。

  • 元理论对应: 原理 3「故障是分布式系统的默认输入」(题群见《核心考点与元理论》§3.7)

【选型判断树】

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

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

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

时间任务内容
0:00–0:30定性“高可用设计题。核心是消灭单点、自动切换、防止故障扩散、可验证”
0:30–1:00定界99.99% ≈ 53 分钟/年,包含计划内发布——所以发布策略也在范围内
1:00–3:30四块展开与四板斧同序:① 冗余(应用无状态+存储主从/集群)② 隔离(线程池/连接池、核心与非核心拆部署)③ 弹性(限流熔断降级+HPA)④ 变更可控(灰度、金丝雀、一键回滚)——第四条最容易被漏,80 分要求说全四条
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% 的时间账

【关联题】

【自测】

  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/Resilience4j 均可(Hystrix 处于维护模式,最后 release 是 2018-11 的 1.5.18,新项目不建议选,老系统沿用可以)。
  3. 隔离:线程池隔离(核心业务独立池)或信号量隔离;不同依赖独立连接池,避免一个下游耗尽共享资源。
  4. 降级:熔断后返回兜底数据(缓存快照/默认值/关闭非核心功能),保证主流程可走完;读多场景可本地缓存兜底。
  5. 失败与降级预案:依赖全挂时核心链路仍可用(付款依赖风控挂 → 降级为限额放行+异步补检);监控熔断触发率、下游 RT、线程池活跃数;预案要演练。本质:不让一个下游的失败沿调用链放大成全站不可用,快速失败优于慢速排队。

【原理溯源】

  • 为什么下游变慢会拖垮上游? 同步 RPC 下,上游线程会阻塞等待下游响应。下游变慢 → 上游线程等待时间变长 → 同样 QPS 需要更多并发线程 → 线程池打满 → 新请求排队 → 上游 RT 也上升 → 上游的上游也慢……故障沿调用链向上传染,这是雪崩的传播机制。关键点:不是下游“挂了”才会传染,“变慢”就够了,而且变慢比挂更隐蔽。

  • 为什么超时是第一道防线? 没有超时(或超时过长)时,一个慢下游能让上游线程无限期挂住,线程池迅速耗尽。合理超时(连接超时 + 读超时)保证单次调用占用线程的时间有上界,即使下游完全无响应,上游线程也会在超时后释放,去处理其他请求。超时是防止“线程被拖死”的基本手段。

  • 为什么熔断比单纯超时更进一步? 超时只限制单次调用时长,但若下游持续慢,大量请求仍然超时,错误率上升,下游被打得更惨(它还在处理这些注定超时的请求)。熔断在错误率/慢调用率超阈值后直接不再发起真实调用,快速失败返回,给下游喘息时间。半开状态定期放少量请求试探,恢复后自动闭合。

  • 为什么需要隔离? 若所有下游共用一个线程池,支付服务耗尽线程后,订单服务调用库存、积分等健康服务也没线程可用——一个依赖故障,全部依赖陪葬。线程池隔离给每个依赖独立的线程池,支付的池耗尽不影响库存调用。信号量隔离开销更低(不切换线程),但隔离粒度较粗。

  • 为什么重试会加剧雪崩? 下游已经过载时,上游重试相当于把 1 倍流量放大成 N 倍,让下游雪上加霜。正确做法:重试必须有上限次数、必须有退避间隔、只对幂等接口重试、只对可重试错误(网络超时,非业务错误)重试。无限制的盲目重试是雪崩的常见帮凶。

  • 元理论对应: 原理 2「过载动力学与雪崩正反馈」+原理 3「故障是分布式系统的默认输入」(题群见《核心考点与元理论》§2.7+§3.7)

【选型判断树】

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

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

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

时间任务内容
0:00–0:30定性“雪崩防护题。我会先讲雪崩怎么传播,再讲容错四板斧:超时、熔断、隔离、降级(与第 7 题「高可用四板斧=冗余/隔离/弹性/变更可控」是两套清单,别混称),最后讲重试陷阱”
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 分能解释“变慢也会传染”;说清熔断三态转换;指出重试放大流量的陷阱;能系统列出“加了还会雪崩”的残留原因

【关联题】

【自测】

  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)能直接显示每一跳的耗时,是定位的最快路径。

【选型判断树】

text
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 分钟;框架 D,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
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 有索引仍慢”的追问;先确认全局/单接口再动手

【关联题】

【自测】

  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 饱和就优化计算/扩容。
  • 为什么要推动整改闭环而不只是出报告? 压测的价值在“发现问题并修好”。流程:压测报告 → 列出瓶颈与风险 → 排优先级 → 优化 → 回归压测验证 → 沉淀基线。没有闭环的压测只是表演。还要把压测变成常态化(大促前必压、重大架构变更后必压),而不是一年一次。

【选型判断树】

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

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

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

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

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

时间任务内容
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 分能解读拐点与先饱和资源;设计终止条件;讲清整改闭环;能分析“压测好上线挂”的原因

【关联题】

【自测】

  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 × 0.1 个/ms = 50,或 0.5s × 100/s = 50),就能保证队列中的任务最坏等待时间不超过业务能接受的延迟。
  • 为什么生产禁止 Executors.newFixedThreadPool? 它用无界 LinkedBlockingQueue,任务堆积会 OOM;newCachedThreadPool 最大线程数是 Integer.MAX_VALUE,突发时会创建海量线程导致 OOM 或线程切换风暴。必须用 ThreadPoolExecutor 显式指定所有参数,让容量边界清晰、可控、可观测。

【选型判断树】

text
线程池参数怎么定?
第一步:判断任务类型
├─ 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 分钟;框架 B,骨架共性见文首第二节,本题只写特定内容)

时间任务内容
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。生产推荐自定义策略。

持续学习,持续积累。