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

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

Scroll Down

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

前言

在实际工作中经常会碰到这样的情况:大家都能说几个高并发的名词,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<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 实际案例

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

排查过程

  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状态码 + 业务code 429 + 42901
业务系统 业务逻辑、数据处理、业务校验 HTTP 200 + 业务code 200 + 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、算法、缓存),成本低效果好。再优化架构(读写分离、负载均衡),成本适中。最后才考虑扩容(加机器、升配置),成本最高。


结语

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

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


参考资料