CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
系统设计入门/课时/性能反模式
RawBlame
课时目录12 节

系统设计入门

  1. 01系统设计是什么
  2. 02性能与扩展基础
  3. 03一致性与 CAP
  4. 04负载均衡
  5. 05缓存模式
  6. 06异步与消息
  7. 07数据架构
  8. 08微服务与网关
  9. 09云设计模式
  10. 10可靠性模式
  11. 11性能反模式
  12. 12综合练习

性能反模式

busy DB、chatty I/O、retry storm、无缓存。

30 分钟

性能反模式

这一课你将能做到

  1. 识别 Busy Database、Chatty I/O、N+1、Retry Storm 四大反模式的症状与根因
  2. 亲手修复一处 N+1 查询(循环查询 → 批量 JOIN),并用 SQL 日志验证修复效果
  3. 为重试配置指数退避 + jitter + 上限,说出「盲目重试如何演变成全网雪崩」的完整链条

为什么要学

架构评审最容易翻车的不是「缺组件」,而是**「能跑但会死」的惯用写法**:开发环境几百条数据一切正常,上线数据一多、流量一来,DB CPU 100%、连接池打满、重试风暴雪崩。反模式清单是设计的负向检查表——知道「别做什么」往往比知道「做什么」更救命。

面试里反模式题是深挖题的入口:「你遇到过的线上性能事故?怎么定位怎么修?」「重试为什么要加退避和 jitter?」「N+1 是什么、怎么发现?」——用事故故事回答,比背概念有说服力。

人话:Busy Database 是让一个人干全公司的活(所有查询压一个库);Chatty I/O 是给同事发消息一页一条连发 50 条(该一封邮件说清的事);Retry Storm 是客户打不通电话就一直狂拨(把占线变成彻底瘫痪)。

先建立图像

四大反模式速查表(评审对照用):

反模式症状根因对策
Busy DBDB CPU 100%、连接耗尽、慢查询堆积无缓存、N+1、全表扫、报表混部缓存、批量、索引、读写分离
Chatty I/OP99 高但单次调用都快循环 RPC、每节点一次请求聚合接口、批量 API、BFF
N+1 查询一次页面请求 DB 往返几十次ORM 懒加载或循环查询JOIN / 批量 IN / 预加载
Retry Storm下游雪崩、错误率 100%、全网瘫痪无退避、无上限、超时过宽指数退避 + jitter + 熔断

本站高危路径画像:

❌ GET /api/problems/1024
     → 查题目 (1 次)
     → 循环 500 个节点,每节点查资源 (500 次)      ← N+1,页面打开 2.5 秒
     → 循环 500 个节点,每节点调一次统计服务 (500 次) ← Chatty I/O

✅ 一次 JOIN 拉全题目+资源 (1 次)
✅ 统计走批量接口 / Redis 预聚合 (1 次)

Retry Storm 的死亡链条(背下来,面试直接用):

DB 变慢 → 请求超时 → 客户端/网关重试 → 有效 QPS ×3
→ DB 更慢 → 更多超时 → 更多重试 → ×9
→ 线程池占满 → 相邻无辜接口也超时 → 全站 502
(重试流量放大 + 超时把「慢」变成「死」)

跟着做

步骤 1:发现 N+1——打开 SQL 日志

# 开发/测试环境开启 SQL 日志
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

访问一次题目详情接口,日志里数 SQL 条数:

==> Preparing: SELECT * FROM problem WHERE id = ?
==> Preparing: SELECT * FROM node_resource WHERE node_id = ?   ← 出现 500 次

一次用户请求 = 501 次 DB 往返。每往返 1ms 是 500ms 纯网络开销,加上每条查询的解析执行,这就是「详情页 2 秒」的真相。发现手段三个:SQL 日志、APM 链路追踪看 DB 段次数、慢查询之外的「QPS 与 DB TPS 比值异常」(1 QPS 打出 500 TPS)。

步骤 2:修复 N+1——循环改批量

// ❌ 错法:循环单查 for (Node node : nodes) { node.setResources(resourceMapper.selectByNodeId(node.getId())); // 500 次 } // ✅ 正法一:一次 JOIN 一起取回来 List<NodeWithResource> rows = nodeMapper.selectAllWithResources(problemId); // 1 次 // MyBatis 用 collection 映射成树 // ✅ 正法二:批量 IN + 内存分组(适合跨服务/跨表场景) Map<Long, List<Resource>> byNode = resourceMapper .selectByNodeIds(nodeIds) // WHERE node_id IN (500 个 id) 1 次 .stream() .collect(Collectors.groupingBy(Resource::getNodeId)); nodes.forEach(n -> n.setResources(byNode.getOrDefault(n.getId(), List.of())));

修复验证:SQL 日志里 501 → 2,接口 P99 从 2000ms → 80ms。优化前后数字都报出来,这是工程汇报的基本功。

步骤 3:修掉 Retry Storm——退避、抖动、上限、熔断

resilience4j:
  retry:
    instances:
      callStatsService:
        max-attempts: 3                      # 含首次,最多试 3 次
        wait-duration: 100ms
        enable-exponential-backoff: true     # 100ms → 200ms → 400ms
        enable-randomized-wait: true         # jitter:加随机抖动防同时重试
  circuitbreaker:
    instances:
      callStatsService:
        failure-rate-threshold: 50           # 失败率过半,熔断 30 秒
        wait-duration-in-open-state: 30s
        sliding-window-size: 20

四件套各自的作用:退避让重试间隔拉长给下游喘息;jitter 让百万客户端的重试错开(否则 30 秒熔断结束时所有请求同一瞬间杀回来,第二波雪崩);上限保证单请求最多放大 3 倍流量;熔断在下游已死时直接快速失败,停止送死流量。

重试只该用于幂等且瞬态的失败:网络抖动、503 可以重试;400 参数错误重试一万次也是 400;非幂等写操作(扣款、提交)重试前必须确认幂等(第 3 课)。

步骤 4:反模式走查清单(评审直接抄)

□ 读路径有没有缓存?还是一切直查主库?          (第 5 课)
□ 循环里有没有单查/单调用?(N+1 / Chatty)     (本课)
□ 重试有没有上限、退避、jitter?非幂等在重试吗? (本课)
□ 所有外部调用都设了超时吗?                     (第 2 课)
□ 写路径里有没有可以异步的旁路任务在同步等待?   (第 6 课)
□ 一次请求的 DB 往返次数是多少?超过 10 次要说明 (本课)
□ 有没有大 key、大事务、大 JSON 全量传输?       (第 5/7 课)
□ 监控里能看到 1 QPS 对应多少 DB TPS 吗?        (放大倍数 = 反模式探测器)

讲清楚要点

  1. 反模式的共同根源是「放大」:N+1 放大 DB 往返、Chatty I/O 放大网络次数、Retry Storm 放大请求量。度量放大的通用探针是放大倍数——1 次用户请求背后有多少次 DB/网络调用,超过个位数就要给理由。
  2. N+1 的本质是 ORM 的甜蜜陷阱:懒加载让开发时看不见 SQL,上线后靠日志/APM 才能暴露。团队的约定应该明确:列表/树形接口禁止循环查询,CI 里可以用 SQL 条数断言测试兜底。
  3. 重试是把双刃剑,默认不开:每个重试配置都要能回答三个问题——重试安全吗(幂等?)、重试几次(上限)、间隔怎么长(退避+jitter)。答不出就不该有这个重试。
  4. 超时是反模式放大的帮凶:超时设太宽(30s),一个慢请求占线程 30 秒,线程池 50 线程 25 秒内被拖光;设太窄又误杀慢查询。按「P99 × 2」设初值,再按熔断和舱壁补位。
  5. 无缓存的读路径等于把 DB 当服务器:读多写少系统(本站 9:1)不加缓存,等于主动选择 DB 做第一个瓶颈。评审看到「直查主库的读接口」就要求给缓存或降级方案。

你可能会懵的地方

  • N+1 和 Chatty I/O 有什么区别? 对象不同:N+1 是对数据库的循环单查(SQL 层),Chatty I/O 是对服务的循环调用(RPC/HTTP 层)。修法同源(批量),但 N+1 用 JOIN/IN,Chatty 用批量 API/BFF 聚合。面试能区分说明有实操。

  • jitter 为什么必须随机,固定退避不行吗? 固定退避让所有客户端同一节奏重试:故障恢复的瞬间,积压的全部重试同时到达,形成「重试同步波」,刚恢复的下游立刻再挂。随机抖动把重试打散成平缓斜坡。这也是第 5 课 TTL 加抖动的同一原理——集中 = 灾难,分散 = 平稳。

  • 怎么在 code review 里快速发现这类问题? 三个动作:看循环体里有没有 select/feign/restTemplate 调用;看配置里有没有 retry 注解而没配上限退避;看新接口的 SQL 日志条数与请求数比值。最后可以建「放大倍数」监控项长期盯。

  • 前端也能造成 Retry Storm 吗? 能,而且更隐蔽:浏览器插件、自动刷新、移动端 SDK 的自动重试没有退避。服务端对策:429 + Retry-After 明示客户端行为、网关限流兜底、WS 代替轮询从根上减少重试源。

  • 压测能提前暴露反模式吗? 大部分能——压测放大流量后 N+1 的 501 次 SQL 会立刻把 P99 顶上天,Retry 配置错误会在人为注入故障时现形(看放大倍数)。所以流程上把「压测看放大倍数」设为上线门禁比评审更可靠。

错法对照

错法问题正解
循环里逐个查库/调服务往返次数随数据量线性爆炸批量 IN / JOIN / 批量聚合接口
重试 10 次、间隔固定 1 秒放大 10 倍流量且同步波3 次 + 指数退避 + jitter + 熔断
对非幂等写操作开启重试重复扣款/重复提交先做幂等(唯一键)再谈重试
超时统一设 30 秒「保险」线程池被慢请求占死按 P99×2 定,配合熔断舱壁
树形页面循环建树查询500 节点 = 500 次 SQL一次取全量内存组装,或预物化树 JSON 缓存

卡点急救

  1. 线上突然大面积超时但应用 CPU 不高:先看放大倍数——DB TPS / 应用 QPS 是否异常飙升(N+1 在数据量增长后爆发);再看下游是否雪崩(重试风暴)。止血顺序:网关限流 → 关闭可疑重试配置(开熔断)→ 缓存热点读路径。
  2. 加了缓存还是慢:查缓存命中是否被穿透(空值缓存缺失)、大 key 序列化耗时(单 value 超过 100KB 要拆)、以及是不是「缓存了但每次都 miss」(TTL 太短或主动失效写错了 key)。
  3. 无法判断该不该给某个调用加重试:三问定夺——失败是瞬态的吗(超时/503 是,400 否)?操作幂等吗?下游被压垮时重试会恶化吗?三问有两个「否」就不加重试,改用降级或告警人工介入。

面试常问

  • 讲一次你排查/修复过的性能事故。 套路:现象(详情页 P99 2 秒)→ 度量(APM 发现 1 次请求 501 次 SQL,放大倍数异常)→ 根因(树形节点循环查询 N+1)→ 修复(批量 IN + 内存组装,SQL 501→2)→ 验证(P99 80ms)→ 防复发(SQL 条数断言进 CI、放大倍数监控)。完整闭环比技巧本身更打动面试官。

  • 重试风暴是怎么发生的?怎么预防? 按「链条 + 四件套」作答:下游变慢→超时→重试放大流量→更慢→全站雪崩。预防:指数退避 + jitter 防同步波、次数上限防流量放大、熔断防送死流量、非幂等不重试。最后补「超时设窄一点、熔断兜底」体现系统性。

  • 你怎么给团队建立反模式的防线? 三层:评审清单(循环查库、重试配置、缓存缺失逐项过)→ CI 门禁(SQL 条数断言、放大倍数压测)→ 运行时监控(1 QPS 对应 DB TPS、429 比例、熔断次数告警)。说明「防线靠机制不靠自觉」。

自测三问

  1. 一次详情页请求 501 次 DB 往返,用什么手段第一时间发现?说出三个。
  2. Retry Storm 的死亡链条有哪几步?四件套各打断链条的哪一环?
  3. 「超时 30 秒 + 重试 10 次」这个配置有哪两个致命问题?

小结一句话

反模式是设计的负向清单——放大倍数(1 次请求背后的 DB/网络调用数)是万能探测器,N+1 用批量修,重试用「退避+jitter+上限+熔断」四件套驯服。

延伸

  • Resilience4j — Retry 与 CircuitBreaker:退避、jitter、熔断的官方配置
  • Microsoft Azure — Retry 模式与性能反模式:重试设计权威出处
  • MyBatis — 延迟加载与嵌套查询:N+1 的 ORM 端来源与规避
  • 下一课:综合练习——用四步法为本站判题平台完成一份扛住 10 万日活的完整设计答卷。
加载本节练习…
← 上一课可靠性模式下一课 →综合练习