阶段:分布式与高并发篇 | 建议时长:5 天 | 前置:01-分布式理论基础、05-中间件篇(Redis/MQ/分库分表基础)
学习目标
- 用一条演进主线讲清高并发系统是怎么一步步长出来的(单机 → 读写分离 → 缓存 → MQ → 分库分表 → 微服务)⭐;
- 说清多级缓存的层次与取舍(本地 Caffeine + Redis),会处理热点 key ⭐;
- 对比四种限流算法并说出各自的 Java 实现(Guava RateLimiter/Sentinel/Redis+Lua)⭐;
- 区分限流、熔断、降级三者,理解雪崩效应与舱壁模式 ⭐;
- 讲清读写分离的主从延迟对策,以及分库分表的分片键选择与三大代价 ⭐;
- 完整输出一份秒杀系统设计方案(流量漏斗 + 防超卖 + 一致性)⭐⭐——这是后端面试最经典的场景题。
核心知识点
1. 高并发演进路径 ⭐
以一个电商订单系统为例,流量从日均 1 万单涨到峰值 10 万 QPS,每一步都是”瓶颈出现 → 针对性加一层”:
| 阶段 | 出现的瓶颈 | 采取的动作 |
|---|---|---|
| 1. 单机 all-in-one | 应用+DB 挤在一台机器 | 应用与 DB 分离部署 |
| 2. 读写分离 | 读多写少,单库读扛不住 | MySQL 主从 + 读写分离,读请求分摊到从库 |
| 3. 加缓存 | 热点读打穿到 DB | Redis 缓存热点数据,DB 只兜底缓存没有的 |
| 4. MQ 削峰 | 写入洪峰(大促/秒杀) | 写请求先进 MQ,消费端按 DB 能力匀速落库 |
| 5. 分库分表 | 单表过亿、单库写入瓶颈 | 垂直拆业务库,水平按分片键拆表 |
| 6. 微服务化 | 单体应用发布慢、模块互相拖垮 | 按业务域拆服务,注册中心 + 网关 + 链路追踪 |
三条铁律:
- 演进由瓶颈驱动,不要一上来就微服务+分库分表(过度设计);
- 每一步都是为了解决上一层扛不住的那一类流量(读 → 缓存、写 → MQ、数据量 → 分片);
- 面试讲演进时永远带一句”当时的瓶颈是什么”,体现判断力而不是背名词。
2. 缓存体系:多级缓存与热点 key ⭐
完整的读链路:浏览器缓存 → CDN → Nginx 缓存 → 本地缓存(Caffeine)→ Redis → MySQL。离用户越近越快,越远越全。
为什么要两级(本地 + Redis):
| 维度 | 本地缓存 Caffeine | Redis |
|---|---|---|
| 读取速度 | 纳秒~微秒(进程内存) | 毫秒内(多一次网络 IO + 序列化) |
| 容量 | 单机有限 | 集群可扩展 |
| 一致性 | 各实例独立,改动难同步 | 集中式,相对好保证 |
| 定位 | 极热的小量数据(配置、商品 top100) | 主要缓存层,容量主力 |
典型组合:先查 Caffeine,未命中查 Redis 并回填本地;更新时先失效 Redis,本地用短 TTL(秒级)+ 过期自动刷新容忍短暂不一致。
Cache<String, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000) // 控制容量,防内存溢出
.expireAfterWrite(Duration.ofSeconds(10)) // 短 TTL 控制不一致窗口
.build();
Product p = localCache.getIfPresent(id);
if (p == null) { // 一级未命中 → 查二级
p = redisTemplate.opsForValue().get("p:" + id);
if (p != null) localCache.put(id, p);
}
热点 key 处理:单个 key 的 QPS 高到单分片扛不住(明星八卦、爆款商品)。手段:① 检测发现(客户端统计 + Redis --hotkeys/monitor);② 热点 key 进本地缓存扛掉大部分读;③ 打散副本(key_1~key_N 随机读);④ 读写分离多从库。
三种缓存问题一句话回顾(详见《01-Redis》):
- 穿透 = 查不存在的数据打穿到 DB → 空值缓存/布隆过滤器;
- 击穿 = 单个热点 key 失效瞬间 → 互斥锁重建/逻辑过期;
- 雪崩 = 大量 key 同时失效或 Redis 宕机 → TTL 随机打散/多级缓存/限流降级。
3. 限流 ⭐
四种算法对比:
| 算法 | 原理 | 优点 | 缺点 | 典型实现 |
|---|---|---|---|---|
| 固定窗口计数 | 单位时间计数器,超阈值拒绝 | 实现最简单 | 窗口边界突刺:两个窗口交界处可通过 2 倍流量 | Redis INCR+EXPIRE |
| 滑动窗口 | 统计最近 N 秒内的请求数 | 解决边界突刺,更平滑 | 精度依赖格子数,内存略高 | Sentinel(滑动窗口统计) |
| 漏桶 | 请求进桶,恒定速率流出 | 绝对平滑,整形流量 | 无法应对合理突发,排队有延迟 | Nginx limit_req |
| 令牌桶 | 恒定速率往桶里放令牌,拿到令牌才放行 | 允许一定突发(桶里攒了令牌) | 速率与容量需调参 | Guava RateLimiter |
三种实现方式:
- Guava RateLimiter(单机):令牌桶,
tryAcquire()拿不到就快速失败。只对单个实例有效,多实例要乘以实例数折算阈值; - Sentinel(单机 + 集群,最常用于生产):滑动窗口统计,支持 QPS/线程数两种维度、流控效果(快速失败/Warm Up/排队等待)、熔断降级一体,配控制台动态改规则;
- Redis + Lua(集群全局限流):多实例共享一份计数器,Lua 保证”判断+计数+设过期”原子执行:
-- 固定窗口:超过阈值返回 0(拒绝),否则自增并放行
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1]) -- 第一次访问才设置过期
end
if current > tonumber(ARGV[2]) then
return 0
end
return 1
Lua 的必要性:INCR 与 EXPIRE 分两步执行时,进程可能死在中间导致 key 永不过期、限流器永久锁死;Lua 脚本整体原子执行,天然规避。
4. 熔断与降级 ⭐
雪崩效应:服务 A 调 B,B 调 C;C 变慢 → B 的线程被拖住堆积 → B 也慢 → A 线程池耗尽 → 整条链路瘫痪。一个下游故障放大成全站故障,就是雪崩。
三个概念各管一件事:
- 限流:流量太多时只放一部分进来(保自己);
- 熔断:下游故障/超时率超阈值时,断路器跳闸,一段时间内直接快速失败,不再真正调用(断开依赖,防雪崩)。断路器三态:关闭(正常放行)→ 打开(直接拒绝,计时)→ 半开(放少量探测请求,成功则恢复);
- 降级:出问题时返回兜底数据/简化逻辑(丢卒保帅:推荐列表挂了就返回默认榜单,不让页面白屏)。
舱壁模式(Bulkhead):像船舱一样隔离资源——给每个下游依赖分配独立线程池/信号量,一个依赖挂掉只烧掉自己的舱,不殃及其他调用。
三者关系一句话:限流是入口处控量,熔断是调用链上断开故障依赖,降级是失败后的兜底预案;Sentinel 里三者通常配合使用。
5. 数据层扩展:读写分离与分库分表 ⭐
读写分离:主库写,从库读,主从复制(主库写 binlog → 从库 IO 线程拉取 → SQL 线程重放)。
- 主从延迟是核心问题:写完立刻读,从库可能还没同步到。对策:① 关键路径强制走主库(下单后查订单);② 写后短时间内的同用户请求路由到主库;③ 半同步复制降低丢数据风险;
- 注意:读写分离解决读压力,不解决单表数据量与写压力。
分库分表:
- 垂直分库:按业务域拆(订单库/商品库/用户库);垂直分表:把大字段、低频字段拆出去;
- 水平分表/分库:同一张表的数据按分片键打散到多个表/库,解决数据量与写入瓶颈。
分片键选择(最关键的决策):
- 原则一:带分片键的查询占绝大多数(如订单表按
user_id分,“查我的订单”直落单片); - 原则二:数据分布均匀,避免热点(不能按”省份”分);
- 冲突处理:商家侧查询”店铺所有订单”没有 user_id → 冗余一份按 seller_id 分的异构索引表,或走 ES/宽表。
分库分表的三大代价(必考):
- 跨片查询:无分片键的条件要扇出到全部分片再聚合,分页/排序代价极高(配合 ES 或异构索引解决);
- 分布式事务:跨库写无法用本地事务,需要 01 篇的分布式事务方案;
- 扩容与数据迁移:翻倍扩容要迁移数据,常用”翻倍扩容 + 双写迁移 + 灰度校验”;
- 附带问题:全局唯一主键(见《01-分布式理论基础》雪花算法)。
ShardingSphere:Apache 分库分表中间件,两种形态——ShardingSphere-JDBC(客户端 SDK,直连数据库,性能好、侵入应用)与 ShardingSphere-Proxy(独立代理进程,对应用透明、支持异构语言)。核心配置概念:逻辑表/真实节点/分片算法。
6. 削峰填谷
- MQ 异步化:短时间洪峰(0 点签到、秒杀下单)先进队列,消费端按数据库承受能力匀速消费,把”尖峰”削成”平谷”;
- 缓冲与排队:请求进缓冲池/答题/令牌预发放,把随机洪峰变成可控节奏;
- 代价:延迟变高(用户看到”排队中”)、需要处理消息可靠性与消费幂等(详见《04-RocketMQ》与 MQ 三篇)。
7. 秒杀系统设计串讲 ⭐⭐
先估量级:100 万用户抢 1 万件商品,假设 10 分钟内 80% 请求涌入 → 峰值读 QPS 可到 10 万级,写(下单)远小于读。99% 的流量应该死在到达数据库之前——这就是流量漏斗。
流量漏斗(五层,自上而下):
| 层 | 手段 | 挡掉什么 |
|---|---|---|
| 1. 前端 | 按钮置灰防重复点击、答题/验证码拉平请求、秒杀 URL 动态加密防提前刷接口 | 机器人与重复点击,削掉大半流量 |
| 2. 网关 | 令牌桶限流(如 5000 QPS)、黑名单/IP 限流、登录态校验 | 超过系统容量的流量与刷子 |
| 3. 服务层 | 风控(同用户/设备限购)、Redis 预扣库存(Lua 原子) | 无资格请求;库存不足直接拒绝,不再往下 |
| 4. 异步层 | 抢到资格 → 发 MQ → 返回”排队中” | 削掉写入洪峰,前端体验平滑 |
| 5. 数据库 | 消费端真正创建订单 + 扣减库存:UPDATE stock SET num = num - 1 WHERE goods_id = ? AND num > 0 | 只接住队列匀速来的写,乐观锁条件 num > 0 兜底防超卖 |
Redis 预扣库存 + Lua 脚本防超卖 ⭐(核中之核):
-- KEYS[1]=库存 key,ARGV[1]=扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]) or '-1')
if stock < tonumber(ARGV[1]) then
return -1 -- 库存不足,直接拒绝
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - 1
要点:GET 判断与 DECRBY 之间不能有缝隙——若先 GET 再 DECR 两步执行,并发下会同时看到”还剩 1”然后都扣成负数(超卖);Lua 单线程原子执行杜绝该窗口。
防刷三板斧:验证码/答题(挡脚本+拉平峰值)、风控规则(同用户限购 1 件、同设备/同 IP 频次限制)、黑名单(提前标记羊毛党账号,网关直接拦截)。
热点与一致性:
- 库存是热点 key:单分片扛不住就分段(
stock_1~stock_10各存 1/10,随机段扣减,合并剩余量); - 不超卖靠 Redis 原子预扣 + DB 条件更新双保险;不少卖靠超时未支付回补库存(延迟消息)+ 定时对账 Redis 与 DB 的库存差异;
- 缓存与 DB 一致性:下单成功以 DB 为准,Redis 库存在扣减成功后随 MQ 消费同步更新,失败有对账兜底。
一分钟回答框架:澄清规模 → 漏斗五层逐层挡流量 → Redis+Lua 原子预扣防超卖 → MQ 异步落库削峰 → DB 条件更新兜底 → 防刷与对账保一致。
动手实践
-
Redis + Lua 限流器
- 用
StringRedisTemplate.execute(RedisScript)封装”固定窗口限流”,参数为 key、窗口秒数、阈值; - 写清注释:为什么必须 Lua(INCR/EXPIRE 两步不原子,进程中断会导致 key 永不过期);
- 验收:20 线程并发调用阈值 10 次/10 秒的限流器,恰好放行 10 个;窗口过期后自动恢复放行。
- 用
-
实现秒杀预扣库存(复用第 7 节 Lua)
- Spring Boot + StringRedisTemplate:初始化库存 → Lua 预扣 → 抢到后发 RocketMQ/RabbitMQ 消息 → 消费端落库
UPDATE ... AND num > 0; - 验收:JMeter 500 并发抢 100 件库存,最终订单恰好 100 条、库存不为负;把每层日志(预扣成功/失败、MQ 消费、落库)打出来核对漏斗。
- Spring Boot + StringRedisTemplate:初始化库存 → Lua 预扣 → 抢到后发 RocketMQ/RabbitMQ 消息 → 消费端落库
-
写一份秒杀设计文档
- 固定三段:① 扣库存(预扣+落库怎么分工)② 防超卖(两层保险在哪)③ 一致性(超时回补、对账、缓存更新);
- 验收:文档里有 QPS 估算过程、流量漏斗图(文字版即可)、防刷清单;能对着文档口头讲 3 分钟不卡壳。
高频面试题
Q:设计一个秒杀系统?⭐⭐
- 先澄清量级:多少库存、多少用户、峰值读多大,再开始设计;
- 流量漏斗:前端(置灰/答题/动态 URL)→ 网关(令牌桶限流+黑名单)→ 服务(风控限购 + Redis 预扣)→ MQ 异步 → DB 匀速落库;
- 防超卖双保险:Redis Lua 原子预扣(GET+DECR 不留缝隙)+ DB
UPDATE ... AND num > 0条件更新; - 削峰:抢到资格先进 MQ 返回”排队中”,消费端按 DB 能力匀速创建订单;
- 热点:库存 key 分段扛单分片极限,读走本地缓存+静态化;
- 收尾讲一致性:超时未支付用延迟消息回补库存,定时对账兜底。
Q:四种限流算法对比?⭐
- 固定窗口:最简单,但窗口交界处有 2 倍突刺;
- 滑动窗口:按小格子统计最近 N 秒,消除突刺,Sentinel 采用;
- 漏桶:恒定速率流出,绝对平滑但压掉合理突发,Nginx 采用;
- 令牌桶:匀速放令牌、桶内可攒,允许突发又能限制平均速率,Guava RateLimiter 采用;
- 实现选型:单机用 RateLimiter/Sentinel,集群全局用 Redis+Lua。
Q:如何防止超卖?⭐
- 第一层:Redis 预扣库存,Lua 把”查库存+扣减”做成原子操作,不留并发窗口;
- 第二层:数据库
UPDATE stock SET num = num - 1 WHERE id = ? AND num > 0,利用行锁+条件更新兜底,返回影响行数为 0 即回滚; - 也可以用
SELECT ... FOR UPDATE悲观锁,但吞吐低,作为对比方案讲; - 最后对账:Redis 与 DB 库存定时核对,发现偏差告警修复。
Q:如何保证缓存与数据库一致性?⭐
- 基础方案 Cache Aside:读→先缓存后 DB;写→先更新 DB,再删除缓存(不是更新缓存);
- 删除失败的补偿:消息队列重试或订阅 binlog(canal)异步删除,解耦且不易漏;
- 并发窗口要求高:延迟双删(写后删一次,隔几百毫秒再删一次,清掉期间旧读回填的脏缓存);
- 结论话术:强一致代价极高,业务上通常接受”最终一致 + 不一致窗口最小化”。
Q:系统突然来 10 倍流量怎么办?
- 应急三件套:限流(保住容量内的流量)、降级(非核心功能先砍)、扩容(无状态服务横向加实例);
- 缓存层检查:命中率是否下降、有没有热点 key 打爆单分片;
- 写洪峰进 MQ 排队削峰,消费速率对齐 DB 承受力;
- 事后:补压测、加容量告警,把”突变”变成”可预测的扩容预案”。
Q:为什么要多级缓存?本地缓存和 Redis 怎么分工?
- Redis 集中管理:容量大、一致性好维护,但每次多一次网络 IO 与序列化(约 0.5~1ms);
- 本地缓存:纳秒级响应,但各实例独立、容量有限、更新难同步;
- 分工:极热、更新少的小数据(配置、TopN 商品)进本地,主力缓存放 Redis,DB 兜底;
- 本地用短 TTL(秒级)控制不一致窗口,或通过 MQ 广播失效通知各实例清本地缓存。
Q:做了读写分离,主从延迟怎么处理?
- 先定性:复制是异步的,延迟通常毫秒级,但大事务、机器负载高时会放大到秒级;
- 关键路径强制读主库:下单后立刻查订单,这类读不信任从库;
- 会话级路由:同一用户”写后 N 秒内”的读请求路由到主库;
- 监控从库同步延迟(Seconds_Behind_Master),落后过大告警并摘除读流量。
推荐资料
| 资料 | 说明 | 链接 |
|---|---|---|
| JavaGuide 读写分离与分库分表 | 数据层扩展的完整思路 | https://javaguide.cn/high-performance/read-and-write-separation-and-library-subtable.html |
| JavaGuide 高可用系统设计指南 | 限流/熔断/降级/隔离的系统化讲解 | https://javaguide.cn/high-availability/high-availability-system-design.html |
| JavaGuide 负载均衡 | 演进路径与网关层的基础概念 | https://javaguide.cn/high-performance/load-balancing.html |
| JavaGuide 消息队列入门 | 削峰、异步、解耦三大作用 | https://javaguide.cn/high-performance/message-queue/message-queue.html |
| JavaGuide 系统设计面试题 | 秒杀/抢红包等场景题的答题框架 | https://javaguide.cn/system-design/system-design-questions.html |
| pdai Redis 缓存问题 | 穿透/击穿/雪崩/一致性的深入篇 | https://pdai.tech/md/db/nosql-redis/db-redis-x-cache.html |
| pdai MySQL 分库分表 | 分片策略与问题的更多细节 | https://pdai.tech/md/db/sql-mysql/sql-mysql-devide.html |
| pdai 架构知识体系 | 高并发演进与架构设计的全景图 | https://pdai.tech/md/arch/arch-z-overview.html |