1 系统设计基础
System Design Fundamentals — 从全局视角理解分布式系统
系统设计 = 城市规划

想象你是一座城市的总规划师。道路网络就是系统间的通信链路,供水系统是数据流动的管道,供电网络是服务可用的保障,应急预案是容灾与降级策略。好的城市规划让城市在暴雨(流量洪峰)中不内涝(不宕机),在管道爆裂(节点故障)时有备用供水(故障转移)。

关键指标详解

指标全称含义典型值
QPSQueries Per Second每秒查询数(读请求为主)知乎 ~30K, 淘宝双11 ~数百万
TPSTransactions Per Second每秒事务数(写操作为主)MySQL 单机 ~3K-8K
Latency响应延迟请求发出到收到响应的时间P99 < 200ms 为佳
Availability可用性系统正常运行时间占比通常要求 ≥ 99.9%
Throughput吞吐量单位时间处理的数据量MB/s 或 GB/s
QPS vs TPS 区别

QPS 偏向"查询",一个页面可能包含多个 Query(接口调用)。TPS 偏向"事务",一个事务可能包含多个操作(扣库存 + 创建订单 + 扣余额)。面试中两个术语经常混用,但严格来说 TPS 更重,通常 TPS ≤ QPS。

可用性对照表 — "几个9" 意味着什么?

可用性等级与年允许宕机时间
可用性 年宕机时间 日宕机时间 可视化 等级
99% 3.65 天 14.4 分钟
基本可用
99.9% 8.77 小时 1.44 分钟
较高可用
99.99% 52.6 分钟 8.64 秒
高可用
99.999% 5.26 分钟 0.86 秒
极高可用
99.9999% 31.5 秒 0.086 秒
电信级

公式: 年宕机时间 = (1 - 可用性) × 365 × 24 × 60 分钟

面试技巧

面试官问"设计一个系统"时,先澄清 QPS 量级可用性要求延迟要求。不同量级的方案天差地别:日活 1 万和日活 1 亿的系统设计完全不同。

2 高可用设计
High Availability — 消除单点故障,确保系统持续可用

冗余策略

三种冗余架构对比
主从模式 (Master-Slave)
Master
读写
▼ 复制
Slave 1只读
Slave 2只读

Master 故障时提升 Slave

双活模式 (Active-Active)
Active 1读写
Active 2读写
DB 同步

两节点同时提供服务

集群模式 (Cluster)
Load Balancer
Node 1
Node 2
Node 3
Node N

无状态节点水平扩展

故障转移 (Failover)

类型方式切换速度优缺点
自动故障转移心跳检测 + 自动切换秒级快,但可能误判(脑裂风险)
手动故障转移运维人员确认后切换分钟~小时级安全,但停机时间长
脑裂问题 (Split Brain)

当两个节点之间的网络断开,各自都认为对方已故障,都提升自己为 Master。解决方案:仲裁节点 (Quorum) — 至少 3 个节点,少数服从多数。类似 Kubernetes 的 etcd 集群要求奇数节点。

健康检查与熔断器 (Circuit Breaker)

熔断器的思想来源于电路中的保险丝:当下游服务异常率超过阈值时,自动"断开",避免级联故障。

熔断器状态机 — Closed → Open → Half-Open
Closed (关闭)
正常放行所有请求
统计失败率
失败率 > 阈值
Open (开启)
快速失败,不调用下游
等待超时窗口
超时后
Half-Open (半开)
放行少量请求探测
成功则 Closed,失败则 Open
↩ 成功 → 回到 Closed | ↩ 失败 → 回到 Open

限流算法 (Rate Limiting)

令牌桶 vs 漏桶算法
令牌桶 (Token Bucket)
T
T
T
T
T
固定速率放入令牌
请求需消耗令牌
允许突发流量(桶内有积累)
典型: Guava RateLimiter
漏桶 (Leaky Bucket)
请求进入桶中排队
固定速率流出处理
平滑输出流量
典型: Nginx leaky bucket
算法突发流量平滑性实现复杂度适用场景
固定窗口❌ 窗口边界问题简单限流
滑动窗口⚠️ 有改善精准限流
令牌桶✅ 允许API 网关、一般业务
漏桶❌ 不允许流量整形、日志写入

优雅降级策略

常见降级手段
  • 读降级:读缓存/兜底数据,而非实时查询数据库
  • 写降级:先写缓存/MQ,异步落盘(如点赞先存 Redis)
  • 功能降级:关闭非核心功能(如评论、推荐),保核心链路
  • 页面降级:返回静态页面或精简版 UI
Production Case: "双十一流量是平时100倍"

场景:某电商平台日常 QPS ~5K,双11 零点峰值达 500K QPS。

方案

  • 限流:网关层令牌桶限流,超出直接返回"活动太火爆"
  • 降级:关闭猜你喜欢、商品评价等非核心功能,释放资源
  • 扩容:提前 2 小时自动扩容 Kubernetes Pod 数量 10 倍
  • 预热:提前将热卖商品数据加载到缓存,避免冷启动
  • 异步:下单走消息队列削峰,用户看到"排队中"
3 负载均衡
Load Balancing — 将流量均匀分发到多个服务实例
负载均衡架构图
Client 1
Client 2
Client 3
▼ ▼ ▼
Load Balancer (Nginx / HAProxy / 云 LB)
Server 1
10.0.0.1:8080
Server 2
10.0.0.2:8080
Server 3
10.0.0.3:8080

负载均衡算法对比

算法原理优点缺点场景
Round Robin 依次轮询 简单公平 不考虑服务器差异 服务器配置一致
Weighted RR 按权重分配 适配不同配置 权重需手动调 异构服务器集群
IP Hash 对 IP 取模 会话保持 节点变更时大量重分配 需要 Session 粘滞
Least Conn 选连接数最少的 动态均衡 需实时统计连接数 长连接 / 请求耗时不均

L4 vs L7 负载均衡

维度L4 (传输层)L7 (应用层)
工作层TCP/UDPHTTP/HTTPS/gRPC
路由依据IP + PortURL, Header, Cookie
性能高(只需看包头)稍低(需解析应用层)
灵活性高(可基于路径路由)
代表LVS, AWS NLBNginx, HAProxy, AWS ALB

Nginx 配置示例

# Nginx upstream 配置 - Weighted Round Robin
upstream backend {
    server 10.0.0.1:8080 weight=5;  # 高配服务器,分配更多流量
    server 10.0.0.2:8080 weight=3;
    server 10.0.0.3:8080 weight=2;  # 低配服务器

    # 健康检查(开源版需第三方模块)
    # max_fails=3 fail_timeout=30s

    # 保持长连接
    keepalive 32;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # IP Hash 会话保持
    # location /api/ {
    #     ip_hash;
    #     proxy_pass http://backend;
    # }
}
Production Case: "某台服务器配置高但负载一样"

问题:三台服务器分别 16C/8C/4C,但 Round Robin 分配了等量流量,导致低配机器 CPU 打满。

解决:切换为 Weighted Round Robin,权重比设为 4:2:1。同时在 Nginx 配置 max_failsfail_timeout 实现自动摘除故障节点。

4 分布式理论
CAP, BASE, Consistency — 分布式系统的理论基础

CAP 定理

CAP = 不可能三角

就像找工作不可能同时满足"钱多、事少、离家近",分布式系统不可能同时满足 C(一致性)A(可用性)P(分区容错)。网络分区是必然的(P 必选),所以实际只能在 CPAP 之间选择。

CAP 三角定理可视化
C
一致性
Consistency
A
可用性
Availability
P
分区容错
Partition Tolerance
网络分区时
只能选一个
P 是必选的
网络一定分区
CP 系统
ZooKeeper, etcd
HBase, MongoDB
AP 系统
Cassandra, DynamoDB
CouchDB, Eureka
⚠ CA 不存在
分布式网络必然分区

BASE 理论

BASE — CAP 的工程实践
字母含义说明
BABasically Available基本可用 — 允许响应时间增加或功能降级
SSoft State软状态 — 允许中间状态(数据副本可暂时不同)
EEventually Consistent最终一致性 — 经过一段时间后,数据最终一致

BASE 是对 CAP 中 AP 方向的延伸,大多数互联网系统采用 BASE + 局部强一致的混合策略。

一致性模型

三种一致性模型时间线对比
强一致性 (Strong Consistency)
Client A
Write x=5
DB Master
x=5 ✅
同步复制
DB Slave
x=5 ✅
Client B
Read x=5 ✅

写入后,任何副本的读取都能看到最新值。代价:延迟高。

最终一致性 (Eventual Consistency)
Client A
Write x=5
DB Master
x=5 ✅
异步复制
⏳ 延迟
DB Slave
x=3 ❌
Client B
Read x=3 ⚠️

短暂时间内可能读到旧值,但最终会一致。性能好,体验差。

因果一致性 (Causal Consistency)
Client A
Write x=5
Client A
通知 B
Client B
Read x=5 ✅
有因果关系的操作保证顺序

有因果关系的操作按顺序可见,无因果关系的操作可乱序。折中方案。

Production Case: "库存超卖问题"

问题:电商秒杀,10 件商品被 100 人抢购,不加控制可能卖 20 件。

方案对比

方案一致性性能适用
数据库悲观锁 (SELECT FOR UPDATE)强一致低并发秒杀
Redis 原子操作 (DECR)强一致 (Redis 单线程)高并发秒杀
分布式锁 (Redisson)强一致中等并发
最终一致 (先扣后异步校验)最终一致极高可容忍超卖后退款
5 分布式事务
Distributed Transactions — 跨服务的数据一致性保障
分布式事务 = 跨银行转账

你从 A 银行转 1000 元到 B 银行。A 扣钱成功了,B 加钱却失败了 — 钱凭空消失了!分布式事务就是确保"要么都成功,要么都回滚"的协调机制。

2PC — 两阶段提交

2PC (Two-Phase Commit) 流程图
Phase 1: 准备阶段 (Prepare)
Coordinator (协调者)
Prepare ▼
Participant A
锁定资源
▲ Yes/Vote
Prepare ▼
Participant B
锁定资源
▲ Yes/Vote
⬇ 全部投票 Yes
Phase 2: 提交阶段 (Commit)
Coordinator (协调者)
Commit ▼
Participant A
执行提交 ✅
Commit ▼
Participant B
执行提交 ✅
❌ 阻塞: Prepare 后资源锁定直到 Commit
❌ 单点: Coordinator 故障则所有参与者阻塞
⚠️ 性能: 两次 RPC 往返

TCC — Try-Confirm-Cancel

TCC 业务层面的两阶段提交
阶段动作示例(转账场景)
Try资源预留/冻结冻结账户 A 的 1000 元余额
Confirm确认执行A 扣除 1000 元,B 增加 1000 元
Cancel取消/回滚解冻 A 的 1000 元

优点:不锁资源(Try 只冻结),性能好。缺点:业务侵入性强,每个操作都要写三个方法。

Saga 模式

Saga 模式 — 正向执行 + 补偿回滚
✅ 正向执行(成功路径)
1
创建订单 — INSERT order (status=PENDING)
2
扣减库存 — UPDATE inventory SET stock = stock - 1
3
扣减余额 — UPDATE account SET balance = balance - 100
4
确认订单 — UPDATE order SET status=PAID
✅ 完成
❌ 补偿回滚(假设步骤3失败)
3'
补偿余额 — 回滚步骤3(如已扣则加回)
compensate
2'
补偿库存 — stock = stock + 1
compensate
1'
取消订单 — UPDATE order SET status=CANCELLED
compensate

本地消息表 + MQ

最终一致性方案

原理:将分布式事务拆解为本地事务 + 消息通知。

  1. 服务 A 执行本地事务时,同时写入一张本地消息表(状态=待发送)
  2. 后台线程定时扫描消息表,将未发送的消息投递到 MQ
  3. 服务 B 消费 MQ 消息,执行本地事务,成功后 ACK
  4. 如果服务 B 失败,MQ 会重试(需保证幂等性)

优点:实现简单,性能好。缺点:消息表需定期清理,有短暂不一致窗口。

分布式事务方案对比

维度2PCTCCSaga本地消息表
一致性强一致强一致最终一致最终一致
性能❌ 低⚠️ 中✅ 高✅ 高
业务侵入❌ 高 (3个方法)⚠️ 中 (需补偿)⚠️ 中
实现复杂度
适用场景传统数据库资金/支付长流程业务异步通知
典型框架XA 协议Hmily, ByteTCCSeata SagaRocketMQ 事务消息
Production Case: "订单创建成功但库存扣减失败"

场景:用户下单,订单服务创建了订单(step 1 ✅),但调用库存服务扣减库存时超时失败(step 2 ❌)。

Saga 补偿方案

  • 触发补偿 → 执行 cancelOrder(orderId),将订单状态设为 CANCELLED
  • 通知用户"库存不足,订单已取消"
  • 如果库存扣减实际成功但响应超时 → 幂等校验,不重复扣减

关键:每个步骤必须有幂等性(同一操作执行多次结果一致),否则补偿可能出问题。

6 缓存策略
Caching Strategies — 用空间换时间,用内存换性能

缓存模式 (Cache Patterns)

四大缓存模式对比
1. Cache Aside (旁路缓存) — 最常用
App
Cache
命中? ✅ 直接返回
→ miss →
Database
查询并写入缓存

读:先缓存后 DB。写:先 DB 再删缓存。应用层控制缓存逻辑

2. Read Through (读穿透)
App
Cache Layer
自动加载
→ 自动 →
Database

缓存层代理了读取逻辑,应用只需问缓存要数据。类似 Spring Cache 的 @Cacheable。

3. Write Through (写穿透)
App
→ 写 →
Cache
同步写 DB
→ 同步 →
Database

写入时同步更新缓存和 DB,保证一致性但写入延迟高。

4. Write Behind (异步写回)
App
→ 写 →
Cache
写入即返回
→ 异步 →
Database
批量刷盘

写入缓存即返回,异步批量写入 DB。性能极高但可能丢数据(宕机未刷盘)。

缓存失效策略

策略说明适用场景
TTL 过期设置过期时间,到期自动删除所有缓存(兜底策略)
主动删除更新 DB 后立即删除缓存Cache Aside 模式
LRU最近最少使用,淘汰最久未访问的Redis maxmemory-policy
LFU最不经常使用,淘汰访问频率最低的热点数据集中的场景

缓存三大问题

缓存穿透 / 击穿 / 雪崩 可视化
1. 缓存穿透 (Penetration) — 查不存在的数据
恶意请求
id=-1
Cache
❌ 没有
DB
❌ 也没有
返回空
不缓存
🔄 重复攻击

解决方案:① 缓存空值 (TTL 5min) ② 布隆过滤器前置校验 ③ 接口参数校验

2. 缓存击穿 (Breakdown) — 热点 Key 过期
热点 Key
过期 ⏰
10K 请求
同时 miss
DB 瞬时
承受 10K QPS
💥 DB 打满

解决方案:① 热点 Key 永不过期 + 异步刷新 ② 互斥锁 (只放一个请求回源) ③ 提前续期

3. 缓存雪崩 (Avalanche) — 大量 Key 同时过期
1000 个 Key
同时过期 ⏰
大量请求
全部 miss
DB 雪崩式
压力暴增
💥 DB 宕机

解决方案:① TTL 加随机偏移 (避免同时过期) ② 多级缓存 (L1 + L2) ③ 熔断降级 ④ 缓存预热

热点 Key 处理

热点 Key 识别与处理

识别:Redis hotkeys 命令 (Redis 4.0+),或通过 MONITOR 命令统计(生产慎用)。

处理

  • 本地缓存:在应用层使用 Caffeine/Guava Cache 作为 L1,Redis 作为 L2
  • 读写分离:热点 Key 复制到多个从节点分散读压力
  • Key 打散:将 hot_key 拆成 hot_key_1, hot_key_2... hot_key_N,随机读取
Production Case: "缓存雪崩导致数据库被打垮"

场景:某社交平台在晚上 8 点高峰期,缓存集群因网络抖动重启,所有缓存同时失效。

经过:缓存失效后所有流量直接打到数据库,数据库 CPU 100%,连接池耗尽,整个服务不可用,持续 30 分钟。

事后改进

  • 缓存 TTL 加随机值 (30min ± 5min),避免集中过期
  • 引入本地缓存 Caffeine 作为 L1,Redis 作为 L2
  • 网关层限流 + 熔断,缓存故障时返回降级数据
  • 缓存预热:服务启动时自动加载热数据
  • Redis Sentinel 哨兵模式,主节点故障 30 秒自动切换
7 数据库扩展
Database Scaling — 从单机到分布式的数据库演进之路

读写分离

读写分离架构
Application
DB Proxy (读写路由)
✏️ 写操作
📖 读操作
Master
写入
Binlog 复制
→ → →
Slave 1
读取
Slave 2
读取

⚠️ 注意主从复制延迟 — 写完立刻读可能读到旧数据(解决方案:强制路由到 Master)

分库分表

水平分表 vs 垂直分表
垂直分表 (按列拆分)
users 表 (100列)
id | name | email | avatar | bio | ...
⬇ 拆分
用户基础表
id | name | email
用户资料表
id | avatar | bio

将不常用字段拆到扩展表

水平分表 (按行拆分)
orders 表 (5000万行)
id | user_id | amount | ...
⬇ 按 user_id % 3 分片
orders_0
user_id % 3 = 0
orders_1
user_id % 3 = 1
orders_2
user_id % 3 = 2

将数据分散到多张表/库

分片键 (Sharding Key) 选择

分片键选择是分库分表最重要的决策
分片键优点缺点适用
user_id用户维度查询快数据可能不均匀(大V)C 端应用
order_id数据均匀分布用户维度查询需跨片订单系统
create_time时间范围查询方便热点问题(最近数据集中)日志/报表
hash(id)数据最均匀范围查询困难通用场景

原则:分片键应满足 ① 数据分布均匀 ② 查询尽量命中单分片 ③ 避免跨片 JOIN

分布式 ID 生成 — 雪花算法 (Snowflake)

Snowflake ID 结构 (64 bit)
0
1 bit
符号位
时间戳
41 bits
~69年
机器ID
10 bits
1024台
序列号
12 bits
4096/ms

每台机器每毫秒可生成 4096 个不重复 ID,全局唯一,趋势递增

Production Case: "单表 5000 万行查询变慢"

症状:订单表 5000 万行,按 user_id 查询耗时从 10ms 涨到 500ms,索引已优化。

方案

  1. 短期:读写分离 + 热数据缓存(Redis),将读压力分走
  2. 中期:按 user_id % 16 水平分 16 张表,每表 ~300 万行
  3. 长期:引入 TiDB / CockroachDB 等 NewSQL 数据库,自动分片

分片策略:用 ShardingSphere-JDBC 中间件,应用层无感知分片。订单 ID 使用 Snowflake 生成(包含机器信息和时间戳)。

8 设计一个完整系统
System Design Interview — 实时 OEE 看板系统完整 walkthrough
OEE = 工厂的健康体检报告

OEE (Overall Equipment Effectiveness, 设备综合效率) 衡量工厂产线的健康程度。就像体检报告有血压、心率、血氧,OEE 由三个指标组成:可用率(设备有没有在运转)、性能率(运转得够不够快)、质量率(产出的产品合不合格)。实时 OEE 看板就是让这些指标像心跳监护仪一样实时显示。

Step 1: 需求澄清

维度需求
功能实时展示多产线 OEE 指标,支持历史趋势查询、告警通知
数据源PLC/SCADA 设备,每秒上报 10-50 个采集点(温度、速度、良品数等)
用户量内部系统,~200 并发用户(看板 + 移动端)
实时性数据延迟 < 5 秒
数据保留秒级数据 7 天,分钟聚合 1 年

Step 2: 容量估算

Quick Estimation
# 假设: 5 条产线 × 20 设备 × 30 采集点 = 3000 个数据点/秒
# 写入 QPS: 3000 点/秒 × 1KB/点 ≈ 3 MB/s ≈ 260 GB/天
# 7 天原始数据: ~1.8 TB
# 200 并发用户, 每人每 5 秒刷新一次 → 读 QPS ≈ 40

# 结论:
# - 写入压力大 (3K QPS), 需要时序数据库
# - 读取压力小 (40 QPS), 普通缓存即可
# - 存储量中等, SSD 够用

Step 3: 系统架构

实时 OEE 看板系统 — 架构全景图
📡 数据采集层 (Edge)
PLC 设备
SCADA 系统
OPC-UA 网关
▼ MQTT / TCP
🔄 数据接入层
EMQX Broker
(MQTT)
Kafka
(消息队列)
Flink
(流处理)
💾 数据存储层
TimescaleDB
(时序数据)
Redis
(实时缓存)
PostgreSQL
(业务数据)
⚙️ 服务层 (Microservices)
OEE 计算服务
告警服务
查询 API
WebSocket 推送
🖥️ 展示层
Web 看板
(React + ECharts)
移动端 App
企业微信通知

Step 4: 技术选型与理由

组件技术为什么选它
消息接入EMQX工业设备原生支持 MQTT,EMQX 百万级连接
消息队列Kafka高吞吐写入 (3K QPS 轻松), 持久化, 回溯消费
流计算Flink窗口聚合 (5s/1min), 实时计算 OEE 指标
时序数据库TimescaleDB基于 PostgreSQL, SQL 友好, 自动压缩旧数据
实时推送WebSocket双向通信, 服务端主动推送更新
缓存Redis缓存最新 OEE 值, 支撑高频轮询
前端React + EChartsECharts 图表丰富, 支持实时数据更新

Step 5: 数据流详解

# 完整数据流:
PLC 设备
  → MQTT 上报 (每秒)
    → EMQX 接收
      → Kafka (topic: raw-telemetry)
        → Flink 消费
          → 计算 OEE = 可用率 × 性能率 × 质量率
          → 结果写入 TimescaleDB (持久化)
          → 结果写入 Redis (实时缓存)
          → 通过 WebSocket 推送到前端看板
          → 告警判断 → 企业微信通知

# OEE 计算公式:
# 可用率 = 实际运行时间 / 计划运行时间
# 性能率 = 实际产量 / 理论产量
# 质量率 = 良品数 / 总产量
# OEE = 可用率 × 性能率 × 质量率 (0~100%)

Step 6: 扩展性设计

如何从 5 条产线扩展到 50 条?
  • Kafka 分区:按产线 ID 分区,增加分区数即可线性扩容
  • Flink 并行度:增加 TaskManager 数量,并行处理各分区
  • TimescaleDB:hypertable 自动分片,支持分布式部署 (MST)
  • API 服务:无状态设计,Kubernetes HPA 自动扩缩容
  • WebSocket:引入 Redis Pub/Sub 做跨节点消息广播
9 面试题精选 (25道)
Interview Questions — 高可用 · 负载均衡 · CAP · 分布式事务 · 缓存 · 数据库
Q1. 什么是系统设计?面试中系统设计题的答题框架是什么?
系统设计是根据需求确定系统架构、组件、接口和数据流的过程。答题框架 (4步法)
需求澄清 — 问清楚 QPS、用户量、功能范围
容量估算 — 计算 QPS、存储、带宽
高层设计 — 画出核心架构图,说明组件职责
深入设计 — 聚焦瓶颈,讨论缓存、分片、高可用等
Q2. 解释 QPS、TPS、吞吐量的区别。你的系统 QPS 是多少? 中频
QPS = 每秒查询数(读为主),TPS = 每秒事务数(写为主,一个事务可能含多个操作),吞吐量 = 单位时间处理的数据量 (MB/s)。
举例:电商系统日常 QPS ~5K,双11 峰值 ~500K;一个 TPS(下单)内部可能触发 5+ 个 QPS(查库存、查价格、扣积分...)。
Q3. "5个9的可用性"意味着什么?如何实现? 必考
99.999% 可用性 = 年宕机时间不超过 5.26 分钟。
实现手段:
冗余 — 多节点部署,消除单点故障
自动故障转移 — 健康检查 + 自动切换(Keepalived、K8s)
灰度发布 — 新版本逐步放量,降低发布风险
混沌工程 — 主动注入故障,验证系统韧性(Chaos Monkey)
Q4. 什么是熔断器?有哪些状态? 必考
熔断器是一种保护机制,当下游服务故障率超过阈值时自动"断开",避免级联失败。
三个状态
Closed (关闭) — 正常放行请求,统计失败率
Open (开启) — 快速失败,直接返回错误,不调用下游
Half-Open (半开) — 超时后放行少量请求探测,成功则 Closed,失败则 Open
典型实现:Hystrix、Sentinel、Resilience4j
生产建议:熔断阈值设为 50% 失败率 / 10 秒窗口,半开状态放行 5 个探测请求。
Q5. 令牌桶和漏桶限流算法有什么区别? 中频
令牌桶:以固定速率向桶中放入令牌,请求消耗令牌。允许突发流量(桶中有积累的令牌时)。适合 API 网关。
漏桶:请求进入桶中排队,以固定速率流出处理。输出平滑,不允许突发。适合流量整形。
关键区别:令牌桶允许短时间内处理大量请求(突发),漏桶严格控制处理速率。
Q6. 负载均衡有哪些算法?Nginx 默认用哪种? 必考
常见算法:Round Robin、Weighted RR、IP Hash、Least Connections、Random。
Nginx 默认使用 Round Robin(轮询)。如果服务器配置不同,用 Weighted RR。
需要会话保持时用 IP Hash。长连接场景用 Least Connections。
生产案例:某公司用 Round Robin 导致低配服务器过载,改用 Weighted RR(权重 5:3:2)后解决。
Q7. L4 和 L7 负载均衡有什么区别? 中频
L4 (传输层):基于 IP + Port 做路由,性能高但不灵活。代表:LVS、AWS NLB。
L7 (应用层):基于 HTTP URL/Header/Cookie 做路由,灵活但性能稍低。代表:Nginx、HAProxy、AWS ALB。
生产环境通常 L4 + L7 组合使用:LVS 做最前端四层负载,Nginx 做七层路由。
Q8. 解释 CAP 定理。为什么说 CA 不存在? 必考
CAP = Consistency (一致性) + Availability (可用性) + Partition tolerance (分区容错)。
CA 不存在的原因:在分布式系统中,网络分区(P)是必然发生的(网络延迟、交换机故障等)。所以 P 必选,只能在 C 和 A 之间权衡。
CP 系统(保证一致性):ZooKeeper、etcd — 分区时拒绝部分请求
AP 系统(保证可用性):Cassandra、Eureka — 分区时可能返回旧数据
Q9. BASE 理论是什么?和 CAP 的关系? 中频
BASE = Basically Available (基本可用) + Soft State (软状态) + Eventually Consistent (最终一致性)。
关系:BASE 是 CAP 中 AP 方向的工程实践。大多数互联网系统采用 BASE 理论 — 牺牲强一致性,换取高可用和高性能,通过异步机制保证最终一致性。
例如:点赞数不需要实时精确,用户看到的是缓存的近似值,最终会同步到数据库。
Q10. 强一致性、最终一致性、因果一致性分别是什么? 中频
强一致性:写入后任何副本的读取都能看到最新值。延迟高(需同步复制)。
最终一致性:短暂时间内可能读到旧值,但最终(通常毫秒到秒级)所有副本一致。
因果一致性:有因果关系的操作保证顺序(如先发帖后评论),无因果关系的操作可乱序。是强一致和最终一致的折中。
Q11. 什么是 2PC?它有什么问题? 必考
2PC (Two-Phase Commit):协调者先问所有参与者"能否提交"(Prepare),全部回答 Yes 后再通知提交(Commit)。
问题
阻塞 — Prepare 后资源被锁定,直到 Commit/Abort,期间其他事务等待
单点故障 — 协调者宕机则所有参与者永远阻塞
性能差 — 两次 RPC 往返 + 同步阻塞,TPS 很低
实际中很少直接用 2PC/XA,更多用 TCC、Saga 或消息最终一致。
Q12. TCC 和 Saga 的区别?各自适用什么场景? 高频
TCC:每个操作提供 Try/Confirm/Cancel 三个方法。Try 阶段冻结资源,Confirm 执行,Cancel 回滚。
→ 适合资金、支付等对一致性要求高的短事务。

Saga:将长事务拆成多个本地事务,每个本地事务有对应的补偿操作。失败时反向执行补偿。
→ 适合长流程业务(如旅行预订:机票→酒店→租车),每个步骤涉及不同服务。

关键区别:TCC 在 Try 阶段就预留资源(隔离性好),Saga 不预留(可能脏读)。
Q13. 如何保证分布式事务的幂等性? 高频
幂等性 = 同一操作执行多次结果一致。常见方案:
唯一 ID + 去重表:每笔操作携带唯一 ID,执行前查去重表是否已处理
乐观锁 (版本号):UPDATE ... SET val=new, version=version+1 WHERE version=old
状态机:只有特定状态才能转换(PENDING → PAID,重复支付时状态已是 PAID 则忽略)
数据库唯一约束:利用 UNIQUE KEY 防止重复插入
Q14. 本地消息表方案是怎么实现最终一致性的? 中频
① 服务 A 执行本地事务时,同时写入一张本地消息表(状态=待发送)— 利用本地事务保证原子性
② 后台定时任务扫描消息表,将未发送的消息投递到 MQ
③ 服务 B 消费 MQ 消息,执行本地事务
④ 如果服务 B 失败,MQ 重试(需保证幂等性)
优点:实现简单,不依赖 XA 协议。缺点:消息表需定期清理,有短暂不一致窗口。
Q15. Cache Aside 模式中,为什么是"先更新 DB 再删缓存"而不是"先删缓存再更新 DB"? 必考
如果先删缓存再更新 DB
① 线程 A 删除缓存
② 线程 B 读缓存 miss,从 DB 读到旧值
③ 线程 B 将旧值写入缓存
④ 线程 A 更新 DB
→ 结果:缓存中是旧值,永久不一致!

先更新 DB 再删缓存:即使删缓存失败,可以靠 TTL 兜底。更严格的方案是用延迟双删:更新 DB → 删缓存 → 延迟 500ms → 再删一次。
Q16. 缓存穿透、击穿、雪崩分别是什么?怎么解决? 必考
穿透:查询不存在的数据 → 缓存和 DB 都没有 → 请求直达 DB。
解决:① 缓存空值 (TTL 短) ② 布隆过滤器 ③ 参数校验

击穿:热点 Key 过期瞬间 → 大量请求同时打到 DB。
解决:① 热点 Key 永不过期 + 异步刷新 ② 互斥锁(只放一个请求回源)

雪崩:大量 Key 同时过期 + 或缓存集群宕机 → DB 瞬时承受巨大压力。
解决:① TTL 加随机偏移 ② 多级缓存 ③ 熔断降级 ④ 缓存预热
Q17. 如何保证缓存和数据库的一致性? 高频
常见方案
Cache Aside + 延迟双删:更新 DB → 删缓存 → 延迟再删。简单但非严格一致。
订阅 Binlog:通过 Canal 监听 MySQL Binlog,异步删除/更新缓存。更可靠。
Read Through + Write Through:缓存层代理读写逻辑,对应用透明。
经验:大多数场景用方案①就够了。一致性要求极高时用方案②(Canal + MQ + 消费者删缓存)。
Q18. 热点 Key 如何处理? 中频
识别:Redis 4.0+ 的 redis-cli --hotkeys,或通过 MONITOR 统计(生产慎用)。
处理
本地缓存:应用层使用 Caffeine/Guava Cache 作为 L1 缓存
读写分离:将热点 Key 复制到多个从节点分散读压力
Key 打散:hot_key → hot_key_1, hot_key_2...hot_key_N,随机读取
Q19. 读写分离有什么问题?主从延迟怎么处理? 必考
主从延迟问题:写入 Master 后,Slave 同步有延迟(通常毫秒级,高峰可能秒级)。写完立刻读可能读到旧数据。
解决方案
强制路由:写完后的短时间内,读操作强制路由到 Master
中间件感知:ShardingSphere 等 ORM 中间件可自动感知 GTID 位置
业务容忍:在 UI 上显示"数据更新中,可能有延迟"
半同步复制:至少一个 Slave 确认收到 Binlog 才返回成功
Q20. 分库分表有哪些策略?分片键怎么选? 必考
策略
垂直分表:按列拆分(将不常用字段移到扩展表)
水平分表:按行拆分(按分片键取模/范围分散数据)

分片键选择原则
① 数据分布均匀(避免热点)
② 查询尽量命中单分片(避免跨片查询)
③ 避免跨片 JOIN 和排序
常用分片键:user_id(C 端应用)、order_id(订单系统)、hash(id)(通用)
Q21. 雪花算法 (Snowflake) 生成的 ID 有什么特点? 中频
结构:64 bit = 1 bit 符号位 + 41 bit 时间戳 + 10 bit 机器 ID + 12 bit 序列号
特点
① 全局唯一 — 机器 ID 保证不同节点不重复
② 趋势递增 — 时间戳在高位,ID 按时间递增(有利于 B+ 树索引)
③ 高性能 — 每台机器每毫秒可生成 4096 个 ID
缺点:依赖时钟 — 时钟回拨会导致 ID 重复(解决方案:回拨检测 + 等待)
Q22. 设计一个秒杀系统,如何防止超卖? 高频
核心挑战:高并发 + 库存有限 + 不能超卖
方案
前端:按钮置灰 + 验证码防刷 + 请求随机丢弃(只放 10% 到后端)
网关层:令牌桶限流 + 用户级频率限制
服务层:Redis DECR 原子扣库存(单线程保证不超卖)
异步下单:扣完库存后发 MQ,消费者异步创建订单
兜底:数据库层用 UPDATE stock SET count=count-1 WHERE count>0 做最终防线
关键:将同步的"扣库存+创建订单"拆成异步的"扣库存(Redis) → MQ → 创建订单(DB)",削峰填谷。
Q23. 设计一个短链接系统 (TinyURL)。 经典
核心 API:createShort(longUrl) → shortUrl, getLong(shortUrl) → longUrl
短码生成
① 哈希 (MD5/MurmurHash) + Base62 编码,取前 6-7 位
② 自增 ID + Base62 编码 (1 → "1", 10000 → "2Bi")
③ 预生成短码池(提前生成一批存 Redis,用完再生成)

存储:MySQL 存映射关系,Redis 缓存热点短链
读取流程:短链请求 → 301/302 重定向到长链
扩展:分库分表(按短码 hash 分片)+ 布隆过滤器防重复
Q24. 如何设计一个高可用的消息队列系统? 中频
核心要求:不丢消息、不重复消费、高吞吐、可回溯
架构
Producer → 写入 Broker 集群 → Consumer 消费
② Broker 集群:分区 (Partition) + 多副本 (Replica) + Leader/Follower
不丢消息:Producer 等待 ISR 副本确认 (acks=all),Consumer 手动提交 offset
不重复:消费者端幂等(去重表/唯一 ID)
高可用:分区多副本 + 自动 Leader 选举(如 Kafka 的 Controller)
顺序消费:同一 Key 的消息路由到同一分区
Q25. 系统出现性能瓶颈,如何排查和优化? 必考
排查步骤 (自上而下)
前端/网关:CDN 是否命中?静态资源是否压缩?
应用层:接口响应时间分布(P50/P95/P99),是否慢 SQL?GC 是否频繁?
数据库层:慢查询日志、EXPLAIN 分析、索引是否生效?连接池是否够?
基础设施:CPU、内存、磁盘 IO、网络带宽是否打满?

常见优化手段
• 慢 SQL → 加索引 / 优化查询 / 读写分离
• 热点查询 → 加缓存 (Redis) / 本地缓存 (Caffeine)
• 数据量大 → 分库分表 / 归档历史数据
• CPU 高 → 异步化 (MQ) / 水平扩容
• 连接池满 → 增大连接池 / 引入连接复用
工具链:Prometheus + Grafana (监控) → SkyWalking (链路追踪) → Arthas (Java 诊断) → pt-query-digest (慢 SQL 分析)

系统设计与架构 | Backend Interview Prep #05

全栈面试备战库 — 持续更新中