高并发系统设计实战指南
高并发系统设计是架构师的必修课,也是大厂面试的高级话题,CSDN 架构板块的流量之王。本文从性能指标讲起,覆盖分层架构、缓存体系、消息削峰、数据库扩展、读写分离、分库分表、限流降级、全链路压测,最后用一个电商秒杀案例完整串起来。
一、高并发的度量指标
1.1 核心指标
| 指标 | 含义 | 参考值 |
|---|---|---|
| QPS | 每秒查询数(读) | 单机几百~几千 |
| TPS | 每秒事务数(写) | 单机几百 |
| 并发数 | 同时处理的请求数 | 与线程池/连接池相关 |
| 响应时间 | 单请求耗时 | p99 < 200ms 算好 |
| 可用性 | 系统可用时间占比 | 99.9% ~ 99.99% |
QPS 计算:一天 1000 万 PV,高峰在 2 小时,高峰 QPS ≈ 1000万×0.8 / (2×3600) ≈ 1100。
性能指标看 p99 不看平均值:平均响应时间会被少数慢请求拉偏,p99(99% 的请求小于该值)才代表真实体验。
1.2 高并发本质
高并发不是"一台机器很牛",而是把压力分散:
单机 1000 QPS → 10 台机器 → 10000 QPS
核心思路:无状态化(任何一台机器都能处理任何请求)+ 水平扩展(加机器)+ 分层防御(缓存扛读、MQ 扛写)。
二、分层架构
2.1 经典五层
用户 → CDN(静态资源加速)
→ 负载均衡(Nginx/LB)
→ 网关(鉴权、限流)
→ 应用层(无状态服务集群)
→ 缓存层(Redis)
→ 数据层(MySQL + 分库分表 + 读写分离)
每一层的作用:
- CDN:静态资源(图片/JS)就近返回,省源站流量;
- 负载均衡:流量分发,健康检查;
- 网关:统一入口,限流鉴权;
- 应用层 :业务逻辑,必须无状态(Session 放 Redis);
- 缓存:扛 80% 的读;
- 数据层:最终一致性,扛 20% 的读写。
2.2 无状态设计(关键)
有状态的问题:用户登录信息存内存,用户下次请求到另一台机器就"掉线"了。
解决方案:状态外置。
Session → Redis(共享)
Token(JWT)→ 客户端保存,服务端无状态
yaml
# Spring Session 存 Redis
spring:
session:
store-type: redis
无状态的好处:随便加机器、机器随便挂、流量随便切。
三、缓存体系(读多写少的核心)
3.1 多级缓存
本地缓存(Caffeine,毫秒级,每台机器一份)
→ Redis(分布式缓存,统一)
→ 数据库(兜底)
为什么多级:本地缓存不经过网络(最快),Redis 统一共享(一致性好),数据库保底。
3.2 缓存三大问题(必考)
穿透(查不存在的 key):
java
// 方案:缓存空值 + 布隆过滤器
if (bloomFilter.mightContain(key)) {
// 才允许查库
}
击穿(热点 key 过期瞬间):
java
// 方案:互斥锁,只放一个线程查库
String lockKey = "lock:" + key;
boolean locked = redis.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) { waitAndRetry(); return; }
try { /* 查库 + 重建缓存 */ } finally { redis.delete(lockKey); }
雪崩(大量 key 同时过期):
java
// 方案:过期时间加随机值
int expire = 3600 + ThreadLocalRandom.current().nextInt(600);
redis.set(key, value, expire, TimeUnit.SECONDS);
3.3 缓存一致性
**Cache-Aside(旁路缓存)**标准做法:
读:先查缓存 → 没有查库 → 回填缓存
写:先更新数据库 → 再删缓存(不是更新缓存!)
java
@Transactional
public void updateUser(User user) {
userMapper.update(user); // 1. 先更新数据库
redis.delete("user:" + user.id); // 2. 再删缓存
}
为什么删缓存而不是更新缓存:更新缓存有并发问题(两个线程同时写),删缓存让下次读时重建,简单可靠。
极端一致:用 Canal 监听 binlog 异步删缓存,或消息队列补偿。
四、消息队列削峰(写多场景)
4.1 削峰原理
高峰 10 万请求/s
↓(MQ 缓冲)
消费端按 1000/s 处理
典型场景:秒杀、下单、积分、异步通知。
4.2 秒杀下单异步化
用户请求 → 网关限流 → 秒杀接口(只写 Redis 预扣)
→ 发 MQ「秒杀成功」
→ 消费者异步:真实扣库存 + 创建订单 + 通知用户
好处:接口毫秒级返回(抢到没抢到立刻知道),数据库不被瞬间打爆。
4.3 消息可靠性(三环节)
生产:acks=all + 重试 + 幂等
Broker:多副本(replication-factor=2)
消费:手动提交 + 业务幂等(唯一键去重)
五、数据库扩展
5.1 读写分离
写 → 主库(1 个)
读 → 从库(多个,binlog 同步)
适用:读多写少(80% 读)。应用层用 ShardingSphere/MyCat 或代码路由。
注意:
- 主从有同步延迟(秒级)------刚写完立刻读可能读不到("写后读"问题);
- 解决:写后读强制走主库,或从库读失败回主库重试。
5.2 分库分表
数据量到千万/亿级,单表扛不住:
分库:按业务拆库(订单库/用户库/商品库)
分表:单库单表拆多表
- 水平分表:按 ID 取模 / 范围 / 一致性哈希
sql
-- 订单表按 user_id 分 16 张表
order_0, order_1, ..., order_15
-- 路由规则:user_id % 16
分片键选择(关键):
- 按高频查询条件选分片键(订单按 user_id 分,查用户订单直接路由);
- 分片键之外的查询变"全分片扫"(要维护映射表或 ES 辅助);
- 跨分片 JOIN 不行 → 冗余字段/聚合查询。
分库分表带来的新问题:
- 全局主键(雪花 ID、美团 Leaf);
- 跨分片分页(归并排序);
- 分布式事务(最终一致)。
5.3 什么时候该分
单表 < 500 万 → 索引优化
500 万 ~ 2000 万 → 读写分离 + 缓存
2000 万+ → 分库分表
经验:先缓存、先读写分离,最后才分库分表------分库分表是最后的武器。
六、限流与降级
6.1 限流算法
| 算法 | 原理 | 特点 |
|---|---|---|
| 固定窗口 | 每秒计数 | 简单,临界问题 |
| 滑动窗口 | 窗口细分 | 更平滑 |
| 令牌桶 | 匀速放令牌 | 允许突发,主流(Guava RateLimiter) |
| 漏桶 | 匀速出 | 强控速 |
Sentinel 限流(前面文章已覆盖):QPS 阈值 + 排队等待/直接拒绝。
6.2 降级与熔断
熔断:错误率超阈值 → 快速失败(不继续调下游)
降级:走兜底逻辑(返回默认值/提示)
隔离:线程池隔离(一个下游满不拖垮其他)
java
@SentinelResource(value = "getProduct", fallback = "productFallback")
public Product getProduct(Long id) {
return productClient.getProduct(id);
}
public Product productFallback(Long id, Throwable e) {
return Product.empty("商品服务暂时不可用");
}
6.3 系统保护三板斧
- 超时:所有下游调用设超时(连接 3s + 读 5s);
- 重试:幂等接口重试 1-2 次(带退避);
- 兜底:缓存/默认值/人工处理。
七、高可用设计
7.1 可用性公式
可用性 = 运行时间 / 总时间
99.9% = 一年故障 8.76 小时
99.99% = 一年故障 52 分钟
7.2 关键手段
- 冗余:每个组件多实例(应用 2+、Redis 主从、MySQL 主从);
- 故障转移:挂了自动切换(Nacos 摘除、Sentinel 熔断、Keepalived VIP);
- 优雅上下线:滚动发布、先摘流量再停机;
- 容灾:多机房、跨区备份。
7.3 幂等设计(高并发必备)
场景:用户点了两次"支付",或超时重试------不能扣两次钱。
方案:
- 唯一约束:订单号唯一索引;
- 状态机:只有"待支付"才能支付;
- 幂等表:请求带 requestId,先查后写;
- Redis 防重:
setIfAbsent("pay:" + orderId, ...)。
八、全链路压测
8.1 为什么要压测
上线前必须知道系统真实极限------别等线上爆了才发现。
8.2 压测流程
1. 定目标:预估峰值 QPS(历史数据 × 增长系数)
2. 建环境:独立压测环境(或线上低峰)
3. 造数据:真实规模的数据量(几千万行)
4. 加压:wrk/JMeter/Locust 逐步加压
5. 观察:QPS、响应时间、CPU、连接数
6. 找瓶颈:哪一层先扛不住
7. 优化后再压
bash
# wrk 快速压测
wrk -t8 -c200 -d30s http://localhost:8080/api/order
8.3 常见瓶颈排查顺序
先看应用层(CPU/线程池满?)
→ 缓存命中率(Redis 打满?)
→ 数据库(慢 SQL?连接池满?)
→ 网络/网关(带宽?Nginx 连接数?)
九、实战:电商秒杀系统设计
9.1 需求与挑战
场景:1 万件商品,10 万人抢------瞬时 QPS 几万。
挑战:
- 数据库瞬间打爆;
- 超卖(卖出 1.5 万件);
- 用户重复请求。
9.2 分层设计
用户(App/浏览器)
→ CDN:静态页面
→ Nginx + 网关:限流(IP + 用户 + 总流量)
→ 秒杀接口(应用层)
├─ Redis 预扣库存(原子 DECR,防超卖)
├─ 校验(每人限购 1 件,Redis Set)
└─ 发 MQ「秒杀成功」
→ MQ 消费者(异步)
├─ 真实扣库存(数据库,幂等)
├─ 创建订单
└─ 通知用户(推送/站内信)
9.3 关键代码片段
Redis 原子扣库存(防超卖):
java
// 库存用 Redis 预扣:DECR 返回负数就是没抢到
Long remain = redis.opsForValue().decrement("seckill:stock:" + skuId);
if (remain < 0) {
redis.opsForValue().increment("seckill:stock:" + skuId); // 还回去
return Result.error("已售罄");
}
// 每人限购
Boolean first = redis.setIfAbsent("seckill:user:" + userId, skuId, 60, TimeUnit.SECONDS);
if (!first) {
return Result.error("每人限购一件");
}
// 发 MQ 异步下单
mq.send("seckill-success", orderDTO);
return Result.ok("抢购成功,正在生成订单");
数据库兜底(最终一致性):
sql
-- 数据库扣库存带条件(防超卖的最后防线)
UPDATE product SET stock = stock - 1
WHERE id = #{skuId} AND stock > 0;
MQ 消费幂等:
java
// 唯一键防重复消费
INSERT INTO seckill_order(order_id, user_id, sku_id)
VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE order_id = order_id;
9.4 秒杀要点总结
| 层 | 手段 |
|---|---|
| 前端 | 静态化、按钮置灰、倒计时 |
| 网关 | 限流(总 QPS + 单用户) |
| 缓存 | Redis 预扣库存、限购标记 |
| 异步 | MQ 削峰,接口毫秒返回 |
| 数据库 | 条件更新防超卖、幂等约束 |
| 降级 | 秒杀失败走兜底、服务保护 |
十、面试高频题速览
- QPS 怎么估算:PV/时间窗/峰值系数;
- 缓存三大问题:穿透/击穿/雪崩及方案;
- 缓存一致性:Cache-Aside,先更库再删缓存;
- 读写分离延迟:写后读强制走主库;
- 分库分表键选择:按高频查询条件;
- 全局 ID 方案:雪花算法、Leaf;
- 防超卖:Redis DECR + 数据库条件更新;
- 幂等设计:唯一约束、状态机、幂等表;
- 限流算法:令牌桶 vs 漏桶;
- 高可用:冗余 + 故障转移 + 幂等 + 降级。
本章小结
高并发设计的核心是分层防御 + 异步削峰 + 数据扩展 :缓存扛读、MQ 扛写、数据库做最终兜底;先无状态化、再水平扩展;先缓存和读写分离、最后才分库分表。记住------能用加机器解决的别用复杂架构,能用缓存解决的别动数据库。
五篇 CSDN 热门主题文档全部完成:Python 数据分析、Spring Cloud Alibaba、JavaScript 进阶、机器学习、高并发系统设计------覆盖数据分析、微服务、前端、AI、架构五大方向。
(全文完)