跳到正文
Java 入门到面试指南
返回

02-高并发系统设计

阶段:分布式与高并发篇 | 建议时长: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. 加缓存热点读打穿到 DBRedis 缓存热点数据,DB 只兜底缓存没有的
4. MQ 削峰写入洪峰(大促/秒杀)写请求先进 MQ,消费端按 DB 能力匀速落库
5. 分库分表单表过亿、单库写入瓶颈垂直拆业务库,水平按分片键拆表
6. 微服务化单体应用发布慢、模块互相拖垮按业务域拆服务,注册中心 + 网关 + 链路追踪

三条铁律:

  1. 演进由瓶颈驱动,不要一上来就微服务+分库分表(过度设计);
  2. 每一步都是为了解决上一层扛不住的那一类流量(读 → 缓存、写 → MQ、数据量 → 分片);
  3. 面试讲演进时永远带一句”当时的瓶颈是什么”,体现判断力而不是背名词。

2. 缓存体系:多级缓存与热点 key ⭐

完整的读链路:浏览器缓存 → CDN → Nginx 缓存 → 本地缓存(Caffeine)→ Redis → MySQL。离用户越近越快,越远越全。

为什么要两级(本地 + Redis):

维度本地缓存 CaffeineRedis
读取速度纳秒~微秒(进程内存)毫秒内(多一次网络 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

三种实现方式:

  1. Guava RateLimiter(单机):令牌桶,tryAcquire() 拿不到就快速失败。只对单个实例有效,多实例要乘以实例数折算阈值;
  2. Sentinel(单机 + 集群,最常用于生产):滑动窗口统计,支持 QPS/线程数两种维度、流控效果(快速失败/Warm Up/排队等待)、熔断降级一体,配控制台动态改规则;
  3. 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/宽表。

分库分表的三大代价(必考):

  1. 跨片查询:无分片键的条件要扇出到全部分片再聚合,分页/排序代价极高(配合 ES 或异构索引解决);
  2. 分布式事务:跨库写无法用本地事务,需要 01 篇的分布式事务方案;
  3. 扩容与数据迁移:翻倍扩容要迁移数据,常用”翻倍扩容 + 双写迁移 + 灰度校验”;
  4. 附带问题:全局唯一主键(见《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 条件更新兜底 → 防刷与对账保一致。

动手实践

  1. Redis + Lua 限流器

    • 用 StringRedisTemplate.execute(RedisScript) 封装”固定窗口限流”,参数为 key、窗口秒数、阈值;
    • 写清注释:为什么必须 Lua(INCR/EXPIRE 两步不原子,进程中断会导致 key 永不过期);
    • 验收:20 线程并发调用阈值 10 次/10 秒的限流器,恰好放行 10 个;窗口过期后自动恢复放行。
  2. 实现秒杀预扣库存(复用第 7 节 Lua)

    • Spring Boot + StringRedisTemplate:初始化库存 → Lua 预扣 → 抢到后发 RocketMQ/RabbitMQ 消息 → 消费端落库 UPDATE ... AND num > 0;
    • 验收:JMeter 500 并发抢 100 件库存,最终订单恰好 100 条、库存不为负;把每层日志(预扣成功/失败、MQ 消费、落库)打出来核对漏斗。
  3. 写一份秒杀设计文档

    • 固定三段:① 扣库存(预扣+落库怎么分工)② 防超卖(两层保险在哪)③ 一致性(超时回补、对账、缓存更新);
    • 验收:文档里有 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