Skip to content

高并发系统设计

从"扛不住"到"稳如磐石"——限流熔断、缓存策略、队列削峰、读写分离、连接池优化、热点数据处理,用真实案例和可运行代码构建一套后端工程师的高并发生存指南。


1. 什么是高并发:先搞清楚你在解决什么问题

"高并发"可能是后端面试中出现频率最高的词,但很多人对它的理解停留在"能抗很多请求"。在动手写代码之前,先搞清楚三个问题:什么指标衡量"并发高不高"?高并发和高可用有什么区别?瓶颈通常出在哪里?

1.1 QPS、TPS、RT:三个核心指标

高并发的三把尺子:

  QPS(Queries Per Second)— 每秒查询数
  ═══════════════════════════════════
  → 服务器每秒能处理多少个请求
  → 衡量系统的吞吐量
  → 例子:商品详情页 QPS = 5000,表示每秒能响应 5000 次查询
  → ⚠️ QPS 是结果,不是原因——你不能"设置"QPS,只能优化系统让它更高
  
  TPS(Transactions Per Second)— 每秒事务数
  ═══════════════════════════════════
  → 每秒完成多少个完整业务事务
  → 一个事务可能包含多个请求(查库存→扣库存→创建订单→扣款)
  → TPS 通常 < QPS,因为一个事务 = 多次查询
  → 例子:下单 TPS = 500,意味着每秒完成 500 个订单
  
  RT(Response Time)— 响应时间
  ═══════════════════════════════════
  → 从请求发出到收到响应的时间
  → 通常看 P50(中位数)、P99(99% 请求的响应时间)
  → P99 比平均值重要!平均 50ms 但 P99 = 2s,说明 1% 用户体验极差
  → 例子:P99 = 200ms 意味着 99% 的请求在 200ms 内完成

三者的关系:

QPS 和 RT 的关系公式:

  QPS = 并发数 / 平均RT
  
  例子:
  → 10 个并发线程,每个请求 100ms
  → QPS = 10 / 0.1 = 100
  → 要提高 QPS?两条路:加并发数 或 降 RT
  
  QPS 的天花板:
  → 单线程最大 QPS = 1000 / RT(ms)
  → RT = 10ms → 单线程 QPS = 100
  → RT = 1ms  → 单线程 QPS = 1000
  → 所以:降低 RT 是提高 QPS 最直接的手段
指标衡量什么关注维度优化方向
QPS吞吐量越高越好加缓存、加机器、异步化
TPS业务处理能力越高越好减少事务耗时、拆分事务
RT(P99)用户体验越低越好减少 IO、优化 SQL、加缓存

💡 面试陷阱:面试官问"你的系统 QPS 是多少",你要追问"哪个接口的 QPS"。商品列表和下单创建订单的 QPS 差了一个数量级——不同接口不能混在一起说。

1.2 高并发 vs 高可用 vs 高性能:别混为一谈

三个概念经常混淆,但解决的是不同的问题:

  高并发(High Concurrency)
  ═══════════════════════════════════
  问题:同时来了 10 万个请求,系统能不能处理?
  关键词:吞吐量、QPS、扩容
  手段:缓存、异步、水平扩展、限流
  
  高可用(High Availability)
  ═══════════════════════════════════
  问题:服务器挂了一台,系统还能不能用?
  关键词:99.99%、冗余、故障转移
  手段:多副本、主从切换、熔断降级、异地多活
  
  高性能(High Performance)
  ═══════════════════════════════════
  问题:单个请求能不能在 50ms 内返回?
  关键词:响应时间、延迟
  手段:算法优化、减少 IO、缓存、连接池

它们的关系:

  高并发 ←──────→ 高性能
    ↑                ↑
    │   互相影响      │
    ↓                ↓
  高可用 ←──────→ 系统设计

  → 高性能(低 RT)有助于高并发(高 QPS)
  → 高并发需要高可用(流量大了更不能挂)
  → 但它们需要的技术不完全相同
  → 本书聚焦"高并发",但会涉及高可用和高性能的手段
维度高并发高可用高性能
核心指标QPS / TPS可用率(99.99%)RT / P99
核心手段扩容 + 缓存 + 异步冗余 + 故障转移优化算法 + 减少 IO
典型场景秒杀、春晚红包银行转账、支付搜索引擎、游戏实时对战
本书覆盖✅ 主线部分涉及部分涉及

1.3 瓶颈定位:CPU、IO、网络、数据库谁先扛不住

高并发系统的四类瓶颈:

  CPU 瓶颈
  ═══════════════════════════════════
  表现:CPU 使用率 > 80%,请求响应慢
  典型场景:复杂计算、JSON 序列化、加密解密
  排查:top / htop 看 CPU 使用率
  解法:优化算法、加缓存减少重复计算、水平扩展
  
  IO 瓶颈(磁盘/文件)
  ═══════════════════════════════════
  表现:磁盘 IO wait 高,数据库查询慢
  典型场景:大量日志写入、文件上传、数据库全表扫描
  排查:iostat / iotop 看磁盘读写
  解法:SSD、减少磁盘操作、读写分离
  
  网络瓶颈
  ═══════════════════════════════════
  表现:带宽打满、大量 TIME_WAIT 连接
  典型场景:返回大 JSON、文件下载、微服务间调用过多
  排查:iftop / netstat 看连接数和带宽
  解法:压缩响应、CDN、减少网络往返次数
  
  数据库瓶颈(最常见!)
  ═══════════════════════════════════
  表现:慢查询、连接数打满、锁等待
  典型场景:几乎所有 Web 应用的第一个瓶颈
  排查:慢查询日志、EXPLAIN、连接池监控
  解法:加缓存(第 2 章)、读写分离(第 6 章)、优化 SQL

瓶颈排查优先级(大多数 Web 应用):

典型后端系统的瓶颈出现顺序:

  数据库 → 网络 → CPU → 磁盘 IO
  ━━━━━━━   ━━━━   ━━━   ━━━━━━
  最先扛不住  其次   较少    最少

  所以高并发优化的通常路径是:
  1. 加缓存(减少数据库压力)   → 第 2 章
  2. 加限流(保护数据库不被打爆) → 第 3 章
  3. 异步化(数据库写入削峰)   → 第 5 章
  4. 读写分离(分担数据库负载)  → 第 6 章

常见系统的 QPS 基准线(单机参考):

组件典型 QPS说明
Nginx(静态文件)10,000 - 100,000纯转发,几乎不消耗 CPU
Python Web(FastAPI)1,000 - 5,000简单接口,无数据库
MySQL(简单查询)3,000 - 8,000主键查询,行级锁
Redis(读取)100,000+纯内存,极快
Python Web + MySQL500 - 2,000真实场景的瓶颈

💡 记住一个经验数字:单台 MySQL 在不做任何优化的情况下,大约能扛 3000-5000 QPS。你的 Web 应用如果 QPS 超过这个数字,第一反应应该是"先加 Redis 缓存"。

第 1 章核心知识回顾:

概念一句话解释
QPS每秒处理的请求数,衡量吞吐量
P99 RT99% 请求的响应时间,比平均值更能反映真实体验
QPS 公式QPS = 并发数 / 平均 RT,降 RT 是提高 QPS 最直接的手段
最常见瓶颈数据库——几乎所有 Web 应用第一个扛不住的都是它
优化路径缓存 → 限流 → 异步 → 读写分离,按这个顺序来

2. 缓存:高并发的第一道防线

上一章说了,数据库是大多数系统最先扛不住的组件。解决方案的第一步几乎永远是——加缓存。这一章讲清楚缓存的策略选择和三大致命问题。

2.1 为什么加 Redis 就能扛 10 倍流量

MySQL vs Redis 速度对比:

  MySQL(磁盘)
  ═══════════════════════════════════
  → 数据存在磁盘上,查询需要磁盘 IO
  → 简单查询:1-10ms
  → 复杂查询(JOIN + WHERE):10-100ms
  → 单机 QPS 上限:3000-8000
  
  Redis(内存)
  ═══════════════════════════════════
  → 数据存在内存里,纯内存操作
  → 读取延迟:0.1-0.5ms(比 MySQL 快 10-100 倍)
  → 单机 QPS 上限:100,000+
  → 所以:把热数据放 Redis,数据库压力瞬间降 10 倍
加缓存前后的架构对比:

  不加缓存:
  ═══════════════════════════════════
  客户端 ──▶ Web API ──▶ MySQL(每次都查库)

                     QPS 5000 就顶不住了

  加了 Redis 缓存:
  ═══════════════════════════════════
  客户端 ──▶ Web API ──▶ Redis(命中?返回!)

                           ╳ 未命中


                         MySQL(查库 → 写回 Redis)
                           
  → 假设缓存命中率 90%
  → 10000 QPS × 10% = 只有 1000 QPS 打到 MySQL
  → MySQL 轻松应对

缓存命中率与数据库压力的关系:

缓存命中率总 QPS = 10,000 时到达 MySQL 的 QPS数据库压力
0%(无缓存)10,000💀 扛不住
80%2,000😰 有点紧张
90%1,000😊 轻松
95%500😎 很舒服
99%100🎉 毫无压力

💡 缓存命中率从 90% 提升到 99%,数据库压力降低了 10 倍。高并发优化的核心就是想尽办法提高缓存命中率。常见手段:合理设置过期时间、预热热点数据、使用多级缓存。

2.2 三种缓存模式:Cache-Aside、Read-Through、Write-Behind

三种模式的核心区别:

  Cache-Aside(旁路缓存)— 最常用 ⭐
  ═══════════════════════════════════
  读:先查缓存 → 未命中 → 查数据库 → 写入缓存
  写:先写数据库 → 删除缓存(不是更新缓存!)
  → 业务代码自己控制缓存逻辑
  → 90% 的场景用这个就够了
  
  Read-Through(读穿透)
  ═══════════════════════════════════
  读:业务只和缓存交互 → 缓存内部去查数据库
  写:同 Cache-Aside
  → 缓存层封装了数据库访问,业务代码更简单
  → 需要缓存中间件支持(如 Spring Cache)
  
  Write-Behind(异步写回)
  ═══════════════════════════════════
  写:先写缓存 → 异步批量写数据库
  → 写性能极高(不等数据库)
  → 风险:缓存挂了数据就丢了
  → 适合:日志、计数器等允许少量丢失的场景

Cache-Aside 模式 Python 代码:

python
import redis
import json

r = redis.Redis()

async def get_product(product_id: int):
    """Cache-Aside 读取"""
    # 1. 先查缓存
    cache_key = f"product:{product_id}"
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)  # 命中,直接返回
    
    # 2. 未命中,查数据库
    product = await db.fetch_product(product_id)
    if product is None:
        # 🔥 防穿透:缓存空值,短过期
        r.setex(cache_key, 60, json.dumps(None))
        return None
    
    # 3. 写入缓存(设置过期时间)
    r.setex(cache_key, 3600, json.dumps(product))
    return product

async def update_product(product_id: int, data: dict):
    """Cache-Aside 写入"""
    # 1. 先写数据库
    await db.update_product(product_id, data)
    # 2. 删除缓存(不是更新!)
    r.delete(f"product:{product_id}")

💡 为什么删除缓存而不是更新缓存? 因为并发场景下,两个请求同时更新缓存可能导致脏数据。假设 A 写完DB、B 写完DB、B 更新缓存、A 更新缓存——缓存里存的是 A 的旧值。删除缓存让下一次读重新从 DB 加载,简单且安全。

模式一致性性能复杂度适用场景
Cache-Aside较好✅ 大多数场景首选
Read-Through较好框架支持时使用
Write-Behind弱(可能丢数据)极高日志、计数器、允许丢失

2.3 缓存三大杀手:穿透、雪崩、击穿

三大缓存问题——面试高频 + 生产中真的会遇到:

  缓存穿透(Cache Penetration)
  ═══════════════════════════════════
  问题:查询一个不存在的数据(如 id = -1)
  → 缓存没有 → 数据库也没有 → 每次都穿透到 DB
  → 恶意攻击大量请求不存在的 ID → 数据库被打爆
  
  解决方案:
  ① 缓存空值:查不到也缓存 None,短过期(60s)
  ② 布隆过滤器:请求先过滤器判断 ID 是否可能存在
     → 不存在的 ID 直接拒绝,不查库
  ③ 参数校验:ID < 0 直接拒绝
  
  
  缓存雪崩(Cache Avalanche)
  ═══════════════════════════════════
  问题:大量缓存同时过期 → 所有请求瞬间打到 DB
  → 通常发生在:缓存批量导入时设了相同过期时间
  → 或 Redis 节点宕机
  
  解决方案:
  ① 过期时间加随机值:TTL = 3600 + random(0, 300)
  ② 多级缓存:本地缓存 + Redis,Redis 挂了本地兜底
  ③ 限流降级:缓存失效时对 DB 查询限流
  ④ Redis 高可用:哨兵 + 集群,避免单点故障
  
  
  缓存击穿(Cache Breakdown)
  ═══════════════════════════════════
  问题:某个热点 Key 过期的瞬间,大量请求同时打到 DB
  → 和雪崩的区别:雪崩是大面积过期,击穿是单个热点 Key
  → 例子:首页推荐数据缓存过期 → 瞬间 10000 请求查 DB
  
  解决方案:
  ① 互斥锁:只让一个请求查 DB,其他等待
  ② 热点不过期:热点 Key 永不过期,后台异步更新
  ③ 逻辑过期:缓存里存过期时间,发现过期后异步更新

互斥锁防击穿 Python 实现:

python
async def get_hot_data(key: str):
    """互斥锁防击穿"""
    cached = r.get(key)
    if cached:
        return json.loads(cached)
    
    # 尝试获取分布式锁
    lock_key = f"lock:{key}"
    if r.set(lock_key, "1", nx=True, ex=5):  # 5 秒自动释放
        try:
            # 拿到锁,查数据库
            data = await db.fetch(key)
            r.setex(key, 3600, json.dumps(data))
            return data
        finally:
            r.delete(lock_key)
    else:
        # 没拿到锁,等 50ms 后重试
        await asyncio.sleep(0.05)
        return await get_hot_data(key)  # 递归重试
问题原因核心解法
穿透查不存在的数据缓存空值 + 布隆过滤器
雪崩大量缓存同时过期过期时间加随机值
击穿热点 Key 过期互斥锁 / 永不过期

💡 面试回答技巧:穿透 → "查不存在的";雪崩 → "大面积同时过期";击穿 → "单个热点过期"。先说清楚区别,再说解决方案,最后说你在项目中用了哪个。

第 2 章核心知识回顾:

概念一句话解释
缓存命中率90%→99% 数据库压力降 10 倍,是最关键的指标
Cache-Aside先查缓存→未命中查 DB→写缓存,90% 场景用这个
删除而非更新写 DB 后删缓存,避免并发更新导致脏数据
穿透查不存在的数据,用缓存空值 + 布隆过滤器
雪崩/击穿大面积 / 单热点过期,用随机 TTL / 互斥锁

3. 限流:保护系统的最后一道门

缓存能扛住大部分读流量,但对于写操作(下单、支付)或缓存穿透场景,流量还是会打到后端。限流就是在系统扛不住之前,主动拒绝超出能力的请求——宁可让少数用户失败,也不能让所有用户一起挂。

3.1 不限流的后果:一个请求拖垮整个系统

不限流的连锁崩溃过程:

  正常状态(QPS = 1000,系统能力 = 2000)
  ═══════════════════════════════════
  请求 ──▶ Web 服务 ──▶ 数据库 ✅ 一切正常
  
  突发流量(QPS = 5000,超出系统能力)
  ═══════════════════════════════════
  Step 1: 5000 请求涌入 → 数据库连接池打满
  Step 2: 新请求排队等连接 → 响应时间从 50ms 飙到 5s
  Step 3: 前端超时重试 → 请求量翻倍到 10000
  Step 4: 线程池/进程池全被占满 → 健康检查接口也超时
  Step 5: 负载均衡认为节点挂了 → 流量转到其他节点
  Step 6: 其他节点也被打爆 → 全站崩溃 💀
  
  核心问题:没有"保险丝"
  → 电路过载了要跳闸(熔断 → 第 4 章)
  → 水压太大了要减压阀(限流 → 本章)

限流的本质——有损保护:

                  不限流              限流
                  ══════              ════
  QPS = 5000     全部放进来         只放 2000 进来
                  ↓                  ↓
  结果           全部超时 5s        2000 正常 + 3000 快速返回 429
                  ↓                  ↓
  用户体验        所有人都挂         60% 用户正常,40% 稍后重试

💡 限流不是目的,保护系统才是。被限流的请求应该快速返回错误(HTTP 429 Too Many Requests),而不是在队列里干等。快速失败 + 前端引导重试 = 用户体验可控。

3.2 四种限流算法:从固定窗口到令牌桶

四种限流算法对比:

  ① 固定窗口(Fixed Window)
  ═══════════════════════════════════
  原理:每个时间窗口(如 1 秒)内计数,超过阈值就拒绝
  |----1s----|----1s----|
  | 100 请求  | 100 请求  |   限制:100/s
  缺点:窗口边界可能突发 2 倍流量
  → 第 1 秒末 99 个 + 第 2 秒初 99 个 = 1 秒内 198 个 ❌
  
  ② 滑动窗口(Sliding Window)
  ═══════════════════════════════════
  原理:窗口按时间滑动,精确统计最近 N 秒的请求数
  |---滑动---▶|
  更精确,但内存开销大(要记录每个请求的时间戳)
  
  ③ 漏桶(Leaky Bucket)
  ═══════════════════════════════════
  原理:请求先进桶,以固定速率漏出处理
      │ 请求涌入 │
      ▼          ▼
  ┌────────────────┐
  │   桶(队列)    │ ← 桶满了就拒绝
  └──────┬─────────┘
         ▼ 固定速率漏出
      处理请求
  优点:输出速率恒定
  缺点:突发流量也被平滑,无法利用空闲时段
  
  ④ 令牌桶(Token Bucket)— 最推荐 ⭐
  ═══════════════════════════════════
  原理:以固定速率生成令牌,请求拿到令牌才能通过
  → 桶里有令牌 → 直接通过
  → 桶空了 → 等待或拒绝
  → 桶可以积攒令牌 → 允许一定突发流量
  优点:平时稳定,突发时可以用积攒的令牌

令牌桶 Python 实现:

python
import time

class TokenBucket:
    """令牌桶限流器"""
    
    def __init__(self, rate: float, capacity: int):
        self.rate = rate          # 每秒生成令牌数
        self.capacity = capacity  # 桶的最大容量
        self.tokens = capacity    # 当前令牌数
        self.last_time = time.time()
    
    def allow(self) -> bool:
        """检查是否允许通过"""
        now = time.time()
        # 补充令牌
        elapsed = now - self.last_time
        self.tokens = min(
            self.capacity,
            self.tokens + elapsed * self.rate
        )
        self.last_time = now
        
        # 消耗令牌
        if self.tokens >= 1:
            self.tokens -= 1
            return True   # 允许通过
        return False      # 限流拒绝

# 使用:每秒 100 个请求,最多积攒 200 个令牌
limiter = TokenBucket(rate=100, capacity=200)

if limiter.allow():
    handle_request()
else:
    return Response(status_code=429, content="请求过多,请稍后重试")
算法突发流量精确度复杂度推荐场景
固定窗口❌ 边界突发最低简单场景、原型
滑动窗口✅ 精确精确限流
漏桶❌ 强制平滑恒定速率处理
令牌桶✅ 允许突发✅ 大多数场景首选

3.3 分布式限流:Redis + Lua 实战

单机令牌桶只能限单个进程的流量。分布式环境下多台机器共享限流计数器,需要 Redis。

为什么用 Redis + Lua?

  问题:多台机器分别计数 → 总量超标
  ═══════════════════════════════════
  限制 100/s,4 台机器各限 25/s?
  → 不均匀!某台可能收到 80 请求,其他 3 台只收 20
  → 必须用全局计数器
  
  为什么用 Lua?
  ═══════════════════════════════════
  → Redis 是单线程,但 GET + INCR + EXPIRE 是 3 个命令
  → 两个请求可能在 GET 和 INCR 之间交错 → 计数不准
  → Lua 脚本在 Redis 中原子执行,不会被中断

Redis + Lua 滑动窗口限流脚本:

python
import redis
import time

r = redis.Redis()

# Lua 脚本:滑动窗口限流(原子操作)
SLIDING_WINDOW_SCRIPT = """
local key = KEYS[1]
local window = tonumber(ARGV[1])    -- 窗口大小(秒)
local limit = tonumber(ARGV[2])     -- 窗口内最大请求数
local now = tonumber(ARGV[3])       -- 当前时间戳(毫秒)

-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)

if count < limit then
    -- 未超限,添加当前请求
    redis.call('ZADD', key, now, now .. math.random())
    redis.call('EXPIRE', key, window)
    return 1   -- 允许
else
    return 0   -- 拒绝
end
"""

# 注册脚本
script = r.register_script(SLIDING_WINDOW_SCRIPT)

def is_allowed(user_id: str, window: int = 1, limit: int = 100) -> bool:
    """检查是否允许通过"""
    key = f"rate_limit:{user_id}"
    now = int(time.time() * 1000)  # 毫秒时间戳
    result = script(keys=[key], args=[window, limit, now])
    return result == 1

FastAPI 中间件接入:

python
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    client_ip = request.client.host
    if not is_allowed(client_ip, window=1, limit=100):
        raise HTTPException(
            status_code=429,
            detail="请求过于频繁,请稍后重试"
        )
    return await call_next(request)

💡 生产环境限流策略:① API 网关层(Nginx / Kong)做全局粗粒度限流 ② 应用层做细粒度限流(按用户/按接口)③ 关键接口单独配限流规则(如下单接口 50/s/用户)。

第 3 章核心知识回顾:

概念一句话解释
限流本质有损保护——拒绝部分请求,保住整个系统
令牌桶允许突发流量 + 整体限速,大多数场景首选
Redis + Lua分布式限流的标准方案,原子操作保证计数准确
HTTP 429限流返回码,配合 Retry-After 头部引导重试
分层限流网关粗粒度 + 应用细粒度,双重保护

4. 熔断与降级:优雅地失败

限流保护的是"入口"——拒绝超量请求。但如果问题出在下游(调用的某个服务挂了),限流帮不上忙。这时候需要熔断(断开故障服务)和降级(用备选方案兜底)。

4.1 服务雪崩:一个慢接口引发的连锁事故

服务雪崩的真实路径:

  正常状态:
  ═══════════════════════════════════
  订单服务 ──▶ 库存服务(50ms)──▶ 返回 ✅
  订单服务 ──▶ 支付服务(100ms)──▶ 返回 ✅
  订单服务 ──▶ 推荐服务(30ms)──▶ 返回 ✅
  
  推荐服务出问题了(RT 从 30ms 飙到 10s):
  ═══════════════════════════════════
  Step 1: 订单服务调推荐服务 → 等待 10s → 线程被占住
  Step 2: 请求不断涌入 → 线程池 50 个线程全在等推荐服务
  Step 3: 新的订单请求进来 → 没有空闲线程 → 排队
  Step 4: 订单接口从 100ms 变成 10s+ → 前端超时
  Step 5: 用户看到下单失败 → 疯狂重试
  Step 6: 订单服务彻底挂了 → 连带库存和支付也用不了 💀

  根本原因:
  → 一个非核心服务(推荐)的故障
  → 拖垮了核心服务(订单)
  → 因为没有隔离和熔断机制
限流 vs 熔断 vs 降级——三者的区别:

  限流(第 3 章):
  → 控制入口流量,"不让太多人进来"
  → 保护自己不被打爆
  
  熔断(本章 4.2):
  → 发现下游故障后快速失败,"不去叫他了"
  → 防止故障传播、保护上游
  
  降级(本章 4.3):
  → 故障时提供备选方案,"虽然不完美但能用"
  → 保证核心功能可用

💡 类比:限流是高速公路入口的红绿灯限行。熔断是发现前方桥断了,直接掉头不过去。降级是桥断了走旁边的小路(慢一点但能到)。

4.2 熔断器模式:Closed → Open → Half-Open

熔断器状态机(来自 Netflix Hystrix 的经典设计):

  ┌──────────┐   失败率 > 阈值   ┌──────────┐
  │  CLOSED  │ ═══════════════▶ │   OPEN   │
  │ (正常) │                   │ (熔断) │
  └────┬─────┘                   └────┬─────┘
       ↑                              │
       │  探测请求成功                  │ 等待超时时间
       │                              ↓
       │                         ┌──────────┐
       └═════════════════════════│HALF-OPEN │
                  失败 → 回 OPEN │ (探测) │
                                └──────────┘

  CLOSED(关闭 = 正常请求通过):
  → 正常调用下游服务
  → 统计失败率,超过阈值 → 切到 OPEN
  
  OPEN(打开 = 请求被拦截):
  → 所有请求直接返回错误(快速失败)
  → 不再调用下游服务
  → 等待一段时间(如 30 秒)→ 切到 HALF-OPEN
  
  HALF-OPEN(半开 = 试探性恢复):
  → 放一个请求去试探下游
  → 成功 → 切回 CLOSED,恢复正常
  → 失败 → 切回 OPEN,继续等待

Python 熔断器实现:

python
import time
from enum import Enum

class State(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    """简易熔断器"""
    
    def __init__(
        self,
        failure_threshold: int = 5,    # 连续失败 N 次触发熔断
        recovery_timeout: int = 30,     # 熔断后等待 N 秒再探测
    ):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.state = State.CLOSED
        self.failure_count = 0
        self.last_failure_time = 0
    
    async def call(self, func, *args, fallback=None, **kwargs):
        """带熔断的调用"""
        # OPEN 状态:快速失败
        if self.state == State.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = State.HALF_OPEN  # 超时了,试探
            elif fallback:
                return await fallback()       # 执行降级
            else:
                raise Exception("Circuit breaker is OPEN")
        
        try:
            result = await func(*args, **kwargs)
            # 成功:重置计数
            if self.state == State.HALF_OPEN:
                self.state = State.CLOSED     # 探测成功,恢复
            self.failure_count = 0
            return result
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = time.time()
            if self.failure_count >= self.failure_threshold:
                self.state = State.OPEN       # 触发熔断
            if fallback:
                return await fallback()
            raise e

# 使用示例
breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30)

async def get_recommendations(user_id):
    return await breaker.call(
        recommendation_service.get,      # 正常调用
        user_id,
        fallback=lambda: {"items": []}   # 降级:返回空推荐
    )

4.3 降级策略:体面地告诉用户"稍后再试"

熔断之后不能只返回错误——要有降级方案,让用户感知尽量小。

四种降级策略:

  ① 返回默认值 / 空结果
  ═══════════════════════════════════
  场景:推荐服务挂了
  降级:返回空推荐列表 []
  用户感知:推荐位空了,但不影响下单
  
  ② 缓存兜底
  ═══════════════════════════════════
  场景:商品详情服务超时
  降级:返回上次缓存的商品数据(可能不是最新价格)
  用户感知:看到的可能是 5 分钟前的价格
  
  ③ 功能裁剪(关闭非核心功能)
  ═══════════════════════════════════
  场景:大促期间系统压力大
  降级:关闭评论、推荐、个性化等非核心功能
  用户感知:只能浏览和下单,暂时不能写评论
  
  ④ 排队等待
  ═══════════════════════════════════
  场景:秒杀抢购
  降级:把请求放入队列,"您前面还有 XX 人"
  用户感知:在排队,比直接报错体验好

降级决策的核心原则:

功能重要性分级:

  P0(绝不能降级):下单、支付、登录
  P1(最后才降级):商品详情、库存查询
  P2(可以降级):推荐、评论、历史记录
  P3(优先降级):数据统计、日志上报、非实时通知

  压力逐级增大时的降级顺序:
  P3 → P2 → P1(P0 永远不降级)
python
# FastAPI 降级装饰器示例
from functools import wraps

def with_fallback(fallback_value):
    """降级装饰器:异常时返回兜底值"""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            try:
                return await func(*args, **kwargs)
            except Exception as e:
                logger.warning(f"{func.__name__} 降级: {e}")
                return fallback_value
        return wrapper
    return decorator

@with_fallback(fallback_value={"items": [], "degraded": True})
async def get_recommendations(user_id: int):
    """获取推荐——如果推荐服务挂了,返回空列表"""
    return await recommendation_service.get(user_id)

💡 降级不是 bug,是设计好的预案。上线前就要想清楚:每个外部依赖挂了怎么办?写在代码里,而不是等线上出事再临时想方案。

第 4 章核心知识回顾:

概念一句话解释
服务雪崩一个慢服务拖垮整个调用链
熔断器三态CLOSED→OPEN→HALF-OPEN,自动探测恢复
降级策略默认值 / 缓存兜底 / 功能裁剪 / 排队等待
功能分级P0 绝不降级,P3 优先降级
限流 vs 熔断限流保护入口,熔断保护调用链

5. 队列削峰:把洪水变成细流

限流是"拒绝多余请求",但有些请求不能拒绝(比如用户下单)。这时候的思路是——不拒绝,但不立刻处理。先放到队列里,后台慢慢消化。

5.1 同步 vs 异步:为什么不是所有请求都要立刻处理

同步处理(传统模式):
═══════════════════════════════════
用户下单 ──▶ 扣库存 ──▶ 创建订单 ──▶ 发短信 ──▶ 返回成功
                    总耗时:500ms
→ 用户等 500ms 才能看到结果
→ 每个步骤都占着线程/连接
→ QPS 受最慢环节限制

异步处理(队列模式):
═══════════════════════════════════
用户下单 ──▶ 扣库存 ──▶ 创建订单 ──▶ 返回"下单成功"(200ms)

                                      ▼ 异步
                              消息队列 ──▶ 发短信
                                      ──▶ 更新统计
                                      ──▶ 推送通知
→ 用户只等核心操作(200ms)
→ 非核心操作异步执行,不阻塞主流程

哪些操作可以异步化?

操作同步/异步理由
扣库存✅ 同步必须立刻确认(防超卖)
创建订单✅ 同步用户需要看到订单号
发短信/邮件异步 ✅延迟几秒用户无感
更新统计报表异步 ✅不影响用户体验
推送通知异步 ✅延迟可接受
生成发票异步 ✅后台处理,用户稍后查看
削峰填谷的原理:

  没有队列(同步处理):
  ═══════════════════════════════════
  请求量:  ████████████                     (峰值 5000 QPS)
  处理量:  ████████████                     (系统也必须 5000 QPS)
  → 系统处理能力 < 峰值 → 崩溃 💀

  有队列(异步处理):
  ═══════════════════════════════════
  请求量:  ████████████                     (峰值 5000 QPS)
  队列:    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓               (缓冲)
  处理量:  ████ ████ ████ ████ ████         (稳定 1000 QPS)
  → 峰值被"削平",低谷被"填满"
  → 系统以恒定速率处理,不会崩溃

💡 核心思维转换:"用户点了下单,一定要在这个请求里完成所有事吗?" 不一定。只做必须同步的(扣库存、创建订单),其他全部扔到队列异步处理。

5.2 消息队列削峰:秒杀场景实战

秒杀场景的队列削峰架构:

  不用队列(直接处理):
  ═══════════════════════════════════
  10:00:00 秒杀开始 → 10000 请求同时到达
  → 数据库连接池 100 → 9900 请求排队
  → 数据库 CPU 100% → 超时 → 全部失败 💀

  用队列(削峰):
  ═══════════════════════════════════
  用户请求 ──▶ 接入层(校验 + 限流)

              ▼ 放入队列(瞬间完成)
         ┌──────────┐
         │ 消息队列  │ ← 10000 条消息缓冲
         └────┬─────┘
              ▼ 消费者以稳定速率处理
         扣库存 + 创建订单(500/s)


         推送结果给用户(WebSocket / 轮询)

Redis List 作为轻量消息队列:

python
import redis
import json
import time

r = redis.Redis()

# ========= 生产者(接收请求,放入队列)=========
async def seckill_request(user_id: int, product_id: int):
    """秒杀请求:校验后放入队列"""
    # 1. 前置校验(快速拒绝)
    stock = r.get(f"stock:{product_id}")
    if not stock or int(stock) <= 0:
        return {"success": False, "msg": "已售罄"}
    
    # 2. 放入队列(毫秒级完成)
    message = json.dumps({
        "user_id": user_id,
        "product_id": product_id,
        "timestamp": time.time()
    })
    r.lpush("seckill_queue", message)
    
    return {"success": True, "msg": "排队中,请稍候查询结果"}

# ========= 消费者(后台稳定处理)=========
def seckill_consumer():
    """消费者:从队列取消息,逐个处理"""
    while True:
        # BRPOP:阻塞等待,有消息才返回
        result = r.brpop("seckill_queue", timeout=5)
        if result is None:
            continue
        
        _, message = result
        data = json.loads(message)
        
        try:
            # 原子扣减库存(Lua 脚本保证原子性)
            stock = r.decr(f"stock:{data['product_id']}")
            if stock < 0:
                # 库存不足,恢复
                r.incr(f"stock:{data['product_id']}")
                notify_user(data["user_id"], "抢购失败,已售罄")
                continue
            
            # 创建订单(写数据库)
            create_order(data["user_id"], data["product_id"])
            notify_user(data["user_id"], "抢购成功!")
        except Exception as e:
            # 处理失败,放回队列重试(或放入死信队列)
            r.lpush("seckill_dead_letter", message)
            logger.error(f"秒杀处理失败: {e}")

5.3 消费者设计:并发控制与消息积压

消费者的三个关键问题:

  ① 消费速度怎么控制?
  ═══════════════════════════════════
  → 消费太快 → 数据库又被打爆(削了个寂寞)
  → 消费太慢 → 消息堆积,用户等太久
  → 最佳实践:消费速率 = 数据库安全水位的 80%
  → 例子:DB 能扛 1000 QPS → 消费速率设 800/s
  
  ② 消息积压了怎么办?
  ═══════════════════════════════════
  → 监控队列长度,积压超过阈值立刻告警
  → 紧急方案:临时增加消费者数量
  → 极端方案:把消息转存到数据库/文件,稍后批量处理
  → 过期消息:超过 5 分钟的秒杀请求直接丢弃
  
  ③ 消息处理失败怎么办?
  ═══════════════════════════════════
  → 重试:失败的消息放回队列尾部,最多重试 3 次
  → 死信队列:重试 3 次还失败 → 放入死信队列人工处理
  → 幂等性:同一消息被处理 2 次结果应该一样
    → 用唯一订单号做幂等键,防止重复下单

消息队列选型:

队列适用场景QPS 级别特点
Redis List轻量级、延迟敏感10 万+简单、无持久化保证
RabbitMQ企业级、需要 ACK1-5 万成熟、功能丰富、保证投递
Kafka大数据、日志流百万级高吞吐、持久化、但延迟稍高
Redis StreamsRedis 生态、需要消费组10 万+Redis 5.0+,像轻量版 Kafka
python
# 多消费者并发处理(线程池模式)
import concurrent.futures

def run_consumers(num_workers: int = 4):
    """启动多个消费者并发处理"""
    with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as pool:
        futures = [pool.submit(seckill_consumer) for _ in range(num_workers)]
        # 等待(实际生产中用更优雅的停止机制)
        concurrent.futures.wait(futures)

# 4 个消费者 × 200/s = 总消费速率 800/s

💡 务实建议:小项目用 Redis List 就够了(代码简单、延迟低)。当你需要消息持久化、ACK 确认、死信队列等企业级特性时,再切换到 RabbitMQ。Kafka 留给日志和大数据场景。

第 5 章核心知识回顾:

概念一句话解释
削峰填谷用队列缓冲峰值流量,消费者以稳定速率处理
同步 vs 异步核心操作同步,非核心(通知/统计)异步
BRPOPRedis 阻塞取队列,有消息才返回,不浪费 CPU
死信队列处理失败的消息归档,防止丢失 + 人工介入
幂等性同一消息处理 N 次结果相同,防止重复操作

6. 数据库优化:高并发下的读写分离与分库分表

前面 5 章的手段(缓存、限流、熔断、队列)本质上都是在"减少到达数据库的请求数"。但最终总有请求要落到数据库——这一章讲怎么让数据库本身更能抗。

6.1 为什么数据库总是第一个扛不住

数据库的四大瓶颈:

  ① 连接数有限
  ═══════════════════════════════════
  → MySQL 默认 max_connections = 151
  → 每个连接占用内存(约 10MB)
  → 1000 连接 = 10GB 内存只用来维护连接
  → 连接打满 → 新请求直接报错 "Too many connections"
  
  ② 锁竞争严重
  ═══════════════════════════════════
  → 行锁:多个请求同时更新同一行 → 排队等锁
  → 表锁:DDL 操作会锁整张表
  → 死锁:A 等 B,B 等 A → 数据库自动检测并回滚一个
  → 锁等待 → 响应时间飙升
  
  ③ 磁盘 IO
  ═══════════════════════════════════
  → 数据最终存在磁盘上(即使有 Buffer Pool)
  → 复杂查询、全表扫描 → 大量随机磁盘读
  → 写操作需要 WAL(Write-Ahead Log)→ 磁盘顺序写
  
  ④ CPU 计算
  ═══════════════════════════════════
  → 复杂 JOIN、子查询、排序 → CPU 密集
  → 大结果集的序列化和网络传输

典型 Web 应用的读写比例:

大多数 Web 应用的读写比 = 8:2 到 9:1

  电商商品页:读 95%,写 5%
  社交 Feed 流:读 90%,写 10%
  后台管理系统:读 70%,写 30%

  这意味着:
  → 读请求是主要压力来源
  → 优化读(加缓存→第 2 章,读写分离→本章 6.2)
  → 保护写(限流→第 3 章,队列→第 5 章)

💡 数据库不是用来抗高并发的——它是用来保证数据一致性的。高并发的本质是尽量把流量挡在数据库前面(缓存/队列),让到达数据库的请求量在安全范围内。

6.2 读写分离:一主多从架构

读写分离架构:

  写请求 ──▶ 主库(Master)

              │ 主从复制(binlog 同步)

  读请求 ──▶ 从库 1(Slave)
         ──▶ 从库 2(Slave)
         ──▶ 从库 3(Slave)

  效果:
  → 写操作全走主库(1 台)
  → 读操作分散到从库(N 台)
  → 读 QPS 提升 N 倍(加从库就行)
  → 3 个从库 = 读 QPS × 3

Python 应用层路由实现:

python
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
import random

# 主库(写)
master_engine = create_engine("postgresql://user:pass@master:5432/mydb")
# 从库(读)
slave_engines = [
    create_engine("postgresql://user:pass@slave1:5432/mydb"),
    create_engine("postgresql://user:pass@slave2:5432/mydb"),
]

def get_read_session() -> Session:
    """读请求:随机选一个从库"""
    engine = random.choice(slave_engines)
    return Session(engine)

def get_write_session() -> Session:
    """写请求:走主库"""
    return Session(master_engine)

# 使用
async def get_product(product_id: int):
    with get_read_session() as session:          # 读 → 从库
        return session.query(Product).get(product_id)

async def create_order(data: dict):
    with get_write_session() as session:         # 写 → 主库
        order = Order(**data)
        session.add(order)
        session.commit()

主从延迟问题:

写后读不一致场景:

  t0: 用户更新昵称 → 写主库 → 成功
  t1: 用户刷新页面 → 读从库 → 还是旧昵称!
  → 因为主从复制有延迟(通常 10-100ms)

  解决方案:
  ═══════════════════════════════════
  ① 关键读走主库:刚写完的数据,短时间内读主库
  ② 强制延迟读:写完等 200ms 再返回
  ③ 版本号/时间戳:前端带上写入时间,从库数据旧就重查主库
  ④ 缓存兜底:写完同时更新缓存,读缓存而非从库

💡 主从延迟是读写分离最大的坑。最简单的解法:写操作完成后,在 Redis 里标记 user:{id}:just_wrote = 1(TTL 2 秒),2 秒内该用户的读请求走主库。

6.3 连接池调优与慢 SQL 治理

连接池关键参数:

python
# SQLAlchemy 连接池配置
engine = create_engine(
    "postgresql://user:pass@localhost:5432/mydb",
    pool_size=20,           # 常驻连接数
    max_overflow=10,        # 允许额外创建的连接数(峰值)
    pool_timeout=10,        # 获取连接超时(秒)
    pool_recycle=3600,      # 连接回收时间(防止被 DB 断开)
    pool_pre_ping=True,     # 使用前 ping 检查连接是否存活
)
# 总最大连接数 = pool_size + max_overflow = 30
# 超过 30 个并发请求 → 等待 pool_timeout → 超时报错

慢 SQL 排查五步法:

Step 1: 开启慢查询日志
═══════════════════════════════════
-- MySQL
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;   -- 超过 500ms 的算慢查询

Step 2: 找到最慢的 SQL
→ mysqldumpslow 分析慢查询日志
→ 或查看 APM 工具(Prometheus / Datadog)

Step 3: EXPLAIN 分析执行计划
═══════════════════════════════════
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
→ type = ALL → 全表扫描 ❌
→ type = ref → 用了索引 ✅
→ rows = 1000000 → 扫描了 100 万行 ❌

Step 4: 加索引
═══════════════════════════════════
CREATE INDEX idx_orders_user_id ON orders(user_id);
→ 再 EXPLAIN → type = ref, rows = 50 ✅

Step 5: 持续监控
→ 新代码上线后关注慢查询数量变化

SQL 优化速查表:

问题原因解决方案
全表扫描WHERE 字段没索引加索引
索引失效LIKE '%xxx' / 函数包裹字段避免前缀通配符、避免函数
查询太多字段SELECT *只查需要的字段
大分页OFFSET 100000改用游标分页(WHERE id > last_id)
N+1 查询ORM 懒加载joinedload / selectinload
锁等待长事务持有锁减小事务范围、拆分大事务

💡 一条经验:90% 的慢查询都是因为缺索引。上线前用 EXPLAIN 检查每个核心 SQL 的执行计划,确保 type 不是 ALL(全表扫描)。

第 6 章核心知识回顾:

概念一句话解释
四大瓶颈连接数、锁竞争、磁盘 IO、CPU,通常连接数先爆
读写分离写走主库,读走从库,读 QPS 按从库数量线性扩展
主从延迟写后立刻读可能读到旧数据,用 Redis 标记 + 短时走主库
连接池pool_size + max_overflow = 最大并发数,不能设太大
慢 SQLEXPLAIN 看执行计划,90% 的问题是缺索引

7. 连接池与资源管理:别让"池子"成为瓶颈

第 6 章提到了数据库连接池配置。这章把连接池讲透——不只是数据库,HTTP 调用、Redis 访问都有连接池的问题。连接池用不好,就是高并发的隐形杀手。

7.1 连接池的本质:为什么不能每次都新建连接

一次数据库连接的成本:

  创建连接:
  ═══════════════════════════════════
  ① TCP 三次握手            → 1-3ms
  ② 数据库认证(用户名/密码)→ 1-2ms
  ③ 分配内存/创建线程        → 1-2ms
  ④ 初始化会话参数           → 0.5ms
  ──────────────────────────────────
  总计:3-8ms(每次请求都要付出的"建连成本")

  如果你的 SQL 查询只要 2ms
  → 建连 5ms + 查询 2ms = 7ms(70% 时间在建连!)
  → 连接池:建连一次,复用 N 次 → 查询 2ms ✅

  不用连接池              用连接池
  ══════════              ═════════
  每次请求:建连+查询+断连   请求:从池中借+查询+归还
  5+2+1 = 8ms            0+2+0 = 2ms
  开销 4 倍!              ✅ 只有查询时间

三种连接池的 Python 配置:

python
# === 1. 数据库连接池(SQLAlchemy)===
from sqlalchemy import create_engine
db_engine = create_engine(
    "postgresql://user:pass@localhost:5432/mydb",
    pool_size=20, max_overflow=10
)

# === 2. Redis 连接池 ===
import redis
redis_pool = redis.ConnectionPool(
    host='localhost', port=6379,
    max_connections=50,        # 最大连接数
    decode_responses=True
)
r = redis.Redis(connection_pool=redis_pool)

# === 3. HTTP 连接池(httpx)===
import httpx
http_client = httpx.AsyncClient(
    limits=httpx.Limits(
        max_connections=100,       # 总连接数
        max_keepalive_connections=20  # Keep-Alive 连接数
    ),
    timeout=10.0
)
# ⚠️ 不要每次请求都 httpx.AsyncClient(),全局复用一个!

💡 连接池是"借还"模式——从池中借一个连接,用完归还。关键是:借了必须还!不还就是"连接泄漏"(7.3 节)。

7.2 池大小怎么算:不是越大越好

经典公式(来自 HikariCP 作者):

  pool_size = CPU 核数 × 2 + 磁盘数

  例子:
  → 4 核 CPU + 1 块 SSD
  → pool_size = 4 × 2 + 1 = 9
  → 没错,就这么少!

  为什么不能设太大?
  ═══════════════════════════════════
  pool_size = 100 时:
  → 100 个连接同时执行 SQL
  → CPU 只有 4 核 → 频繁上下文切换
  → 每个查询都变慢(从 2ms 变成 20ms)
  → 总 QPS 反而下降!

  pool_size = 10 时:
  → 10 个连接排队执行
  → CPU 切换少,每个查询稳定 2ms
  → 总 QPS = 10 / 0.002 = 5000 ✅
池太小 vs 池太大:

  池太小(pool_size = 3):
  → 第 4 个请求等不到连接 → 超时报错
  → 系统能力没有充分利用
  
  池太大(pool_size = 200):
  → 200 个查询同时跑 → CPU 上下文切换风暴
  → 每个查询变慢 → 连接占用时间变长 → 需要更多连接
  → 恶性循环!
  
  刚好(pool_size = CPU×2+磁盘):
  → CPU 被充分利用,不会过载
  → 查询速度最快 → 连接很快归还 → 小池高吞吐

不同场景推荐配置:

场景CPU 核数推荐 pool_sizemax_overflow
开发环境255
小型应用41010
中型应用82010
大型应用163020

💡 违反直觉但正确:连接池越小,吞吐量可能越高。因为少量连接=少量并发=CPU 不切换=查询快=连接快速归还=更多请求被处理。HikariCP 官方建议初始值用公式算,然后压测微调。

7.3 连接泄漏:最隐蔽的高并发杀手

连接泄漏 = 借了连接没还

  表现:
  ═══════════════════════════════════
  → 系统刚启动时一切正常
  → 运行几小时/几天后开始报 "连接超时"
  → 重启后恢复,过一段时间又出问题
  → 连接池监控显示:活跃连接数只增不减
  
  原因:
  ═══════════════════════════════════
  → 代码异常时没有释放连接(最常见)
  → 忘记关闭 Session/Connection
  → 在函数里创建连接但没在 finally 中释放

典型泄漏代码 vs 正确写法:

python
# ❌ 错误写法:异常时连接不会归还
def get_user_bad(user_id: int):
    session = Session()
    user = session.query(User).get(user_id)  # 如果这里抛异常
    session.close()     # ← 这行不会执行!连接泄漏!
    return user

# ✅ 正确写法 1:try/finally
def get_user_v1(user_id: int):
    session = Session()
    try:
        return session.query(User).get(user_id)
    finally:
        session.close()  # ← 无论成功失败都会执行

# ✅ 正确写法 2:上下文管理器(推荐)
def get_user_v2(user_id: int):
    with Session() as session:   # ← 自动关闭
        return session.query(User).get(user_id)

# ✅ 正确写法 3:FastAPI 依赖注入
def get_db():
    db = Session()
    try:
        yield db
    finally:
        db.close()

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: Session = Depends(get_db)):
    return db.query(User).get(user_id)
    # 请求结束自动执行 finally → 连接归还

排查连接泄漏:

bash
# 查看数据库当前连接数
# PostgreSQL
SELECT count(*) FROM pg_stat_activity;

# MySQL  
SHOW STATUS LIKE 'Threads_connected';

# 如果连接数持续增长且不释放 → 有泄漏
# SQLAlchemy 开启连接池日志
import logging
logging.getLogger('sqlalchemy.pool').setLevel(logging.DEBUG)

💡 防泄漏铁律:永远用 withtry/finally 管理连接。永远不要 session = Session() 后直接用。在 FastAPI 中用 Depends(get_db) 是最安全的模式。

第 7 章核心知识回顾:

概念一句话解释
建连成本3-8ms/次,连接池复用后几乎为 0
池大小公式CPU核数 × 2 + 磁盘数,不要拍脑袋设 100
池太大的代价CPU 上下文切换 → 查询变慢 → 恶性循环
连接泄漏借了不还,表现为连接数只增不减
防泄漏永远用 with / try-finally / Depends

8. 热点数据处理:当所有请求都打到同一个 Key

前面讲的缓存策略假设流量均匀分散在很多 Key 上。但现实中经常有"热点"——某个商品搞秒杀、某条微博突然爆了——所有请求集中到同一个 Key,把 Redis 单节点打爆。

8.1 热点 Key 是怎么产生的

热点 Key 的典型场景:

  ① 秒杀商品
  ═══════════════════════════════════
  → product:12345 → 10 万人同时访问
  → 所有请求打到 Redis 同一个 Key
  
  ② 明星出轨/热搜事件
  ═══════════════════════════════════
  → weibo:hot:1 → 千万人同时刷
  → 流量在几分钟内从 0 飙到百万
  
  ③ 首页大促 Banner
  ═══════════════════════════════════
  → homepage:banner → 每个用户打开 App 都请求
  → 所有 DAU 的流量集中在一个 Key
  
  ④ 计数器/排行榜
  ═══════════════════════════════════
  → like_count:post_999 → 爆款文章疯狂点赞
热点 Key 的危害:

  Redis 集群模式下(Cluster / 分片):
  ═══════════════════════════════════
  → 一个 Key 只存在于一个分片上
  → 10 万 QPS 全打到分片 3 → 分片 3 过载
  → 其他分片空闲 → 集群扩容解决不了热点问题
  
  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
  │分片 1│  │分片 2│  │分片 3│  │分片 4│
  │ 空闲 │  │ 空闲 │  │ 💀  │  │ 空闲 │
  └──────┘  └──────┘  └──────┘  └──────┘

               10 万 QPS 全在这里

热点 Key 识别方法:

bash
# Redis 4.0+ 热点 Key 分析
redis-cli --hotkeys

# 监控命令(实时显示高频命令)
redis-cli monitor | head -1000

# 业务层:记录 Key 访问频率
# 在应用层统计,访问超过阈值的 Key 标记为热点

💡 热点 Key 不能靠加 Redis 节点解决——因为一个 Key 只在一个节点上。解法是"把一个 Key 变成多个 Key"(打散)或"在 Redis 前面再加一层本地缓存"(多级缓存)。

8.2 多级缓存:本地缓存 + Redis 的组合拳

多级缓存架构:

  请求 ──▶ L1 本地缓存(进程内存)

            ╳ 未命中


          L2 Redis 缓存

            ╳ 未命中


          L3 数据库

  各层特点:
  ═══════════════════════════════════
  L1 本地缓存:延迟 < 0.01ms,容量小(几百 MB)
  → 命中的话,请求根本不出进程
  → 多实例之间数据不同步(最终一致)
  
  L2 Redis:延迟 0.1-0.5ms,容量大(几十 GB)
  → 所有实例共享,数据一致
  → 热点 Key 10 万 QPS 可能打爆单节点
  
  L3 数据库:延迟 1-100ms,容量最大
  → 最终数据源,保证持久化

Python 本地缓存实现:

python
from cachetools import TTLCache
import redis
import json

# L1:本地缓存(进程内存,最多 1000 个 Key,TTL 10 秒)
local_cache = TTLCache(maxsize=1000, ttl=10)

# L2:Redis
r = redis.Redis()

async def get_product_multilevel(product_id: int):
    """多级缓存读取"""
    cache_key = f"product:{product_id}"
    
    # L1:查本地缓存
    if cache_key in local_cache:
        return local_cache[cache_key]     # 命中!不经网络
    
    # L2:查 Redis
    cached = r.get(cache_key)
    if cached:
        data = json.loads(cached)
        local_cache[cache_key] = data     # 回填 L1
        return data
    
    # L3:查数据库
    data = await db.fetch_product(product_id)
    if data:
        r.setex(cache_key, 3600, json.dumps(data))  # 回填 L2
        local_cache[cache_key] = data     # 回填 L1
    return data

热点 Key 打散策略:

把 1 个热点 Key 变成 N 个 Key:

  原始:product:12345 → 10 万 QPS 打到 1 个分片
  
  打散:product:12345:0
       product:12345:1
       product:12345:2  → 分散到不同分片
       ...
       product:12345:9
  
  写入时:10 个 Key 都写入同样的数据
  读取时:随机选一个 Key 读取
  → 10 万 QPS 分散到 10 个分片 = 每个分片 1 万 QPS ✅
python
import random

def get_sharded_key(base_key: str, shard_count: int = 10) -> str:
    """生成打散后的 Key"""
    shard = random.randint(0, shard_count - 1)
    return f"{base_key}:{shard}"

# 读取:随机选一个分片
key = get_sharded_key("product:12345")
data = r.get(key)

# 写入/更新:所有分片都更新
for i in range(10):
    r.setex(f"product:12345:{i}", 3600, json.dumps(data))

8.3 秒杀库存扣减:Redis + Lua 原子操作

秒杀超卖的根本原因——非原子操作:

  ❌ 错误流程(先查后扣,两步操作):
  ═══════════════════════════════════
  线程 A: GET stock → 返回 1      (还有库存)
  线程 B: GET stock → 返回 1      (也看到有库存)
  线程 A: SET stock 0             (扣减成功)
  线程 B: SET stock -1            (超卖了!💀)

  ✅ 正确流程(Lua 脚本原子操作):
  ═══════════════════════════════════
  线程 A: Lua(检查+扣减) → 成功,stock = 0
  线程 B: Lua(检查+扣减) → 失败,库存不足
  → Lua 在 Redis 中原子执行,中间不会被打断

Redis + Lua 原子扣减库存:

python
DEDUCT_STOCK_SCRIPT = """
local stock_key = KEYS[1]
local quantity = tonumber(ARGV[1])

-- 获取当前库存
local stock = tonumber(redis.call('GET', stock_key))

if stock == nil then
    return -1   -- Key 不存在
end

if stock < quantity then
    return -2   -- 库存不足
end

-- 扣减库存(原子操作)
local new_stock = redis.call('DECRBY', stock_key, quantity)
return new_stock   -- 返回剩余库存
"""

deduct_stock = r.register_script(DEDUCT_STOCK_SCRIPT)

async def seckill(user_id: int, product_id: int):
    """完整的秒杀流程"""
    stock_key = f"stock:{product_id}"
    
    # Step 1: Lua 原子扣减库存
    result = deduct_stock(keys=[stock_key], args=[1])
    
    if result == -1:
        return {"success": False, "msg": "商品不存在"}
    if result == -2:
        return {"success": False, "msg": "已售罄"}
    
    # Step 2: 库存扣减成功,发送到队列异步创建订单
    r.lpush("order_queue", json.dumps({
        "user_id": user_id,
        "product_id": product_id,
        "remaining_stock": result
    }))
    
    return {"success": True, "msg": f"抢购成功!剩余 {result} 件"}
秒杀完整防护体系(串联前面所有章节):

  用户点击"抢购"


  ① 限流(第 3 章):令牌桶限制 1000/s


  ② 本地缓存(8.2):检查是否已售罄(避免无效请求到 Redis)


  ③ Lua 原子扣减(8.3):Redis 原子操作,防超卖


  ④ 队列(第 5 章):扣减成功后放入队列


  ⑤ 消费者创建订单(写数据库)


  ⑥ 通知用户结果(WebSocket / 轮询)

💡 秒杀系统的核心不是"快",是"准"。宁可慢一点处理,也不能超卖。Redis Lua 保证了扣减的原子性,队列保证了下游不被打爆,限流保证了整体不崩溃。

第 8 章核心知识回顾:

概念一句话解释
热点 Key所有请求集中到一个 Key,集群扩容也解决不了
多级缓存L1 本地(无网络)→ L2 Redis → L3 DB
Key 打散1 个 Key → N 个 Key,分散到不同分片
Lua 原子操作检查 + 扣减在 Redis 内一步完成,防超卖
秒杀体系限流 → 本地缓存 → Lua 扣减 → 队列 → 创建订单

9. 压测与容量规划:上线前的最后一关

前面 8 章讲了各种高并发手段,但最终有没有效果,必须用数据说话。压测是上线前验证系统能力的唯一方式——不压测就上线,等于闭着眼过马路。

9.1 压测工具选型:wrk、Locust、k6

工具语言学习成本适用场景特点
wrkC单接口极限测试极快、资源占用少、不适合复杂场景
LocustPython业务场景模拟✅ Python 写用例、Web UI、分布式
k6Go/JSCI/CD 集成JS 写脚本、支持阈值检查、可嵌入流水线
JMeterJava企业级全链路功能全但笨重、GUI 操作

wrk 快速压测(30 秒搞定):

bash
# 安装
brew install wrk

# 基础压测:8 线程、200 连接、持续 30 秒
wrk -t8 -c200 -d30s http://localhost:8000/api/products

# 输出示例:
# Requests/sec:  3521.47      ← QPS
# Latency   Avg:  56.78ms     ← 平均延迟
#           99%: 234.12ms     ← P99 延迟
# Transfer/sec:   1.23MB      ← 吞吐量

Locust 业务场景压测(推荐):

python
# locustfile.py
from locust import HttpUser, task, between

class WebUser(HttpUser):
    wait_time = between(1, 3)  # 用户思考时间 1-3 秒
    
    @task(10)  # 权重 10:高频操作
    def browse_products(self):
        self.client.get("/api/products")
    
    @task(5)   # 权重 5:中频操作
    def view_product(self):
        self.client.get("/api/products/1")
    
    @task(1)   # 权重 1:低频操作
    def create_order(self):
        self.client.post("/api/orders", json={
            "product_id": 1,
            "quantity": 1
        })
bash
# 启动 Locust(Web UI 模式)
locust -f locustfile.py --host http://localhost:8000

# 打开 http://localhost:8089 设置并发用户数和启动速率
# 实时看 QPS、RT、错误率曲线

💡 推荐 Locust:Python 写用例(后端同学无学习成本)、支持混合场景模拟(浏览:详情:下单 = 10:5:1)、Web UI 实时看图表、支持分布式多机压测。

9.2 四步压测法:从单接口到全链路

四步压测法:

  Step 1: 基准测试
  ═══════════════════════════════════
  目的:测出系统"裸机"能力
  方法:最简单的接口(如 /health),不经过业务逻辑
  关注:框架本身的 QPS 上限、网络/系统瓶颈
  → 这是你系统的理论天花板
  
  Step 2: 单接口压测
  ═══════════════════════════════════
  目的:找到每个核心接口的 QPS 和 P99
  方法:逐个压核心接口(商品列表、详情、下单)
  关注:各接口的 QPS、P99 RT、错误率
  → 找到最弱的接口(木桶的短板)
  
  Step 3: 混合场景压测
  ═══════════════════════════════════
  目的:模拟真实用户行为
  方法:按比例混合接口(浏览 60%、详情 30%、下单 10%)
  关注:整体 QPS、各接口是否互相影响
  → 接近真实场景,可能发现新瓶颈
  
  Step 4: 全链路压测
  ═══════════════════════════════════
  目的:验证系统在峰值下的整体表现
  方法:包含缓存、队列、数据库、外部服务
  关注:端到端延迟、各组件水位、报警触发
  → 最接近生产的测试

压测时关注的核心指标:

指标关注点告警阈值(参考)
QPS是否达到预期< 目标 QPS 的 80%
P99 RT长尾延迟> 500ms
错误率是否有请求失败> 0.1%
CPU是否过载> 80%
内存是否泄漏持续增长不回落
DB 连接数是否打满> pool_size × 80%
Redis QPS是否达到瓶颈> 单节点 8 万
压测常见坑:

  ❌ 在本机压本机 → 压测工具和服务抢 CPU
  ❌ 数据库是空表 → 空表查询比百万行快 10 倍
  ❌ 没有预热缓存 → 冷启动 QPS 远低于正常值
  ❌ 只看平均 RT → P99 可能是平均值的 10 倍
  ❌ 压测时间太短 → 来不及暴露内存泄漏/连接泄漏
  
  ✅ 正确做法:
  → 压测客户端和服务部署在不同机器
  → 用生产级数据量(至少百万级)
  → 先预热 5 分钟再开始计数
  → 至少压 10 分钟以上

9.3 容量规划:算清楚需要多少机器

容量规划公式:

  所需机器数 = 目标 QPS / 单机 QPS × 冗余系数

  冗余系数:
  ═══════════════════════════════════
  → 通常取 1.5 - 2.0
  → 保证单台挂了,剩余机器能抗住
  → 保证突发流量有余量

  例子:
  → 目标 QPS = 10000
  → 单机 QPS(压测得出)= 2000
  → 冗余系数 = 1.5
  → 所需机器 = 10000 / 2000 × 1.5 = 7.5 → 8 台

完整容量规划实例(电商大促):

已知条件:
  → 大促预估 DAU = 100 万
  → 峰值系数 = 日均流量的 3 倍(集中在晚 8-10 点)
  → 平均每用户每分钟 2 次请求

  Step 1: 算日均 QPS
  ═══════════════════════════════════
  日总请求 = 100 万 × 20 次/用户 = 2000 万
  日均 QPS = 2000 万 / 86400 ≈ 230

  Step 2: 算峰值 QPS
  ═══════════════════════════════════
  峰值 QPS = 日均 QPS × 峰值系数 = 230 × 3 ≈ 700
  → 这是正常峰值

  Step 3: 大促峰值(额外 ×3-5 倍)
  ═══════════════════════════════════
  大促峰值 QPS = 700 × 5 = 3500

  Step 4: 算机器数
  ═══════════════════════════════════
  单机 QPS(压测)= 1000
  机器数 = 3500 / 1000 × 1.5 = 5.25 → 6 台 Web 服务器
  
  Step 5: 别忘了数据库和 Redis
  ═══════════════════════════════════
  → 缓存命中率 90% → 350 QPS 到 DB → 1 台 MySQL 够
  → Redis 单机 10 万+ QPS → 1 台 Redis 够
  → 但都要配主从/哨兵做高可用
组件规划方式关键数字
Web 服务目标 QPS / 单机 QPS × 1.5压测得出单机能力
数据库到达 DB 的 QPS / 单机安全水位考虑缓存命中率
Redis热点 Key QPS / 单节点能力单节点约 10 万
消息队列峰值写入 TPS / 单节点能力考虑消费速率

💡 容量规划不是一次性的。每次大版本发布后都要重新压测,因为新功能可能改变系统性能特征。最好把压测集成到 CI/CD 里,每次发版自动跑性能回归。

第 9 章核心知识回顾:

概念一句话解释
LocustPython 写压测用例,Web UI 实时看结果,推荐首选
四步压测基准 → 单接口 → 混合场景 → 全链路,逐步逼近真实
P99 > 平均值关注第 99 百分位延迟,不要被平均值骗了
容量公式机器数 = 目标 QPS / 单机 QPS × 冗余系数(1.5-2)
预热压测前先跑 5 分钟,让缓存和连接池热起来

10. 实战案例:从 0 到 10 万 QPS 的演进路径

前面 9 章是"工具箱",这一章是"装修实战"——用一个电商系统的成长过程,把所有工具串联起来。每个阶段遇到什么瓶颈、用什么方案、架构怎么演进,一步步看。

10.1 阶段一(100 QPS):单机能跑就行

初始架构——最简单的单体应用:

  用户 ──▶ Nginx ──▶ FastAPI ──▶ PostgreSQL

                      └──▶ 本地文件存储

  技术栈:
  → FastAPI + SQLAlchemy + PostgreSQL
  → 单台 4 核 8GB 云服务器
  → Nginx 做反向代理 + 静态文件
  → 日活 1000,QPS ≈ 50-100
python
# 阶段一的代码——直接查数据库,简单粗暴
@app.get("/api/products/{product_id}")
async def get_product(product_id: int, db: Session = Depends(get_db)):
    product = db.query(Product).get(product_id)
    if not product:
        raise HTTPException(404, "商品不存在")
    return product

@app.post("/api/orders")
async def create_order(data: OrderCreate, db: Session = Depends(get_db)):
    product = db.query(Product).get(data.product_id)
    if product.stock < data.quantity:
        raise HTTPException(400, "库存不足")
    product.stock -= data.quantity
    order = Order(**data.dict())
    db.add(order)
    db.commit()
    # 同步发短信(阻塞 200ms)
    send_sms(order.user_phone, f"订单创建成功: {order.id}")
    return order
阶段一的问题和感受:

  ✅ 能跑就行,开发速度快
  ✅ 架构简单,好理解好维护
  
  ⚠️ 随着用户增长出现的第一批问题:
  → 商品详情页有时候 500ms+ 才返回(慢 SQL)
  → 订单创建因为发短信要等 200ms
  → 某次推广活动 QPS 到 500 → 数据库连接打满
  → 结论:该加缓存了!→ 进入阶段二

10.2 阶段二(1000-10000 QPS):缓存 + 限流 + 队列

阶段二架构——加了 Redis 和消息队列:

  用户 ──▶ Nginx(限流)──▶ FastAPI ──▶ Redis(缓存)
                              │              │
                              │              ╳ 未命中
                              │              ↓
                              │         PostgreSQL

                              └──▶ Redis Queue ──▶ 消费者(发短信/统计)

  新增组件:
  → Redis:缓存热点数据 + 分布式限流
  → 消息队列:异步处理非核心操作
  → 服务器扩到 2 台(Nginx 负载均衡)

改造后的代码——对比阶段一:

python
# 阶段二:加了缓存 + 异步
@app.get("/api/products/{product_id}")
async def get_product(product_id: int):
    # 改造 1:先查缓存(第 2 章)
    cache_key = f"product:{product_id}"
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)       # 命中率 90% → DB 压力降 10 倍
    
    product = await db.fetch_product(product_id)
    if product:
        r.setex(cache_key, 3600, json.dumps(product))
    return product

@app.post("/api/orders")
async def create_order(data: OrderCreate):
    # 改造 2:限流(第 3 章)
    if not is_allowed(data.user_id, limit=10):
        raise HTTPException(429, "操作过于频繁")
    
    # 核心操作仍然同步
    order = await process_order(data)
    
    # 改造 3:异步化(第 5 章)
    r.lpush("notification_queue", json.dumps({
        "type": "sms",
        "phone": order.user_phone,
        "content": f"订单创建成功: {order.id}"
    }))
    # 不再等 200ms 发短信
    return order
阶段二的改造效果:

  改造前(阶段一)          改造后(阶段二)
  ════════════════          ════════════════
  QPS 上限:500             QPS 上限:5000
  商品详情 RT:50-500ms     商品详情 RT:5ms(缓存命中)
  下单 RT:300ms            下单 RT:100ms(异步发短信)
  DB QPS:500               DB QPS:50(90% 被缓存挡住)

  ⚠️ 新的瓶颈出现:
  → 大促时 QPS 到 8000 → 单台 DB 开始吃力
  → 某个商品上了热搜 → 热点 Key 问题
  → 2 台 Web 服务器 CPU 到 90%
  → 结论:需要读写分离 + 水平扩展 → 进入阶段三

10.3 阶段三(10 万 QPS):分布式架构全家桶

阶段三架构——全面分布式:

  用户 ──▶ CDN(静态资源)
       ──▶ API 网关(限流 + 路由)


  ┌───────────────────────────────┐
  │  Web 服务集群(6 台,HPA 弹性) │
  └───────┬──────────┬────────────┘
          │          │
          ▼          ▼
   L1 本地缓存    Redis Cluster(3 主 3 从)

                     ╳ 未命中

              PostgreSQL(1 主 2 从,读写分离)
              
  异步链路:
  Web ──▶ RabbitMQ ──▶ 消费者集群

          └──▶ 通知服务 / 统计服务 / 搜索索引

阶段三使用的所有技术手段:

技术解决的问题对应章节
Redis 缓存数据库读压力第 2 章
令牌桶 + Redis Lua 限流入口流量保护第 3 章
熔断器 + 降级下游故障隔离第 4 章
RabbitMQ 消息队列写操作削峰 + 异步化第 5 章
读写分离(1 主 2 从)数据库读写压力分离第 6 章
连接池调优资源利用率第 7 章
多级缓存 + Key 打散热点数据第 8 章
Locust 压测 + 容量规划上线前验证第 9 章
三个阶段的演进路径总览:

  阶段一(100 QPS)              阶段二(5000 QPS)
  ═══════════════                ═══════════════
  单机 + 单 DB                   + Redis 缓存
  直接查库                       + 消息队列
  同步处理                       + 限流
  ──────────────────────────────────────────────
  
  阶段三(10 万 QPS)
  ═══════════════
  + 读写分离
  + Redis Cluster
  + 多级缓存
  + 熔断降级
  + 弹性扩缩容
  + 全链路压测

  核心演进逻辑(每次只解决当前最大瓶颈):
  ─────────────────────────────────────────
  数据库慢了 → 加缓存
  入口流量大 → 加限流
  写操作慢了 → 加队列
  读压力大了 → 读写分离
  单点热点了 → 多级缓存
  服务不稳了 → 熔断降级

💡 架构演进的核心原则:不要过度设计。100 QPS 的系统不需要分布式架构——单机能搞定的事不要用集群。每次只解决当前最大的瓶颈,用最小的成本解决问题。


全书总结

恭喜你读完了!回顾一下从"扛不住"到"稳如磐石"的完整路径:

你的高并发知识体系:

  第 1 章  核心指标 ─── QPS/TPS/RT + 瓶颈定位
  第 2 章  缓存 ────── Redis 策略 + 穿透/雪崩/击穿
  第 3 章  限流 ────── 四种算法 + Redis Lua 分布式限流
  第 4 章  熔断降级 ── 三态熔断器 + P0-P3 降级分级
  第 5 章  队列削峰 ── 同步→异步 + 秒杀实战
  第 6 章  数据库 ──── 读写分离 + 慢 SQL 治理
  第 7 章  连接池 ──── 池大小公式 + 连接泄漏防护
  第 8 章  热点数据 ── 多级缓存 + Lua 原子扣减
  第 9 章  压测 ────── Locust + 容量规划公式
  第 10 章 演进实战 ── 100→5000→10 万 QPS 三阶段

高并发的三条核心原则——记住这三条就够了:

  1. :把流量挡在数据库前面(缓存 + 限流)
  2. :把大任务拆成小任务(异步 + 队列 + 读写分离)
  3. :出了问题有备选方案(熔断 + 降级 + 回滚)

剩下的,就是在实践中反复踩坑、反复优化,直到这些方案变成你的条件反射。祝你的系统稳如磐石!🚀

坚持是一种品格