Skip to content

高并发系统设计 ​

S6-016选修高级约 90 分钟JDK 21Java 高级与架构 · 架构设计

1. 本节定位 ​

项目 B 的秒杀架构讲的是「一个场景怎么防」;本节把高并发变成可计算的工程:几个机器、几个池子、几把锁、限流用哪种。

★ 四组一手实测 ✓✓:

text
容量估算:日活 100 万 → 峰值 6667 QPS → 9 台(加冗余 14 台)→ 并发下限 333
排队曲线:负载率 0.90 → 0.95(只涨 5 个点),总延迟 100ms → 200ms(翻倍)✗✗
利特尔定律:并发 = QPS × RT;池子只配 50 → 上限 1000 QPS(差 6.7 倍)✗✗
限流实测:临界突发下「固定窗口 20/20 ✗✗ | 滑动窗口 10/20 | 令牌桶 11/20 | 漏桶 11/20(排队)」

2. 学习目标 ​

  • 会做容量估算(DAU → QPS → 峰值 → 机器 → 并发)★★★;
  • 能解释压测「拐点」的数学来源 ★★★;
  • 会用利特尔定律反推池子/线程/连接数下限 ★★★;
  • 能实现并选对限流算法(分清新旧窗口、突发容忍)★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ 高并发项目最容易翻车的两件事 ✗✗:

翻车方式代价
拍脑袋配机器/池子 ✗上线即瓶颈(池子 50 却要 6667 QPS)
等到压测拐点再扩容 ✗✗拐点之后延迟已经失控,扩容窗口以分钟计

★★ 而两件事都可以提前算出来 ✓✓✓:算量 → 留水位 → 限流兜底(T2-015 的压测就是用来标定「单机能力」的)✓。

5. 核心概念 ​

5.1 容量估算:从业务量到机器数(实测)★★★ ​

text
日活 1000000 人 × 人均 20 次 = 日请求 20000000 次
均匀分布:QPS = 20000000 / 86400 ≈ 231
口径 A(20% 请求集中在 10 分钟峰值段):6667 QPS
口径 B(经验峰值系数 5):1157 QPS
★ 取更大者做设计输入:**6667 QPS**(估算宁可偏大)

单机能力(必须用压测标定):800 QPS/台
机器数 = 6667 / 800 ≈ 8.3 → 向上取整 **9 台**
按 1.5 倍冗余(9 × 1.5 = 13.5)→ **14 台** ✓✓

★★ 三个要点 ✓✓✓:

  1. 峰值用两个口径交叉验证(业务分布 + 经验系数),取大者 ✓;
  2. 单机能力必须实测(压测标定),不能抄网上的数字 ✓✓;
  3. 冗余不是浪费:N+1 是为了「坏一台还能扛峰值」✗✓。

5.2 排队曲线:压测「拐点」的数学解释(实测)★★★ ​

text
负载率 ρ | 平均排队 Wq | 总延迟 | 平均排队长度
 0.50    |     10.0 ms |  20.0 ms | 0.5
 0.70    |     23.3 ms |  33.3 ms | 1.6
 0.80    |     40.0 ms |  50.0 ms | 3.2
 0.90    |     90.0 ms | 100.0 ms | 8.1
 0.95    |    190.0 ms | 200.0 ms | 18.0
 0.98    |    490.0 ms | 500.0 ms | 48.0

★★★ 一句话 ✓✓✓:系统接近饱和时,延迟对流量是「非线性」的 —— ρ 从 0.90 到 0.95(+5 个点),延迟翻倍;到 0.98 时已经是 5 倍 ✗✗。

★ 这就是 T2-014 压测里「并发 ×64 而 QPS 只 +25%、P99 涨 36 倍」的根源 ✓✓✓ —— 拐点不是 bug,是排队论的必然:所以容量水位要留在 70% 以下 ✓✓。

5.3 利特尔定律:池子该配多大(实测)★★★ ​

text
目标 6667 QPS,平均响应 50 ms
并发处理中请求 = QPS × RT = 6667 × 0.050s = **333 个**

-- 反例:下游连接池只配了 50 --
池子上限 = 50 / 0.05s = **1000 QPS** ✗✗(远低于目标 6667)

★★ 三个「连乘」检查 ✓✓✓(任何一环不够都会卡住整条链):

环节下限说明
入口线程池≥ 333不足 → 请求排队在入口
下游连接池≥ 333不足 → 上游线程全在等连接(《线程池与 ThreadPoolExecutor》)
数据库 max_connections≥ 333 × 实例数最容易忽略的一环 ✗✗

★ 反直觉推论 ✓:下游慢一倍(RT 100ms),需要的池子就翻一倍 —— 「扩容池子」是治标,「让下游变快」才是治本 ✓✓。

5.4 四种限流算法(实测)★★★ ​

同一段「窗口临界突发」(t=900~1090ms 共 20 个请求,限流 10/秒):

算法通过实测行为结论
固定窗口20/20 ✗✗窗口 0 放满 10 个、窗口 1 又放 10 个临界双倍(瞬时 105/秒)✗✗
滑动窗口10/20 ✓任何 1 秒内都不超 10 个精确,但要存时间戳
令牌桶11/20 ✓✓前 10 个花掉满桶,之后按速率补充允许蓄力突发,O(1)
漏桶11/20 ✓入桶后恒定速率流出,最长排队 990ms保护下游,入口不限突发

★★ 选型 ✓✓✓:网关/服务端默认用令牌桶(Guava RateLimiter / Sentinel 的选择)✓; 单机限流只保自己,全局限流要上 Redis 或网关层 ✓(呼应 《CAP 与 BASE 理论》 的 CP 取舍)。

★ 固定窗口的教训 ✓✓:它的「超额」不是 10%,而是瞬时 2 倍 —— 任何「按周期重置」的设计都要问一句:周期边界会发生什么? ✓✓。

5.5 分层防护的定量配比 ★★ ​

结合 《项目 B:高并发场景与架构设计》 的秒杀链路,把「每层拦多少」写清楚:

text
10 万请求 → CDN/网关(拦静态与恶意)→ 限流(只放 1 万)→ 缓存预扣(挡掉 9000 无库存)
          → MQ 削峰(异步落库 1000)→ 数据库(实际写入 1000)
★ 核心:**每一层都把量级降一个数量级**,让数据库只面对「真实成交」✗✓

6. 递进示例 ​

6.1 容量估算的「五步」 ​

text
① 业务量(DAU × 人均请求)
② 峰值 QPS(两个口径取大)
③ 单机能力(压测标定)
④ 机器数(向上取整 + 冗余)
⑤ 并发下限(Little 定律)→ 反向校验池子配置

6.2 令牌桶最小实现 ​

java
boolean tryAcquire(long nowMs) {
    tokens = Math.min(capacity, tokens + (nowMs - lastRefillMs) * ratePerMs);
    lastRefillMs = nowMs;
    if (tokens >= 1) { tokens -= 1; return true; }
    return false;
}

6.3 反例:全局限流写成本地 if (count++ < N) ​

text
单机限流 × 10 台 = 实际上限 10N ✗✗(每台都以为自己只有 N)
正解:Redis 计数器(Lua 原子)或网关统一限流 ✓✓

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/CapacityEstimationLesson.java、labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/RateLimiterLesson.java

实测输出(节选):

text
===== 2. 排队模型(M/M/1,服务时间 10ms):拐点从哪来 =====
   0.90   |     90.0 ms |  100.0 ms | 8.1
   0.95   |    190.0 ms |  200.0 ms | 18.0
  ★★ 这就是压测「拐点」的数学解释:**接近饱和时,一点流量增长会被延迟放大数倍**

===== 3. 利特尔定律:池子该配多大 =====
  并发处理中请求 = QPS × RT = 6667 × 0.050s = **333 个**
  池子上限 = 50 / 0.05s = **1000 QPS** ✗✗(远低于目标 6667)

===== 1~4. 四种限流:同一段临界突发 =====
    t=990ms 通过 ✓(窗口 0: 10/10)      ← 固定窗口
    t=1000ms 通过 ✓(窗口 1: 1/10)      ← 边界另一侧重新计数
  通过 20/20:**20 个请求在 190ms 内全部通过** ✗✗(瞬时约 105/秒)
  ...
  滑动窗口:通过 10/20 ✓  令牌桶:通过 11/20 ✓  漏桶:接受 11/20(最长排队 990ms)

8. Debug 观察点 ​

  1. 把排队曲线的服务时间改成 50ms:所有延迟等比放大 ✓✓(服务时间影响全局);
  2. 把限流场景改成「完全平滑的 10/s」:四种算法都通过,差异只在突发 ✓✓;
  3. 把令牌桶容量从 10 改成 100:观察「能容忍多大的突发」✓;
  4. 用真实压测(T2-015)标定你服务单机 QPS,替换本节假设值 ✓✓✓。

9. 常见错误 ​

#错误说明
1把「日活 × 请求 / 86400」当设计 QPS ✗✗峰值是均匀值的 5~30 倍 ✓✓
2等压测拐点到了才扩容 ✗✗✗拐点后延迟已非线性失控 ✓✓
3只配线程池、忘了连接池/DB 连接 ✗✗连乘检查缺一不可 ✓✓
4限流用固定窗口扛突发 ✗✗瞬时 2 倍额度,等于没限 ✓✓
5单机限流当全局限流 ✗✗✗实际上限 = N × 单机额度 ✓✓
6容量水位不留(跑到 90%+) ✗✗一点波动就进非线性区 ✓✓

10. 动手实验 ​

  1. 跑 CapacityEstimationLesson 三段,把 DAU 换成你自己项目的数字 ✓✓✓;
  2. 跑 RateLimiterLesson,把突发序列改成你自己的业务模式(如整点开抢)✓✓;
  3. 用 Guava RateLimiter 或 Sentinel 复现令牌桶行为,对拍本节实现 ✓✓;
  4. 做一次真实压测:逐步加压,记录 QPS-P99 曲线,标出拐点位置(T2-015)✓✓✓。

验收方式:给定一个业务量与单机能力,能算出机器数、水位、并发下限与限流方案。

扩展任务:为项目 B 写《容量与限流方案》:目标 QPS、机器水位、池子配置、限流算法与阈值来源 ✓✓

11. 练习题 ​

  1. 峰值 QPS 为什么要用两个口径取大者?
  2. 负载率 0.9 → 0.95 延迟翻倍的数学原因是什么?
  3. 利特尔定律怎么用来反推池子大小?举一个「下游变慢一倍」的后果。
  4. 固定窗口的「临界双倍」怎么产生?哪种算法没有这个问题?
  5. 为什么说单机限流不等于全局限流?
参考答案(要点)
  1. 业务分布有偏差、经验系数也粗糙,取大者留安全边际 ✓✓★
  2. 排队公式 Wq = ρ/(μ(1-ρ)):分母趋近 0 时延迟爆炸(非线性)✓✓★
  3. 并发 = QPS × RT;下游 RT 翻倍 → 所需并发翻倍(池子不够就排队)✓✓★
  4. 计数按周期重置,边界两侧各放一份额度;滑动窗口/令牌桶/漏桶都没有 ✓✓★
  5. 额度是每台的:10 台 × 单机 N = 10N;全局要 Redis/网关统一计数 ✓✓★

12. 掌握标准 ​

  • [ ] 会做五步容量估算 ★★★;
  • [ ] 能解释拐点与水位 ★★★;
  • [ ] 会用 Little 定律校验池子 ★★★;
  • [ ] 能实现四种限流并知道差异 ★★★;
  • [ ] 知道分层防护的定量思维 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《高可用系统设计》 高可用系统设计 —— 承接本节的「容量与限流」,讲故障与重试的数学。 监控侧呼应:T2-016 服务端监控与瓶颈分析。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引