高并发系统设计
高并发系统设计
一套完整的高并发防护体系:限流 → 熔断 → 降级 → 异步化 → 多级缓存,层层兜底
高并发系统设计是大厂面试的必考主题。光背概念远远不够,面试官想听到的是你在什么场景选了什么方案,为什么不选另一种。本文把五大核心策略的「选型逻辑」和「工程细节」一次性讲透。
一、限流——流量的第一道闸门
四种限流算法
计数器(Fixed Window):最简单,按时间窗口计数。致命缺陷是「临界问题」——在窗口切换的瞬间,实际流量可以达到限制的 2 倍。
滑动窗口(Sliding Window):解决了计数器的临界问题,窗口随时间平滑移动。Redis ZSet 实现:member 存请求 UUID,score 存时间戳,查询时 ZREMRANGEBYSCORE 清除过期记录再 ZCARD 统计当前数量,全程 Lua 脚本保证原子性。
漏桶(Leaky Bucket):请求进入固定容量的桶,以恒定速率处理输出。桶满则丢弃。特点是无论多大突发流量,输出永远匀速,适合调用有速率限制的第三方 API(如 OpenAI、支付接口)。
令牌桶(Token Bucket):系统以固定速率往桶中投放令牌,请求到来时消耗令牌。允许突发——桶中积累的令牌可以被瞬间消费。Guava RateLimiter 和 Spring Cloud Gateway 的 RequestRateLimiter 都是令牌桶实现。大多数业务场景推荐令牌桶。
| 对比维度 | 漏桶 | 令牌桶 |
|---|---|---|
| 突发流量 | 不允许,严格匀速 | 允许,可积累令牌 |
| 典型实现 | Nginx limit_req | Guava RateLimiter |
| 推荐场景 | 调用限速第三方 API | 大多数业务限流 |
多维度限流设计
生产中不能只做一层限流,需要三个维度叠加:
接口级限流(全局):/api/chat 整体不超过 5000 QPS
用户级限流(Redis 计数):每个用户每分钟不超过 60 次
IP 级限流(防爬虫):每个 IP 每秒不超过 20 次面试追问:如何用 Redis 实现分布式限流?
用 Redis + Lua 脚本实现滑动窗口。Lua 脚本是 Redis 内部原子执行的,不会有并发竞争问题。步骤:ZREMRANGEBYSCORE 删过期记录 → ZCARD 获取数量 → 数量 < 阈值则 ZADD 添加并允许,否则拒绝。
二、熔断——防止级联雪崩
三状态机模型
熔断器是「自动开关」,核心是三个状态的转换:
Closed(正常)→ 失败率超阈值 → Open(熔断,快速失败)
Open → 等待 timeWindow 秒 → Half-Open(半开,放入少量探测请求)
Half-Open → 探测成功 → Closed(恢复正常)
Half-Open → 探测失败 → Open(重新熔断)为什么需要半开状态? 没有半开,熔断后要么永远不恢复,要么定时全量恢复(可能直接再次压垮)。半开用少量探测请求(如 3 个)试探服务是否恢复,成功才完全打开,是「渐进式恢复」。
框架选型
| 框架 | 推荐程度 | 说明 |
|---|---|---|
| Hystrix | 不推荐 | 已停止维护 |
| Resilience4j | 推荐(Spring Boot 官方) | 轻量、注解驱动 |
| Sentinel | 推荐(国内主流) | 功能完整,限流+熔断一体 |
Sentinel 的熔断维度更丰富:支持异常比例、慢调用比例、异常数三种模式触发熔断,而 Resilience4j 支持异常比例和慢调用。国内微服务项目优先选 Sentinel。
三、降级——服务的最后防线
降级是「主服务挂了,给用户一个还能接受的替代结果」,而不是直接报错。降级要有层次:
① 主服务调用失败
↓
② 返回 Redis 缓存的旧数据
↓ 缓存未命中
③ 调用备用服务(如降级到小模型)
↓ 备用服务也不可用
④ 返回预置的兜底数据(静态默认值)
↓ 无兜底数据
⑤ 快速失败,返回友好错误提示面试高频:降级和熔断的区别?
熔断是触发机制(检测到下游故障达到阈值,主动断开调用链路,快速失败,保护自己不被下游拖垮)。降级是处理策略(服务不可用时,返回可接受的替代结果,保证核心功能可用)。关系:熔断是触发降级的一种原因,熔断后执行降级逻辑。
降级注意事项:降级数据必须明确标注来源(如「[缓存数据,可能不是最新]」),不能以假充真欺骗用户。
四、异步化——削峰填谷的核心手段
同步调用是高并发的天然瓶颈——请求线程被阻塞,吞吐量受限于线程池大小。异步化的核心思路是:将耗时操作从主链路移出。
消息队列削峰
秒杀、下单场景最典型:瞬时 10 万 QPS 进来,后端数据库只能承受 2000 QPS。直接打会宕机。引入 MQ 后:
用户请求 → 网关限流 → 写入 Kafka/RocketMQ(极快)→ 立即返回「处理中」
↓
消费者按后端能力匀速消费(2000 QPS)用户感受到的是「异步确认」体验(类似淘宝下单后跳转「订单确认中」页面),而不是等待超时。
异步化适用场景
- 写多读少的统计:点击量、浏览数、点赞数,先写 Kafka,再异步聚合入库
- 邮件/短信通知:发送成功后异步推送,不影响主流程
- 秒杀/大促下单:MQ 作为「缓冲池」保护数据库
- 日志写入:日志先写本地磁盘 Buffer,异步 Flush
五、多级缓存——读流量的终极优化
三级缓存架构
客户端请求
↓
L0:CDN 静态缓存(< 1ms,命中则不到服务器)
↓ 未命中
L1:Caffeine 本地缓存(< 1ms,进程内,无网络开销)
↓ 未命中
L2:Redis 分布式缓存(1-5ms,全集群共享)
↓ 未命中
L3:数据库(10-100ms)三大缓存问题
缓存穿透:查询不存在的 Key,每次都打到数据库。
- 解决:布隆过滤器(启动时将合法 ID 加入,请求前拦截非法 ID)或缓存空值(短 TTL,30 秒)
缓存击穿:热点 Key 过期瞬间,大量并发请求同时穿透到数据库。
- 解决:互斥锁(Redis SETNX,只允许一个线程查库回写,其他等待)或热点 Key 永不过期 + 异步刷新
缓存雪崩:大批量 Key 同时过期,或 Redis 集群宕机。
- 解决:TTL 随机化(基础 TTL ± 随机偏移,错开过期时间)+ Redis 集群 + Caffeine 本地缓存降级兜底
缓存一致性最佳实践
写操作推荐「先更新 DB,再删缓存(而非更新缓存)」:
为什么删缓存而不是更新缓存?
并发写场景:A 更新 DB=100,B 更新 DB=200,B 更新缓存=200,A 更新缓存=100
→ 最终 DB=200 但缓存=100,产生脏数据。
删除缓存则无此问题:下次读请求从 DB 读取最新值并回写,最终一致。删缓存失败怎么办?结合 @TransactionalEventListener(phase=AFTER_COMMIT) 确保事务提交后删缓存,失败则通过消息队列异步补偿重试。
六、面试题精选
Q:系统某接口突然 QPS 暴增 10 倍,你会怎么处理?
分三层处理:① 网关/Nginx 限流,超出阈值直接 429;② Sentinel 熔断保护下游数据库,防止雪崩;③ 触发降级,返回缓存数据或简化版响应,保证核心功能可用。同时排查流量暴增原因(爬虫/营销活动/DDoS)针对性处理。
Q:令牌桶和漏桶的本质区别?
漏桶:无论多大流量进来,出口永远匀速,无法利用系统空闲期的富余处理能力,适合保护固定速率的下游。令牌桶:允许突发——系统空闲时桶中积累令牌,突发时可瞬间消费,长期平均速率受控。绝大多数业务场景选令牌桶。
Q:如何设计一个 AI 应用的限流策略?
AI 应用的特殊性在于计费单位是 Token 而非请求数。限流维度应该是 Token 消耗量:请求前按消息长度预估 Token 扣配额,请求完成后用实际 Token 数修正。同时做多维度限流:用户级(防止个人超用)+ 接口级(保护后端服务)+ 全局级(控制总成本)。
知识星球深度内容
完整限流熔断实战代码(Sentinel + Resilience4j 两套方案)、AI Token 限流完整实现、高并发系统设计模拟题,加入「AI 工程师加速社区」知识星球获取 👉 立即加入
