Java 面试笔记VII · 05 / 05

Lesson 70 · 分布式系统设计

高可用架构设计:从单点到百万 QPS 的演进

深度·🔥 极高·#架构·#高可用·#系统设计

第 1 站

面试官:如何设计一个能承载百万 QPS 的系统?

"从 0 到 1 设计一个电商系统,你会怎么做?当流量增长 10 倍、100 倍、1000 倍时,架构怎么演进?" —— 面试官想考察的是你的架构思维和技术视野,而不是某个具体技术点的深度。

本文以一个电商系统为蓝本,按照流量从 0 到百万 QPS 的增长过程,逐步演进架构。每个阶段都解决上一阶段的瓶颈:

阶段 0:单机部署

单机架构 · 日活 1000 以下
// 一台服务器搞定一切

用户[Nginx][Tomcat(Spring Boot)][MySQL]
                          ↓
                    所有代码都在这里

// 配置:4C8G,MySQL 在同一台机器
// 能承载:~100 QPS
// 瓶颈:CPU、内存、磁盘 IO 都是上限

优化方式:
  垂直扩展(Scale Up)→ 升级到 16C64G
  → 加 SSD → 加内存
  → 单机上限大约 1000-2000 QPS
  → // 但单机扩展有天花板,且是单点故障
阶段 0:单机部署架构 单台服务器 Nginx Tomcat MySQL(同一台机器) 单点故障 + 扩展天花板 ~2000 QPS
图 1单机架构:所有组件在一台服务器上,存在单点故障
第 2 站

阶段 1-2:负载均衡 + 应用集群

单机扩展有天花板,下一步是水平扩展(Scale Out):部署多台应用服务器,前面挂负载均衡器。

阶段 1-2:负载均衡 + 应用集群 Nginx / LVS 负载均衡 App Server 1 Spring Boot App Server 2 Spring Boot App Server 3 Spring Boot App Server N ... MySQL(主从) 可承载:5000-10000 QPS(取决于应用服务器数量)
图 2负载均衡 + 应用集群:水平扩展应用层
负载均衡策略 · 常见算法
轮询(Round Robin)
  → 依次分发到后端服务器
  → 适合所有服务器配置相同的情况

加权轮询(Weighted Round Robin)
  → 根据服务器权重分发(权重高的多分配)
  → 适合服务器配置不同的情况

最少连接(Least Connections)
  → 分发给当前连接数最少的服务器
  → 适合长连接场景

IP Hash
  → 根据客户端 IP 哈希到固定服务器
  → 保证同一个客户端的请求始终到同一台服务器
  → 适合有 Session 的场景(但不推荐,应用层应无状态)

一致性哈希
  → 基于请求特征(如 userId)做一致性哈希
  → 服务器增减时只影响少量请求的映射
关键原则
应用层必须无状态。Session 不要存在 Tomcat 内存里,用 Redis 集中存储。这样任何一台服务器都可以处理任何请求,负载均衡才能真正生效。
第 3 站

阶段 3:数据库读写分离

应用层可以水平扩展,但数据库是瓶颈。大部分互联网业务的读写比例是 10:1 甚至 100:1,读远多于写。读写分离把读请求分散到多个从库:

阶段 3:数据库读写分离 + 分库分表 应用层集群 Master 主库 写操作 Slave 1 读操作 Slave 2 读操作 Slave 3 读操作 binlog 分库分表:ShardingSphere / MyCat 当单表超过 500 万行,考虑水平拆分(按 userId 取模分库)
图 3读写分离:主库写,从库读,通过 binlog 同步
读写分离注意事项
主从延迟问题:
  写完主库后立即读从库 → 可能读到旧数据
  // 解决方案:
  // 1. 关键业务强制读主库(@Master)
  // 2. 写后短暂延迟再读
  // 3. 写后把数据写入缓存,读缓存兜底

分库分表策略// 水平分表:同一张表按规则拆到多个库
  user_id % 4 → 4 个库,每个库存 1/4 的用户
  // 垂直分表:大字段拆到扩展表
  user_base (id, name, age) + user_detail (id, avatar, bio)

分库分表的坑:
  跨库 JOIN → 改为应用层关联
  分布式事务 → 需要分布式事务方案(Seata/TCC)
  全局排序/分页 → 二次排序或游标分页
  全局唯一 ID → 雪花算法(Snowflake)/ Leaf
第 4 站

阶段 4:缓存层——挡在数据库前的防线

数据库是系统中最慢的组件之一。引入缓存层可以大幅减少数据库压力:

阶段 4:多级缓存架构 CDN 缓存 Nginx 缓存 本地缓存 Caffeine Redis 集群 MySQL miss miss miss miss 静态资源 页面缓存 热点数据 业务缓存 持久化 缓存三大问题及解决方案 缓存穿透(查不存在的数据)→ 布隆过滤器 / 缓存空值 缓存击穿(热点 key 过期)→ 互斥锁重建 / 永不过期 | 缓存雪崩(大量 key 同时过期)→ 过期时间加随机值
图 4多级缓存架构:CDN → Nginx → 本地缓存 → Redis → MySQL
缓存策略 · Cache Aside vs Write Through
Cache Aside(旁路缓存)—— 最常用
  读:先查缓存 → 命中则返回 → 未命中则查 DB → 写入缓存
  写:先更新 DB → 再删除缓存(不是更新缓存!)
  // 为什么删除而不是更新?避免并发写导致的不一致

Write Through(写穿透)
  写操作同时更新缓存和 DB(由缓存中间件代理)
  → 保证缓存和 DB 的一致性
  → 但写入延迟增加

Write Behind(写回)
  写操作只更新缓存,异步批量刷新到 DB
  → 写入性能最高
  → 但宕机可能丢数据(Redis 持久化问题)

// 工程实践中 Cache Aside + 延迟双删 最常用:
// 1. 删除缓存 → 2. 更新 DB → 3. 延迟 500ms → 4. 再删缓存
// 步骤 4 兜底解决步骤 2 和 3 之间的并发读导致的脏缓存
第 5 站

阶段 5:消息队列——异步处理与流量削峰

同步调用链路越长,系统响应越慢、耦合度越高。引入消息队列(MQ)可以实现异步处理流量削峰

阶段 5:消息队列实现异步处理 同步调用(下单耗时 800ms) 创建订单 200ms 扣库存 200ms 扣余额 200ms 发短信 200ms 共 800ms 异步调用(下单耗时 200ms) 创建订单 200ms 发送 MQ 消息 共 200ms MQ → 消费者异步处理(库存、余额、短信) MQ 三大作用:异步(解耦) + 削峰(缓冲) + 解耦(上下游不直接依赖)
图 5消息队列:同步调用 800ms → 异步调用 200ms,性能提升 4 倍
MQ 选型对比
RocketMQ(阿里开源)
  特点:事务消息、延迟消息、死信队列
  适合:电商交易、金融场景
  单机吞吐:10 万级 TPS

Kafka(LinkedIn 开源)
  特点:超高吞吐、分区消费、日志存储
  适合:日志采集、大数据管道、实时流处理
  单机吞吐:100 万级 TPS

RabbitMQ(Pivotal)
  特点:灵活路由(Exchange)、消息确认机制
  适合:复杂业务路由、企业应用集成
  单机吞吐:级 TPS

// 电商系统通常选 RocketMQ(事务消息 + 业务功能丰富)
// 大数据/日志系统通常选 Kafka(超高吞吐)
第 6 站

阶段 6-7:微服务拆分 → 服务网格

当系统规模继续增长,单体应用需要拆分成微服务。而当微服务数量爆炸时,需要服务网格来管理:

微服务架构演进 微服务架构 用户服务 订单服务 库存服务 支付服务 Nacos + Sentinel 服务网格(Service Mesh) 服务 A Sidecar 服务 B Sidecar 控制平面(Istio) Sidecar 代理所有通信 业务代码无需关注服务治理 完整高可用电商架构 DNS/CDN → LVS/Nginx → API Gateway → 微服务集群 → Redis → MQ → MySQL 集群 + Nacos(注册中心)+ Sentinel(限流熔断)+ SkyWalking(链路追踪)
图 6从微服务到服务网格的演进,以及完整的高可用架构
微服务治理全家桶
注册中心:Nacos / Consul / ZooKeeper
  → 服务注册与发现

配置中心:Nacos / Apollo / Spring Cloud Config
  → 动态配置推送

API 网关:Spring Cloud Gateway / Kong / APISIX
  → 路由、鉴权、限流、日志

RPC 框架:Dubbo / gRPC / OpenFeign
  → 服务间通信

链路追踪:SkyWalking / Jaeger / Zipkin
  → 分布式调用链追踪,定位慢请求

日志聚合:ELK(Elasticsearch + Logstash + Kibana)
  → 集中式日志查询

监控告警:Prometheus + Grafana
  → 系统指标采集 + 可视化 + 告警
第 7 站

容量规划、监控告警与总结

高可用架构不只是技术问题,还需要容量规划来量化:

容量规划核心公式
日均 QPS = 日活用户 × 人均请求数 ÷ 86400
峰值 QPS ≈ 日均 QPS × 峰值系数(通常 3-5 倍)
所需服务器数 = 峰值 QPS ÷ 单机 QPS × 冗余系数(1.5-2 倍)
容量规划示例 · 电商大促
// 场景:日活 1000 万用户的电商大促

日均 QPS1000万 用户 × 50 请求/人 ÷ 86400 秒 ≈ 5800 QPS

峰值 QPS(大促零点):
  5800 × 5(峰值系数)≈ 29000 QPS

所需服务器:
  单机 QPS = 500(Spring Boot + Redis + MySQL)
  29000 ÷ 500 × 1.5(冗余)≈ 87 台应用服务器

数据库规划:
  主库写入 QPS = 29000 × 10%(写比例)= 29004 个库 → 每个库 725 写入 QPS(可承受)
  从库 8 个 → 每个从库 3200 读取 QPS

Redis 规划:
  缓存命中率 95% → 只有 5% 穿透到 DB
  Redis 集群 6 节点(3 主 3 从)→ 单节点 10 万 QPS

高可用部署模式:

多机房部署方案
同城双活:
  两个机房在同一城市,延迟 < 2ms
  → 数据库主从部署在两个机房
  → 应用层同时对外服务
  → 通过 DNS 权重分配流量
  // 优点:机房级容灾,RPO ≈ 0

异地多活:
  多个机房分布在不同城市/国家
  → 每个机房独立处理请求
  → 数据通过 MQ 异步同步
  → 通过 GSLB(全局负载均衡)分配流量到最近机房
  // 优点:城市级容灾,延迟最优
  // 难点:数据冲突解决(如两个机房同时修改同一用户信息)

两地三中心:
  同城双活 + 异地灾备
  → 同城两个机房做主从同步(保证数据一致)
  → 异地机房做冷备(极端灾难恢复)
  // 银行、金融行业的标配架构

容灾指标:
  RPO(Recovery Point Objective):能容忍丢多少数据
  RTO(Recovery Time Objective):多久恢复服务
  同城双活:RPO ≈ 0,RTO < 1 分钟
  异地多活:RPO ≈ 秒级,RTO ≈ 分钟级

全链路压测:

全链路压测 · 验证高可用架构
// 高可用架构需要全链路压测来验证

核心原则:
  在生产环境模拟真实流量,验证系统在高负载下的表现
  → 发现瓶颈点(哪个服务先扛不住)
  → 验证限流、熔断、降级是否按预期工作
  → 验证容量规划是否准确

流量染色:
  压测请求添加特殊 Header(如 x-test: true)
  → 压测数据写入影子库/影子表
  → 不影响线上真实数据

常见压测工具:
  JMeter — 功能全面,支持多种协议
  Gatling — Scala 编写,性能优异
  自研压测平台 — 大厂通常自建(如阿里 PTS)
层级监控指标告警阈值
基础设施CPU、内存、磁盘、网络CPU > 80%、磁盘 > 85%
应用层QPS、RT、错误率、线程数RT > 500ms、错误率 > 1%
中间件Redis 命中率、MQ 积压、DB 连接数命中率 < 90%、积压 > 1 万
业务层订单量、支付成功率、转化率支付成功率 < 95%

架构演进全景图:

高可用架构演进全景 单机部署 ~100 QPS 负载均衡 +应用集群 ~5K QPS 读写分离 +分库分表 ~20K QPS 缓存层 多级缓存 ~100K QPS MQ 异步 削峰填谷 ~300K QPS 微服务 +服务治理 ~500K QPS Service Mesh ~1M QPS 高可用设计六字诀 (拆分/分库分表)→ (多级缓存)→ (异步/MQ) (降级/熔断)→ (限流)→ (监控/告警)
图 7从单机到百万 QPS 的架构演进路径
本课文脑图回顾
  • 单机 → 垂直扩展(Scale Up)有天花板,必须转向水平扩展(Scale Out)
  • 负载均衡:Nginx/LVS + 无状态应用集群,Session 集中存储到 Redis
  • 读写分离:主库写、从库读,分库分表解决单表瓶颈
  • 缓存层:CDN → Nginx → 本地缓存 → Redis → DB,解决三大缓存问题
  • 消息队列:异步处理 + 流量削峰 + 服务解耦
  • 微服务 + 服务网格:拆分 → 治理 → Sidecar 代理
  • 容量规划公式:峰值 QPS = 日均 QPS × 峰值系数,服务器数 = 峰值 QPS ÷ 单机 QPS × 冗余
  • 高可用六字诀:分、缓、异、降、限、监