聊聊高并发系统:那些容易混淆的概念和实际问题

77 阅读35分钟

聊聊高并发系统:那些容易混淆的概念和实际问题

前言

在实际工作中经常会碰到这样的情况:大家都能说几个高并发的名词,QPS、限流、缓存什么的,但真要系统地讲清楚,往往就卡壳了。

很多开发人员平时做项目碰到性能问题就临时搜一搜,东拼西凑地优化一下,从来没认真梳理过。本文就系统地整理一下高并发这块儿的核心知识,把那些容易混淆的概念和实际问题讲清楚。


一、高并发的基础概念:那些容易混淆的术语

1.1 QPS(Queries Per Second)

QPS 是指系统每秒能够处理的请求数量。比如系统 QPS=1000,就是每秒能处理1000个请求。

需要注意的是,QPS通常是理论或测试值,实际运行时会受各种因素影响,可能达不到这个数值。

1.2 TPS(Transactions Per Second)

TPS 是指系统每秒完成的事务数。

QPS 和 TPS 经常被混淆,其实它们的关注点不同:QPS 关注接口层面(HTTP请求次数),TPS 关注业务层面(完整的业务操作)。

拿下单来说,用户调用1次 /api/createOrder 接口,从接口角度看是1个Query(QPS+1),从业务角度看完成了1笔订单(TPS+1)。所以通常 1次下单 = 1个QPS = 1个TPS,只不过这1个TPS内部可能包含查询库存、扣减库存、创建订单等多次数据库操作。

1.3 吞吐量(Throughput)

吞吐量是指单位时间内系统实际完成的工作量。它和 QPS/TPS 的关注点不太一样:QPS/TPS 看的是请求数量(收到多少请求、完成多少事务),吞吐量看的是实际处理的数据量(传输了多少数据、处理了多少业务)。

吞吐量有两种常见的衡量方式:

按请求数量衡量时,吞吐量其实就等于 QPS。比如系统每秒处理1000个订单查询请求,吞吐量就是1000请求/秒。

按数据量衡量时,吞吐量看的是每秒传输的数据大小(MB/s、GB/s)。比如系统每秒传输500MB图片数据,吞吐量就是500MB/s。这种情况下,QPS和吞吐量是两回事——QPS可能只有100(100个请求),但每个请求返回5MB数据,吞吐量就是500MB/s。

实际工作中,讨论接口性能时一般说QPS/TPS,讨论网络带宽、存储IO时说吞吐量(MB/s)。大多数时候,说的"吞吐量"其实就是指QPS。

1.4 并发数(Concurrency)

并发数是指系统同时处理的请求数量。比如100个用户同时访问系统,并发数就是100。

这里要注意一个常见误区:并发数不等于在线用户数。在线用户可能只是在浏览页面,并没有真正发起请求。

1.5 流量(Traffic)

流量通常指单位时间内的访问量,可以用请求数、数据传输量等来衡量。对于高并发系统来说,最需要关注的是峰值流量——系统在某个时间点承受的最大流量。

1.6 这些指标怎么算?接口维度还是系统维度?

这个问题在工作中经常碰到,特别是压测的时候,老板问"咱们系统QPS多少",回答1000,结果他又问"是哪个接口",这时候就需要明确是接口维度还是系统维度的指标。

1.6.1 指标的计算方法

QPS的计算

QPS = 总请求数 / 时间窗口(秒)

举例:
- 1分钟内收到3000个请求
- QPS = 3000 / 60 = 50

TPS的计算

TPS = 总事务数 / 时间窗口(秒)

举例:
- 1分钟内完成了120笔订单(每笔订单是1个事务)
- TPS = 120 / 60 = 2

吞吐量的计算

按请求数:吞吐量 = 成功处理的请求数 / 时间(秒)
按数据量:吞吐量 = 传输的数据量(MB) / 时间(秒)

举例:
- 1小时内成功处理360万个请求 → 吞吐量 = 3600000 / 3600 = 1000 请求/秒
- 1分钟内传输30GB数据 → 吞吐量 = 30 * 1024MB / 60秒 = 512 MB/s

响应时间的计算

1. 平均响应时间(Average Response Time)

平均响应时间 = 所有请求响应时间之和 / 请求总数

举例:
10个请求的响应时间分别是:
50ms, 60ms, 55ms, 65ms, 58ms, 62ms, 57ms, 59ms, 61ms, 5000ms

平均响应时间 = (50+60+55+65+58+62+57+59+61+5000) / 10 = 552.7ms

问题来了:明明9个请求都在60ms左右完成,只有1个慢请求5000ms,但平均值却是552ms,这不能反映真实情况!

2. P99响应时间(99th Percentile Response Time)

P99的意思是:99%的请求响应时间都在这个值以下

计算步骤:
1. 将所有请求按响应时间从小到大排序
2. 找到第99%位置的请求
3. 这个请求的响应时间就是P99

举例(100个请求):
- 排序后:10ms, 15ms, 20ms, ..., 50ms, ..., 100ms, ..., 5000ms
- 第99个请求的响应时间是:100ms
- 那么P99响应时间 = 100ms
- 含义:99%的用户(99个人)体验到的响应时间都在100ms以内

即使最后1个请求是5000ms(慢),也不会影响P99的值。

为什么关注P99而不只看平均值?

指标特点问题
平均响应时间简单直观容易被少数慢请求拉高,不能反映大部分用户的真实体验
P99响应时间反映99%用户的体验能过滤掉极端慢的请求,更贴近真实情况

实际案例对比

某接口100个请求:
- 95个请求:50ms
- 4个请求:100ms  
- 1个请求:10000ms(超时)

平均响应时间 = (95*50 + 4*100 + 1*10000) / 100 = 147.5ms  ❌ 不准确
P99响应时间 = 100ms  ✅ 准确(99%的用户体验都在100ms以内)

所以在监控和压测时,我们更关注P99,而不是平均值。

其他常见指标

  • P95:95%的请求响应时间都在这个值以下
  • P50(中位数):50%的请求响应时间都在这个值以下
  • P999:99.9%的请求响应时间都在这个值以下(要求更严格)
1.6.2 接口维度 vs 系统维度

这些指标既可以是接口维度,也可以是系统维度,取决于你要分析什么。

维度说明使用场景举例
接口维度统计单个接口的性能定位具体问题接口/api/getUserInfo 接口 QPS=500
服务维度统计单个服务的性能评估服务能力用户服务 QPS=2000
系统维度统计整个系统的性能评估整体能力整个电商系统 QPS=10000

实际工作中的应用

  1. 压测时

    • 先测接口维度:找出慢接口
    • 再测系统维度:评估整体容量
  2. 监控时

    • 接口维度监控:实时看各接口QPS、响应时间
    • 系统维度监控:看整体负载、资源使用率
  3. 限流时

    • 接口级限流:热点接口单独限流(如:查询接口限流1000)
    • 系统级限流:整个服务限流(如:用户服务总QPS限流5000)

举个完整的例子

电商系统:
├─ 用户服务(系统维度:QPS=2000)
│  ├─ /api/login          (接口维度:QPS=500, 响应时间=50ms)
│  ├─ /api/getUserInfo    (接口维度:QPS=800, 响应时间=30ms)
│  └─ /api/updateProfile  (接口维度:QPS=700, 响应时间=100ms)
│
├─ 订单服务(系统维度:QPS=1500)
│  ├─ /api/createOrder    (接口维度:QPS=300, 响应时间=200ms)
│  ├─ /api/queryOrder     (接口维度:QPS=1000, 响应时间=80ms)
│  └─ /api/cancelOrder    (接口维度:QPS=200, 响应时间=150ms)
│
└─ 商品服务(系统维度:QPS=3000)
   ├─ /api/getProduct     (接口维度:QPS=2500, 响应时间=20ms)
   └─ /api/searchProduct  (接口维度:QPS=500, 响应时间=120ms)

整个电商系统(系统维度):QPS = 2000 + 1500 + 3000 = 6500

统计工具

  • 接口维度:Spring Boot Actuator + Prometheus
  • 系统维度:SkyWalking、Pinpoint、云监控平台
  • 实时统计:Nginx日志、ELK

二、系统性能评定指标:如何衡量一个系统的好坏?

评价一个高并发系统,不能只看单一指标,需要综合考虑:

2.1 响应时间(Response Time)

  • 定义:从发起请求到收到响应的时间。

  • 分类:

    • 平均响应时间:所有请求的平均值
    • P90响应时间:90%的请求响应时间低于这个值
    • P99响应时间:99%的请求响应时间低于这个值

看响应时间别只看平均值,很容易被慢请求拉高。实际工作中更关注P99,因为它代表了大部分用户的真实体验。

2.2 可用性(Availability)

系统正常运行的时间占比。不同的可用性要求,对应的停机时间差别很大:

  • 99.9%可用性:每年停机约8.76小时
  • 99.99%可用性:每年停机约52.6分钟
  • 99.999%可用性:每年停机约5.26分钟(俗称"5个9")

一般的业务系统,99.9%就够用了。金融、支付这种核心系统才需要99.99%以上。

2.3 错误率(Error Rate)

  • 定义:请求失败的比例。

  • 常见分类

    • 4xx错误:客户端错误(如404、403)
    • 5xx错误:服务器错误(如500、503)

2.4 资源利用率

  • CPU使用率
  • 内存使用率
  • 网络带宽使用率
  • 磁盘I/O使用率

有个经验:CPU使用率最好不要超过70%,留点余地应对突发流量。实际案例中,如果压测时CPU就跑到90%,上线后很容易在第一波高峰就出现故障。

2.5 网络带宽打满了是怎么回事?

高并发场景下经常碰到一个被忽略的问题:系统慢不一定是代码的锅,也可能是网络带宽不够了。

2.5.1 什么是"带宽打满"?

可以这样理解:服务器就像一条高速公路,带宽是车道数量,数据是汽车。车太多超过车道容量,就堵车了。

从技术角度看:

先理解单位换算

Mbps = Megabits per second (兆比特/秒) - 带宽单位
MB/s = Megabytes per second (兆字节/秒) - 传输速度单位

换算公式:Mbps ÷ 8 = MB/s
因为:1 Byte (字节) = 8 bits (比特)

常见带宽对应的传输速度:
- 100Mbps  = 100÷8 = 12.5MB/s
- 200Mbps  = 200÷8 = 25MB/s
- 1000Mbps = 1000÷8 = 125MB/s

记忆技巧:运营商说的"100M宽带",实际下载速度最高只有12.5MB/s

计算带宽瓶颈

假设服务器带宽是 100Mbps(即12.5MB/s)

如果每个请求返回的数据是 100KB:
- 理论最大QPS = 12.5MB/s ÷ 100KB = 125 QPS

这时候即使代码再优秀,CPU、内存都很空闲,
QPS也上不去,因为网络带宽已经打满了!
2.5.2 如何判断是不是带宽问题?

监控指标

# Linux下查看网络流量
iftop    # 实时查看网络流量
nload    # 实时查看带宽使用情况

# 带宽使用率
当前流量 / 总带宽 > 80%  就要警惕了

典型症状

  1. CPU使用率正常(30%-50%)
  2. 内存使用率正常(50%-60%)
  3. 数据库响应快(< 50ms)
  4. 但是接口响应慢(> 1s)
  5. 网络带宽使用率高(> 90%)

云平台监控

  • 阿里云:云监控 → ECS → 网络监控 → 网络流出带宽
  • 腾讯云:云监控 → 云服务器 → 外网出带宽利用率
  • AWS:CloudWatch → EC2 → NetworkOut
2.5.3 哪些场景容易打满带宽?

场景1:返回大量数据

// 不好的做法:一次返回1000条记录,每条10KB
@GetMapping("/users")
public List<User> getAllUsers() {
    return userService.findAll();  // 返回10MB数据
}

// 好的做法:分页返回
@GetMapping("/users")
public Page<UsergetUsers(@RequestParam int page) {
    return userService.findByPage(page, 20);  // 每页20条,约200KB
}

场景2:图片、视频等大文件

问题:
- 单个图片5MB,QPS=100
- 需要带宽:5MB × 100 = 500MB/s = 4Gbps

解决方案:
- 使用CDN(内容分发网络)
- 图片压缩、懒加载
- 对象存储(OSS)

场景3:日志太多

// 不好的做法:高并发下打印大量日志
logger.info("用户信息:{}", user);  // user对象很大

// 好的做法:只打印关键信息
logger.info("用户登录:userId={}", user.getId());
2.5.4 如何解决带宽问题?

方案1:升级带宽(最直接,但成本高)

云服务器带宽价格参考(大概):
- 100Mbps → 200Mbps:每月多花几百元
- 适合:临时活动、预算充足

方案2:使用CDN(推荐)

原理:
- 静态资源(图片、CSS、JS)分发到全国各地的CDN节点
- 用户访问时,从最近的节点获取
- 大幅减轻源站带宽压力

效果:
- 可减轻80%以上的带宽压力
- 成本:比升级带宽便宜很多

方案3:数据压缩

// Spring Boot启用Gzip压缩
server:
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,application/json
    min-response-size: 1024  # 超过1KB才压缩

效果:
- JSON数据可压缩70%-80%
- 1MB数据 → 200KB

方案4:优化返回数据

优化方向:
1. 只返回必要字段(不要把整个对象都返回)
2. 分页查询(不要一次返回几千条)
3. 图片缩略图(不要返回原图URL)
4. 数据缓存(减少重复传输)

方案5:负载均衡

多台服务器分担流量:
- 服务器A:带宽100Mbps
- 服务器B:带宽100Mbps  
- 服务器C:带宽100Mbps
总带宽:300Mbps
2.5.5 实际案例

案例:某电商系统商品列表页很慢

排查过程

  1. 检查代码:响应时间50ms,没问题
  2. 检查数据库:查询时间30ms,也没问题
  3. 检查CPU:使用率40%,还是没问题
  4. 检查带宽:使用率95%,问题在这!

原因分析

  • 每个商品返回了20张高清大图URL和详细描述
  • 每个请求返回数据约2MB
  • 服务器带宽200Mbps(25MB/s)
  • 理论QPS只有:25MB/s ÷ 2MB = 12.5

解决方案

  1. 列表页只返回1张缩略图 + 简短描述
  2. 详情页才返回所有图片
  3. 图片使用CDN
  4. 启用Gzip压缩

优化效果

  • 单个请求数据从2MB降到50KB
  • 理论QPS从12.5提升到500
  • 实际QPS从10提升到300+
  • 用户体验明显改善

三、压测:高并发系统的"体检报告"

3.1 什么是压测?

压力测试(Stress Testing) 是通过模拟大量用户并发访问,测试系统在高负载下的表现。

3.2 压测的目的

  1. 找出系统瓶颈:CPU、内存、数据库、网络哪个先扛不住?
  2. 确定系统容量:系统最多能支撑多少QPS?
  3. 验证扩容方案:加机器能提升多少性能?
  4. 为限流提供依据:应该限流到多少?

3.3 压测的关键指标

  • 最大QPS:系统能承受的最大请求数
  • 临界点QPS:响应时间开始明显增加的点(通常取P99 < 200ms时的QPS)
  • 系统资源使用情况:CPU、内存、数据库连接数等

3.4 压测的常用工具

  • JMeter:功能强大,支持多种协议
  • Gatling:基于Scala,性能优秀
  • wrk:命令行工具,轻量级
  • 云压测平台:阿里云PTS、腾讯云压测等

四、限流:高并发系统的"安全阀"

4.1 为什么需要限流?

想象一下:系统经过压测,发现最大能承受 QPS=1000,但某天突然来了 QPS=5000 的流量。

不限流的后果

  • 服务器CPU、内存打满
  • 所有请求响应变慢
  • 数据库连接池耗尽
  • 整个系统雪崩,所有用户都访问不了

限流的作用:保护系统,让部分请求正常处理,而不是让所有请求都失败。

4.2 限流阈值怎么确定?

限流值到底怎么定?很多人以为是拍脑袋定的,其实应该是压测出来的,但也不能完全照搬压测结果。

确定限流值的步骤:
  1. 压测找出最大QPS:比如压测结果是 QPS=1200 时,系统开始出现大量超时。

  2. 设置安全阈值:取压测值的 70%-80% 作为限流值。

    • 原因:留出余地应对突发流量和系统抖动
    • 所以:限流设置为 1000 左右
  3. 分场景限流

    • 核心接口:限流可以宽松一些(如1000)
    • 非核心接口:限流可以严格一些(如500)
    • 重资源接口(如导出、查询大数据):限流更严格(如100)
  4. 动态调整

    • 监控实际流量和系统表现
    • 根据业务增长调整限流值
    • 扩容后重新压测,调整限流值

4.3 限流的常见算法

4.3.1 固定窗口算法

在固定时间窗口(如1秒)内限制请求数量。实现简单,但存在"临界问题"——在窗口边界可能瞬间超过限制。

4.3.2 滑动窗口算法

将时间窗口细分,更平滑地限流。解决了固定窗口的临界问题,限流更精准。

4.3.3 漏桶算法(Leaky Bucket)

请求进入"桶",以恒定速率流出。可以做流量整形,输出平滑,但无法应对短时突发流量。

4.3.4 令牌桶算法(Token Bucket)

以恒定速率生成令牌,请求需要获取令牌才能通过。允许一定程度的突发流量(桶内积累的令牌),是最常用的限流算法,Guava RateLimiter、Sentinel都基于此实现。

4.4 限流的实现工具

常用的限流工具有:Guava RateLimiter(单机限流)、Sentinel(阿里开源,功能强大,支持分布式限流)、Hystrix(Netflix开源,限流+熔断降级)、网关层限流(Nginx、Kong、Spring Cloud Gateway)。

4.5 限流的不同层次:代码层 vs 平台层

这是一个非常实用的问题!限流不只是代码层面的事情,实际工作中有多个层次可以做限流。

4.5.1 限流的四个层次
用户请求
    ↓
【1. CDN/DNS层】          ← 最外层防护
    ↓
【2. 云平台/WAF层】       ← 运维配置,无需改代码
    ↓
【3. 网关/负载均衡层】    ← 接口级、域名级限流
    ↓
【4. 应用代码层】         ← 业务逻辑限流
    ↓
后端服务
4.5.2 各层次详细说明
层次1:CDN/DNS层限流

特点

  • 最外层防护
  • 可以防御DDoS攻击
  • 配置简单,成本低

适用场景

  • 防止恶意攻击
  • 全局流量控制

工具

  • 阿里云CDN、腾讯云CDN
  • Cloudflare
层次2:云平台层限流(最常用!)

在云平台直接配置,不需要改代码,这是最常用的方式。

阿里云示例

阿里云控制台操作:
1. 云盾 → Web应用防火墙(WAF) → 防护配置
2. 设置限流规则:
   - 域名:www.example.com
   - 限流条件:单个IP每秒访问次数 > 100
   - 动作:拦截/返回503

或者使用API网关:
1. API网关 → API管理 → 流量控制
2. 按API维度限流:
   - API:/api/getUserInfo
   - QPS限制:1000
   - 超限后返回:429 Too Many Requests

腾讯云示例

腾讯云控制台操作:
1. API网关 → 服务 → 使用计划
2. 创建使用计划:
   - 名称:高峰期限流策略
   - 请求配额:10000次/天
   - 请求频率:100次/秒
   
3. 绑定到API或域名

AWS示例

AWS控制台操作:
1. API Gateway → Throttle Settings
2. 设置限流:
   - Rate Limit:1000 requests/second
   - Burst Limit:2000 requests

优点

  • 不需要改代码
  • 配置简单,生效快
  • 统一管理
  • 可视化配置

缺点

  • 不够灵活(无法根据复杂业务逻辑限流)
  • 有一定成本
层次3:网关/负载均衡层限流

在网关层统一限流,这是微服务架构的常见做法。

Nginx限流

# nginx.conf

# 基于IP限流
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;

# 基于域名限流
limit_req_zone $host zone=domain_limit:10m rate=1000r/s;

# 基于接口限流
limit_req_zone $request_uri zone=api_limit:10m rate=100r/s;

server {
    listen 80;
    server_name www.example.com;
    
    # 应用限流规则
    location /api/ {
        # 限流:100QPS,允许burst 200
        limit_req zone=api_limit burst=200 nodelay;
        
        # 超出限流返回自定义错误
        limit_req_status 429;
        
        proxy_pass http://backend;
    }
    
    # 热点接口单独限流
    location /api/hot-api {
        limit_req zone=api_limit burst=50 nodelay;
        proxy_pass http://backend;
    }
}

# 自定义429错误页面
error_page 429 /429.html;
location = /429.html {
    return 429 '{"code":429,"msg":"访问太频繁,请稍后再试"}';
}

Spring Cloud Gateway、Kong等网关也都支持限流配置,原理类似,可以按IP、接口路径、用户等多种维度限流。

优点

  • 统一限流,不侵入业务代码
  • 可以按域名、接口、IP等多维度限流
  • 灵活配置

缺点

  • 需要运维能力
  • 无法实现复杂的业务限流逻辑
层次4:应用代码层限流

在业务代码中实现限流,最灵活但需要开发。

场景1:使用Guava RateLimiter(单机)

import com.google.common.util.concurrent.RateLimiter;

@RestController
public class UserController {
    
    // 创建限流器:每秒100个令牌
    private final RateLimiter rateLimiter = RateLimiter.create(100);
    
    @GetMapping("/api/getUserInfo")
    public Response getUserInfo() {
        // 尝试获取令牌,最多等待100ms
        if (!rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            return Response.fail("系统繁忙,请稍后重试");
        }
        
        // 正常业务逻辑
        return userService.getUserInfo();
    }
}

场景2:使用Sentinel(分布式)

Sentinel支持注解方式限流,更适合分布式场景,配置也更灵活。具体配置可以参考Sentinel官方文档。

场景3:复杂业务限流

// 场景:VIP用户和普通用户不同的限流策略
@Service
public class OrderService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public Response createOrder(Long userId) {
        // 判断用户类型
        boolean isVip = userService.isVip(userId);
        
        // 不同用户不同限流
        int limit = isVip ? 100 : 10;  // VIP用户每秒100次,普通用户10次
        
        // 使用Redis实现分布式限流
        String key = "order_limit:" + userId;
        Long count = redisTemplate.opsForValue().increment(key, 1);
        
        if (count == 1) {
            redisTemplate.expire(key, 1, TimeUnit.SECONDS);
        }
        
        if (count > limit) {
            if (isVip) {
                return Response.fail("您的操作太频繁了");
            } else {
                return Response.fail("您的操作太频繁了,开通VIP可享受更高频率");
            }
        }
        
        // 正常业务逻辑
        return doCreateOrder(userId);
    }
}

优点

  • 最灵活,可以实现复杂的业务逻辑
  • 可以根据用户、时间等维度精细化限流

缺点

  • 需要开发工作量
  • 侵入业务代码
4.5.3 实际工作中如何选择?

推荐的组合方案(多层防护)

【层次1:云平台WAF】
- 防DDoS攻击
- IP黑名单
- 全局限流(如:单IP每秒1000次)

    ↓
    
【层次2:云平台API网关】(常用方式)
- 按域名限流:www.example.com → 5000 QPS
- 按接口限流:/api/hot-api → 1000 QPS
- 优点:不用改代码,配置简单

    ↓
    
【层次3:网关层限流】(可选)
- 如果是微服务架构,在Spring Cloud Gateway统一限流
- 好处:更灵活,可以按服务、版本限流

    ↓
    
【层次4:代码层限流】(核心接口)
- 只对核心业务接口做精细化限流
- 例如:秒杀、支付等接口
- 可以根据用户等级、商品库存等业务因素限流

选择建议

场景推荐层次理由
小团队、快速上线云平台层不用改代码,配置快
中大型企业云平台 + 网关层统一管理,灵活配置
复杂业务场景云平台 + 代码层精细化控制
微服务架构网关层 + 代码层服务级限流 + 接口级限流
4.5.4 云平台限流方案的优缺点分析

在实际工作中,很多团队会选择在云平台的接口层次或域名层次做限流,这是一种成熟且高效的实践方案。

优点

  1. 快速生效:不需要发版,改配置即可
  2. 运维友好:可视化配置,不需要懂代码
  3. 统一管理:所有限流规则在一个平台管理
  4. 成本可控:云平台提供的限流功能性价比高
  5. 防护全面:可以防御DDoS、CC攻击

适合的场景

  • 按接口路径限流(如:/api/user/info 限流1000)
  • 按域名限流(如:api.example.com 限流5000)
  • 按IP限流(如:单个IP每秒100次)
  • 简单的限流逻辑

不适合的场景(需要代码层补充):

  • 复杂业务逻辑(如:VIP用户限流10000,普通用户1000)
  • 动态限流(如:根据库存动态调整限流值)
  • 业务级限流(如:每个用户每天只能下10单)

方案建议

  • 日常场景:使用云平台限流
  • 核心业务:云平台 + 代码层双重保护
  • 复杂场景:引入Sentinel等框架

五、Spring/SpringBoot项目的性能瓶颈

5.1 典型瓶颈分析

5.1.1 线程池耗尽

Tomcat默认最大线程数200,高并发时很容易不够用。可以根据实际情况调整:

server:
  tomcat:
    threads:
      max: 500  # 根据实际情况调整
      min-spare: 50
5.1.2 数据库连接池耗尽

HikariCP默认最大连接数只有10,高并发场景下远远不够。调整配置:

spring:
  datasource:
    hikari:
      maximum-pool-size: 50  # 根据数据库能力调整
      minimum-idle: 10
5.1.3 数据库慢查询

单个慢查询占用连接时间长,会导致连接池耗尽。优化方向:优化SQL、添加索引、使用缓存(Redis)、读写分离。

5.1.4 同步阻塞调用

调用外部接口时阻塞线程,会大幅降低系统吞吐量。改用异步调用或消息队列。

5.1.5 内存溢出

大对象、内存泄漏会导致频繁GC,严重影响性能。可以调整JVM参数、使用流式处理大数据、排查内存泄漏点。

5.2 性能瓶颈的定位方法

定位性能瓶颈可以从几个方向入手:JVM监控(JVisualVM、JConsole、Arthas)、APM工具(SkyWalking、Pinpoint、Zipkin)、日志分析(查看慢日志、错误日志)、压测对比(逐步增加并发,找出临界点)。


六、流量超载:用户会看到什么?

6.1 场景分析

假设:系统压测 QPS=1000,限流设置为1000,现在实际流量达到 QPS=2000。

6.1.1 不做任何处理(最差情况)

用户看到

  • 页面转圈圈,长时间无响应
  • 最后显示"请求超时"或"502 Bad Gateway"
  • 用户体验:非常差,不知道发生了什么
6.1.2 做了限流但没有友好提示(一般情况)

用户看到

  • 部分用户:正常访问(QPS=1000以内)
  • 超出的用户:直接返回"系统繁忙,请稍后重试"
  • 用户体验:能理解,但不够友好
6.1.3 做了限流+友好提示(推荐)

用户看到

  • 排队提示:"当前访问人数较多,您前面还有XX人,预计等待XX秒"
  • 降级页面:显示简化版内容,减少资源消耗
  • 引导分流:"现在访问的人太多了,您可以稍后再试,或者访问XX页面"

6.2 如何给用户更好的体验?

6.2.1 系统层面的优化

可以使用排队机制,把请求放到消息队列(RabbitMQ、Kafka)里,给用户显示排队进度。还可以做优雅降级,关闭非核心功能,返回稍微旧一点的缓存数据。如果使用容器化(Docker、Kubernetes),可以实现弹性扩容,自动检测流量并快速扩容。

6.2.2 前端层面的优化

提示文案要友好,别直接显示"Error: 429 Too Many Requests",改成"访问的人太多啦,请稍后再试"会好很多。可以提供预约功能,或者引导用户到其他低负载页面。Loading动画也很重要,不要让用户觉得页面卡死了,最好能显示处理进度。

6.2.3 业务层面的优化

提前做预热,缓存热点数据,活动开始前通知用户。可以设置预约时间段,引导用户错峰访问,做到削峰填谷。限流提示也要区分场景:秒杀系统可以直接说"商品已抢完"(业务提示),普通系统说"系统繁忙,请稍后重试"(系统提示)。

6.3 响应码设计:理想 vs 现实

6.3.1 实际工作中的真实情况

理想情况:HTTP状态码和业务状态码完美配合。

现实情况(更常见):

  1. 只关注业务状态码:HTTP都是200,业务code区分成功/失败

  2. 统一网关处理:有些状态码(如503、504、429)由全公司统一的大网关返回,不是业务系统做的

  3. 职责分层:

    • 网关层:处理限流、熔断、服务未部署、超时等基础设施问题
    • 业务系统:只处理业务逻辑,返回业务状态码

这是大多数公司的实际做法。

让我们分别说明这两种情况:


6.3.2 方案1:只用业务状态码(更常见)

特点

  • HTTP状态码永远是 200 OK
  • 通过业务code区分成功/失败
  • 简单直接,前后端统一处理

响应格式

// 成功
HTTP/1.1 200 OK
{
  "code": 200,
  "message": "success",
  "data": { ... }
}

// 业务失败
HTTP/1.1 200 OK
{
  "code": 40001,
  "message": "余额不足",
  "data": null
}

// 系统异常
HTTP/1.1 200 OK
{
  "code": 50001,
  "message": "系统异常",
  "data": null
}

业务状态码规范示例

业务Code含义场景客户端处理
200成功正常业务正常展示
40001参数错误参数校验失败提示用户修改
40002业务校验失败余额不足、库存不足提示用户
40101未登录token过期跳转登录
40301无权限权限不足提示无权限
42901限流访问太频繁延迟重试
50001系统异常代码异常提示联系客服
50301服务降级依赖服务不可用提示稍后重试
50401请求超时依赖服务超时自动重试

实现要点

  • 定义统一的Response对象,包含code、message、data字段
  • 业务代码中根据不同场景返回不同的业务code
  • 全局异常处理器统一捕获异常并返回对应的业务code
  • 前端axios拦截器根据业务code做不同处理(未登录跳转、限流重试等)

优点

  • 前后端处理逻辑统一
  • 不需要关心HTTP状态码
  • 业务code可以自定义,更灵活
  • 简单易用

缺点

  • 监控系统无法通过HTTP状态码统计错误率(都是200)
  • 负载均衡器无法识别异常(都是200)

6.3.3 方案2:统一网关处理(大厂常见架构)

架构示意

客户端
   ↓
【统一网关(全公司)】
   ├─ 限流:返回 429 + {"code": 42901}
   ├─ 熔断:返回 503 + {"code": 50301}
   ├─ 服务未部署:返回 503 + {"code": 50302}
   ├─ 超时:返回 504 + {"code": 50401}
   └─ 路由转发
       ↓
   【业务系统】
       ├─ HTTP永远200
       └─ 只返回业务code

特点

  • 网关层:处理基础设施问题(限流、熔断、未部署、超时)→ HTTP状态码 + 业务code
  • 业务系统:只处理业务逻辑 → HTTP永远200 + 业务code
  • 职责清晰:基础设施问题不需要业务系统关心

网关配置示例(以阿里云API网关为例)

1. 限流规则:
   - 触发限流 → 返回429
   - 响应体:{"code": 42901, "message": "访问太频繁"}

2. 熔断规则:
   - 后端服务连续失败3次 → 熔断
   - 返回503
   - 响应体:{"code": 50301, "message": "服务暂时不可用"}

3. 超时配置:
   - 后端超时时间:5秒
   - 超时返回504
   - 响应体:{"code": 50401, "message": "请求超时"}

4. 服务未部署:
   - 后端服务无法连接
   - 返回503
   - 响应体:{"code": 50302, "message": "服务维护中"}

实现要点

  • 业务系统只关注业务逻辑,不处理限流、熔断、超时等基础设施问题
  • 前端需要同时处理业务系统返回的业务code和网关返回的HTTP状态码
  • 网关层统一配置限流、熔断、超时等规则

这种架构的优点

  • 职责分离:业务系统只关注业务,基础设施问题由网关统一处理
  • 全公司统一:限流、熔断策略全公司一致,不需要每个系统单独实现
  • 运维友好:修改限流策略不需要改代码,网关配置即可
  • 监控清晰:网关层可以统一监控429/503/504的触发情况

这种架构的分工

层次负责内容返回格式举例
网关层限流、熔断、路由、认证、超时、服务发现HTTP状态码 + 业务code429 + 42901
业务系统业务逻辑、数据处理、业务校验HTTP 200 + 业务code200 + 40002

6.3.4 两种方案对比
对比项只用业务code网关+业务code
HTTP状态码永远200网关返回真实状态码
业务系统复杂度需处理限流、超时等只关注业务逻辑(更简单)
监控需从响应体解析HTTP状态码直接统计(更方便)
前端复杂度简单(只看code)需同时处理HTTP和code
统一性每个系统自己实现全公司统一(更规范)
适用场景小团队、系统少中大型公司、微服务(更常见)

6.3.5 实际工作建议

如果公司有统一网关

  1. 业务开发人员:

    • 只关注业务状态码
    • 不需要处理限流、熔断、超时
    • HTTP永远返回200
    • 代码简单,专注业务
  2. 网关配置人员(运维/架构师):

    • 配置限流规则
    • 配置熔断规则
    • 配置超时时间
    • 监控网关层指标
  3. 前端开发人员:

    • 兼容处理HTTP状态码(网关返回)
    • 兼容处理业务code(业务系统返回)
    • 根据不同code做重试策略

业务状态码设计规范

1xxxx:信息类(较少用)
2xxxx:成功
  - 200:成功
  - 201:创建成功

4xxxx:客户端错误(不可重试)
  - 40001:参数错误
  - 40002:业务校验失败
  - 40101:未登录
  - 40301:无权限
  - 40401:资源不存在
  - 42901:限流(可延迟重试)

5xxxx:服务器错误(可重试)
  - 50001:系统异常
  - 50301:服务降级
  - 50302:服务未部署
  - 50401:请求超时

6.3.7 总结

大多数公司的实际情况:

  • 业务开发:只关注业务状态码,HTTP永远200
  • 网关层:统一处理限流、熔断、超时,返回HTTP状态码
  • 监控运维:网关层监控HTTP状态码,业务层监控业务code

响应码设计对比

场景网关返回业务系统返回
正常业务-HTTP 200 + code 200
业务失败-HTTP 200 + code 40002
未登录-HTTP 200 + code 40101
限流HTTP 429 + code 42901-
熔断降级HTTP 503 + code 50301-
服务未部署HTTP 503 + code 50302-
超时HTTP 504 + code 50401-

总结一下这种架构的核心思想:基础设施问题(限流、熔断、超时、未部署)由网关统一处理,业务逻辑问题(参数错误、业务校验、权限)由业务系统处理。职责分离,各司其职。业务开发人员不需要关心HTTP状态码,专注业务就行。


七、高并发系统 vs 秒杀系统:有何不同?

7.1 秒杀系统的特点

  • 流量特征:瞬时极高,持续时间短

  • 业务特征:商品数量有限,抢完即止

  • 用户预期:知道可能抢不到

  • 技术方案

    • 页面静态化
    • 库存预热到Redis
    • 消息队列异步处理
    • 明确的业务提示:"商品已售罄"

7.2 普通高并发系统的特点

  • 流量特征:持续较高,可能有波峰

  • 业务特征:正常业务,不应该"抢不到"

  • 用户预期:应该能访问

  • 技术方案

    • 缓存(Redis)
    • 数据库优化(索引、读写分离)
    • CDN加速
    • 负载均衡
    • 友好的系统提示:"系统繁忙,请稍后重试"

7.3 关键区别:业务提示 vs 系统提示

系统类型限流原因用户期望提示类型示例
秒杀系统商品数量有限知道可能抢不到业务提示"商品已售罄"
普通系统系统容量有限应该能访问系统提示"系统繁忙,请稍后重试"

重要提醒:普通高并发系统不应该让用户觉得是"抢",而是"暂时访问不了",需要扩容或优化。


八、实战案例:从0到1构建高并发系统

8.1 案例背景

某电商系统,日常 QPS=200,促销活动期间预计 QPS=2000。

8.2 优化方案

阶段1:基础优化(成本低,效果明显)

先从低成本的优化入手。把商品信息、用户信息缓存到Redis,缓存命中率能到90%以上。慢查询添加索引,避免全表扫描。静态资源(图片、CSS、JS)走CDN。

这一波操作下来,QPS能提升到500左右。

阶段2:架构优化(成本中等,效果显著)

做读写分离,主库写、从库读,读请求占80%,压力分散了不少。应用服务器从2台扩到5台,用Nginx做负载均衡。数据库连接池最大连接数调整为50。

这一轮优化后,QPS能到1200。

阶段3:高级优化(成本高,效果保底)

使用Sentinel做限流,阈值设为1000,超出部分返回友好提示。非核心功能(推荐、评论)直接关闭或降级,返回缓存数据。关键接口用RabbitMQ排队,给用户显示排队进度。

经过这三轮优化,系统能支撑QPS=2000+,超出部分有友好提示,不会崩溃。

8.3 压测验证

用JMeter模拟2000并发用户,压测结果:QPS=1000时,P99响应时间在200ms以内;QPS=1500时,限流开始生效,部分请求返回"系统繁忙";QPS=2000时,系统稳定,没有崩溃。


九、常见误区

误区1:"加机器就能解决所有问题"

不一定。如果瓶颈在数据库,加再多应用服务器也没用。需要先找出真正的瓶颈,再对症下药。

误区2:"限流阈值 = 压测最大QPS"

限流阈值应该是压测最大值的70%-80%,留点余地应对突发情况。

误区3:"缓存能解决一切"

缓存有成本。数据一致性问题、缓存穿透/击穿/雪崩、内存成本,这些都需要考虑。

误区4:"高并发 = 秒杀"

秒杀只是高并发的一种特殊场景,日常的高并发系统其实更常见。


十、总结:高并发系统的核心思路

10.1 核心原则

提前规划,不要等系统崩了再优化。分层防护,限流、降级、熔断多管齐下。给用户友好提示,让他们知道发生了什么。持续监控,实时发现问题。上线前一定要压测。

10.2 技术手段总结

手段作用适用场景
缓存减少数据库压力读多写少
读写分离分散数据库压力读写比例悬殊
限流保护系统,防止雪崩流量不可控
降级保核心功能系统资源不足
排队削峰,提升用户体验可以等待的场景
扩容提升系统容量流量持续增长

10.3 优化顺序建议

先优化代码(SQL、算法、缓存),成本低效果好。再优化架构(读写分离、负载均衡),成本适中。最后才考虑扩容(加机器、升配置),成本最高。


结语

高并发系统的优化,没有一劳永逸的方案,都是在实践中慢慢摸索出来的。

几个关键点:压测是基础,没有压测就无法准确评估系统容量;限流是保命的,不要等系统崩溃了才想起来做限流;用户体验最重要,技术手段最终还是要为用户服务。


参考资料

本文使用 markdown.com.cn 排版