聊聊高并发系统:那些容易混淆的概念和实际问题
前言
在实际工作中经常会碰到这样的情况:大家都能说几个高并发的名词,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 |
实际工作中的应用:
-
压测时:
- 先测接口维度:找出慢接口
- 再测系统维度:评估整体容量
-
监控时:
- 接口维度监控:实时看各接口QPS、响应时间
- 系统维度监控:看整体负载、资源使用率
-
限流时:
- 接口级限流:热点接口单独限流(如:查询接口限流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% 就要警惕了
典型症状:
- CPU使用率正常(30%-50%)
- 内存使用率正常(50%-60%)
- 数据库响应快(< 50ms)
- 但是接口响应慢(> 1s)
- 网络带宽使用率高(> 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<User> getUsers(@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 实际案例
案例:某电商系统商品列表页很慢
排查过程:
- 检查代码:响应时间50ms,没问题
- 检查数据库:查询时间30ms,也没问题
- 检查CPU:使用率40%,还是没问题
- 检查带宽:使用率95%,问题在这!
原因分析:
- 每个商品返回了20张高清大图URL和详细描述
- 每个请求返回数据约2MB
- 服务器带宽200Mbps(25MB/s)
- 理论QPS只有:25MB/s ÷ 2MB = 12.5
解决方案:
- 列表页只返回1张缩略图 + 简短描述
- 详情页才返回所有图片
- 图片使用CDN
- 启用Gzip压缩
优化效果:
- 单个请求数据从2MB降到50KB
- 理论QPS从12.5提升到500
- 实际QPS从10提升到300+
- 用户体验明显改善
三、压测:高并发系统的"体检报告"
3.1 什么是压测?
压力测试(Stress Testing) 是通过模拟大量用户并发访问,测试系统在高负载下的表现。
3.2 压测的目的
- 找出系统瓶颈:CPU、内存、数据库、网络哪个先扛不住?
- 确定系统容量:系统最多能支撑多少QPS?
- 验证扩容方案:加机器能提升多少性能?
- 为限流提供依据:应该限流到多少?
3.3 压测的关键指标
- 最大QPS:系统能承受的最大请求数
- 临界点QPS:响应时间开始明显增加的点(通常取P99 < 200ms时的QPS)
- 系统资源使用情况:CPU、内存、数据库连接数等
3.4 压测的常用工具
- JMeter:功能强大,支持多种协议
- Gatling:基于Scala,性能优秀
- wrk:命令行工具,轻量级
- 云压测平台:阿里云PTS、腾讯云压测等
四、限流:高并发系统的"安全阀"
4.1 为什么需要限流?
想象一下:系统经过压测,发现最大能承受 QPS=1000,但某天突然来了 QPS=5000 的流量。
不限流的后果:
- 服务器CPU、内存打满
- 所有请求响应变慢
- 数据库连接池耗尽
- 整个系统雪崩,所有用户都访问不了
限流的作用:保护系统,让部分请求正常处理,而不是让所有请求都失败。
4.2 限流阈值怎么确定?
限流值到底怎么定?很多人以为是拍脑袋定的,其实应该是压测出来的,但也不能完全照搬压测结果。
确定限流值的步骤:
-
压测找出最大QPS:比如压测结果是 QPS=1200 时,系统开始出现大量超时。
-
设置安全阈值:取压测值的 70%-80% 作为限流值。
- 原因:留出余地应对突发流量和系统抖动
- 所以:限流设置为 1000 左右
-
分场景限流:
- 核心接口:限流可以宽松一些(如1000)
- 非核心接口:限流可以严格一些(如500)
- 重资源接口(如导出、查询大数据):限流更严格(如100)
-
动态调整:
- 监控实际流量和系统表现
- 根据业务增长调整限流值
- 扩容后重新压测,调整限流值
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 云平台限流方案的优缺点分析
在实际工作中,很多团队会选择在云平台的接口层次或域名层次做限流,这是一种成熟且高效的实践方案。
优点:
- 快速生效:不需要发版,改配置即可
- 运维友好:可视化配置,不需要懂代码
- 统一管理:所有限流规则在一个平台管理
- 成本可控:云平台提供的限流功能性价比高
- 防护全面:可以防御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状态码和业务状态码完美配合。
现实情况(更常见):
- 只关注业务状态码:HTTP都是200,业务code区分成功/失败
- 统一网关处理:有些状态码(如503、504、429)由全公司统一的大网关返回,不是业务系统做的
- 职责分层:
- 网关层:处理限流、熔断、服务未部署、超时等基础设施问题
- 业务系统:只处理业务逻辑,返回业务状态码
这是大多数公司的实际做法。
让我们分别说明这两种情况:
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状态码 + 业务code | 429 + 42901 |
| 业务系统 | 业务逻辑、数据处理、业务校验 | HTTP 200 + 业务code | 200 + 40002 |
6.3.4 两种方案对比
| 对比项 | 只用业务code | 网关+业务code |
|---|---|---|
| HTTP状态码 | 永远200 | 网关返回真实状态码 |
| 业务系统复杂度 | 需处理限流、超时等 | 只关注业务逻辑(更简单) |
| 监控 | 需从响应体解析 | HTTP状态码直接统计(更方便) |
| 前端复杂度 | 简单(只看code) | 需同时处理HTTP和code |
| 统一性 | 每个系统自己实现 | 全公司统一(更规范) |
| 适用场景 | 小团队、系统少 | 中大型公司、微服务(更常见) |
6.3.5 实际工作建议
如果公司有统一网关:
-
业务开发人员:
- 只关注业务状态码
- 不需要处理限流、熔断、超时
- HTTP永远返回200
- 代码简单,专注业务
-
网关配置人员(运维/架构师):
- 配置限流规则
- 配置熔断规则
- 配置超时时间
- 监控网关层指标
-
前端开发人员:
- 兼容处理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、算法、缓存),成本低效果好。再优化架构(读写分离、负载均衡),成本适中。最后才考虑扩容(加机器、升配置),成本最高。
结语
高并发系统的优化,没有一劳永逸的方案,都是在实践中慢慢摸索出来的。
几个关键点:压测是基础,没有压测就无法准确评估系统容量;限流是保命的,不要等系统崩溃了才想起来做限流;用户体验最重要,技术手段最终还是要为用户服务。