系统设计面试题 —— 从分布式基础到高可用架构的深度问答
覆盖 CAP 定理、一致性协议(Paxos/Raft)、分布式锁、限流、缓存策略、消息队列、微服务、负载均衡、高可用设计十大核心主题,30 道高频面试题附架构图解
系统设计是高级工程师面试的”分水岭”——它考察的不是你能不能写代码,而是你能不能把多个组件组合成一个可靠、可扩展的系统。很多候选人算法写得很好,但系统设计一开口就暴露经验不足。
这篇文章的组织思路:每道题先给出”一句话记忆点”,再展开原理和架构思考,最后给出面试回答要点。侧重”为什么这么设计”而非死记硬背。
第一部分:分布式系统基础
Q1:什么是 CAP 定理?
记忆点:分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者最多同时满足两个。由于网络分区不可避免,实际只能在 C 和 A 之间取舍。
| 属性 | 含义 | 例子 |
|---|---|---|
| C(一致性) | 所有节点在同一时刻看到相同的数据 | 写入后立即读到最新值 |
| A(可用性) | 每个请求都能收到响应(不保证是最新数据) | 系统不会拒绝服务 |
| P(分区容错) | 网络分区时系统仍能工作 | 部分节点之间断网,系统不瘫痪 |
1
2
3
4
5
6
7
8
9
10
11
12
实际系统的选择:
CP 系统:牺牲可用性保一致性
→ ZooKeeper、etcd、HBase
→ 网络分区时拒绝部分请求,确保数据一致
AP 系统:牺牲一致性保可用性
→ Cassandra、DynamoDB、Eureka
→ 网络分区时允许读到旧数据,但保证服务可用
CA 系统:只在单机/无分区环境存在
→ 传统单机数据库(MySQL 单实例)
→ 分布式环境下 P 不可避免,CA 不现实
面试加分: CAP 不是非此即彼的”三选二”,实际上是在一致性和可用性之间做连续的权衡。大多数系统追求”最终一致性”而非强一致性。
Q2:什么是 BASE 理论?和 ACID 什么关系?
记忆点:BASE 是对 CAP 中 AP 方向的延伸——基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventually Consistent)。ACID 追求强一致性,BASE 追求高可用+最终一致。
| 维度 | ACID | BASE |
|---|---|---|
| 一致性模型 | 强一致性 | 最终一致性 |
| 适用场景 | 单机数据库、金融交易 | 分布式系统、互联网应用 |
| 可用性 | 可能因加锁降低 | 优先保证可用 |
| 典型系统 | MySQL InnoDB | DynamoDB、Cassandra |
最终一致性的常见实现:
- 读时修复(Read Repair):读到不一致数据时触发修复
- 写时修复(Write Repair / Anti-Entropy):后台异步比对修复
- 异步复制 + 冲突解决(Last Write Wins / Vector Clock)
Q3:什么是一致性哈希?它解决了什么问题?
记忆点:普通哈希取模(hash % N)在节点增减时几乎所有数据都要迁移。一致性哈希把节点和数据映射到同一个环上,节点变化时只影响相邻数据,迁移量为 1/N。虚拟节点解决数据倾斜。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
一致性哈希环:
0
/ \
Node-A ● ● Node-B
/ \
key1 ● ● key3
| |
key2 ● ● Node-C
\ /
● ●
key4 key5
规则:数据沿顺时针方向找到的第一个节点就是它的归属节点。
key1, key2 → Node-A
key3 → Node-B
key4, key5 → Node-C
新增 Node-D(在 Node-A 和 Node-B 之间):
只有原本属于 Node-B 的部分数据迁移到 Node-D,其他不变。
虚拟节点: 物理节点少时数据可能分布不均。解决方法:每个物理节点映射多个虚拟节点到环上(比如 Node-A 映射为 Node-A-1, Node-A-2, …, Node-A-150),让数据更均匀分布。
第二部分:一致性协议
Q4:Raft 共识算法的核心思想?
记忆点:Raft 把共识问题分解为三个子问题——Leader 选举、日志复制、安全性。任意时刻最多一个 Leader,所有写操作经 Leader,Leader 把日志复制到多数节点后提交。
1
2
3
4
5
6
7
8
9
10
11
Raft 角色:
Leader → 处理所有客户端请求,复制日志到 Follower
Follower → 被动接收 Leader 的日志复制
Candidate → Follower 超时没收到心跳时变成 Candidate 发起选举
Leader 选举流程:
1. Follower 的选举超时计时器到期
2. 变成 Candidate,任期号 +1,投票给自己
3. 向其他节点发 RequestVote
4. 获得多数票 → 成为 Leader
5. Leader 定期发送心跳(空 AppendEntries)维持权威
日志复制流程:
1
2
3
4
5
客户端请求 → Leader 追加日志条目
Leader → 发 AppendEntries RPC 到所有 Follower
多数 Follower 确认 → Leader 提交日志(apply 到状态机)
Leader 通知 Follower 提交
→ 返回客户端成功
面试加分: Raft 的安全性保证——选举时 Candidate 的日志必须至少和大多数节点一样新(lastLogTerm 更大,或 term 相同但 lastLogIndex 更大),否则不会被选为 Leader。这保证了新 Leader 一定包含所有已提交的日志。
Q5:Paxos 和 Raft 的区别?
记忆点:Paxos 是理论基础(Lamport 提出),正确但难以实现和理解;Raft 是工程友好的简化版本,明确 Leader 角色让理解和实现更容易。实际工程中用 Raft 更多(etcd、Consul)。
| 维度 | Paxos | Raft |
|---|---|---|
| 提出者 | Lamport (1998) | Diego Ongaro (2014) |
| 可理解性 | 难(论文公认难懂) | 设计目标就是易懂 |
| Leader | 可有可无(Multi-Paxos 有) | 强制 Leader |
| 日志 | 允许空洞(乱序提交) | 连续,不允许空洞 |
| 典型实现 | Chubby (Google) | etcd, Consul, TiKV |
Q6:ZooKeeper 是什么?它能做什么?
记忆点:ZooKeeper 是分布式协调服务,基于 ZAB 协议(类似 Paxos),提供配置管理、服务发现、分布式锁、Leader 选举等能力。核心数据模型是一棵树(ZNode)。
1
2
3
4
5
6
ZooKeeper 常见用途:
1. 配置中心:各服务 watch 配置节点,配置变化自动通知
2. 服务发现:服务启动时创建临时节点,宕机自动删除
3. 分布式锁:创建临时顺序节点 + watch 前一个节点
4. Leader 选举:多个节点创建临时顺序节点,序号最小的是 Leader
5. 集群管理:监控节点上下线
第三部分:分布式锁
Q7:分布式锁有哪些实现方案?
记忆点:三种主流方案——Redis(性能高但有可靠性风险)、ZooKeeper(可靠但性能略低)、数据库(简单但不推荐高并发场景)。
| 方案 | 实现原理 | 优点 | 缺点 |
|---|---|---|---|
| Redis | SET key uuid NX PX timeout | 性能高、实现简单 | 主从切换可能丢锁 |
| ZooKeeper | 创建临时顺序节点 | 可靠、有 watch 机制 | 性能比 Redis 低 |
| 数据库 | INSERT ... ON DUPLICATE KEY 或 FOR UPDATE | 简单 | 性能差、不适合高并发 |
Q8:Redis 分布式锁怎么实现?有什么坑?
记忆点:SET key uuid NX PX 30000 加锁,Lua 脚本比较 uuid 后删除释放锁。核心问题——锁过期了但业务还没执行完(用看门狗续期),Redis 主从切换丢锁(用 Redlock)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
加锁:
SET resource_lock <uuid> NX PX 30000
→ NX: 不存在才设置(互斥)
→ PX: 过期时间(防止死锁)
→ uuid: 唯一标识(防止误删别人的锁)
释放锁(Lua 脚本保证原子性):
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
为什么要用 uuid?
→ 防止 A 的锁过期后 B 拿到锁,A 执行完后误删 B 的锁
两大难题:
- 锁过期问题:业务执行时间超过锁的过期时间 → 看门狗机制(Redisson),后台线程每隔 1/3 过期时间续期一次
- Redis 集群可靠性:Master 挂了,锁还没同步到 Slave,Slave 升主后锁丢失 → Redlock 算法(向 N 个独立 Redis 节点加锁,多数成功才算成功)
Q9:Redlock 算法靠谱吗?
记忆点:Redlock 是 Redis 作者 antirez 提出的分布式锁算法,向 N(通常5)个独立 Redis 节点加锁,多数成功且总耗时小于锁过期时间才算成功。Martin Kleppmann 质疑其安全性(时钟跳跃、GC 暂停),antirez 回应但争议未完全解决。
1
2
3
4
5
6
7
Redlock 流程(5 个 Redis 节点):
1. 获取当前时间 T1
2. 依次向 5 个节点发 SET key uuid NX PX 30000
3. 获取当前时间 T2
4. 如果至少 3 个节点加锁成功 且 T2-T1 < 锁过期时间
→ 加锁成功,实际过期时间 = 原始过期时间 - (T2-T1)
5. 否则向所有节点释放锁
实际建议: 如果对一致性要求极高(比如金融场景),用 ZooKeeper/etcd 实现分布式锁;如果是防重、限流等容忍极低概率失败的场景,Redis 单节点锁即可。
第四部分:限流
Q10:常见的限流算法有哪些?
记忆点:四种——固定窗口(简单但有边界突发)、滑动窗口(精确但内存大)、漏桶(恒定速率)、令牌桶(允许突发,最常用)。
固定窗口计数器
1
2
3
4
5
|---窗口1(100次)---|---窗口2(100次)---|
0s 60s 120s
缺点:窗口边界处可能出现两倍流量
→ 窗口1的最后1秒 100 次 + 窗口2的第1秒 100 次 = 1秒内 200 次
滑动窗口计数器
1
2
3
4
5
6
|←------60s窗口------→|
-----|----|----|----|-------|---
统计这个范围内的请求数
每次请求检查过去60秒内的请求数,超过阈值则拒绝
解决了固定窗口的边界突发问题
漏桶算法
1
2
3
4
5
6
7
8
9
10
请求流入
↓
+============+
| 漏 桶 | ← 桶满则拒绝
| (队列) |
+============+
↓
恒定速率流出 ← 不管流入多快,流出速率恒定
特点:平滑流量,但无法应对合理的突发请求
令牌桶算法(推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
令牌以固定速率放入桶中
↓
+============+
| 令牌桶 | ← 桶满则丢弃新令牌
| (存令牌) |
+============+
↓
请求来了取一个令牌,取到才放行
特点:
- 桶中有积攒的令牌 → 允许突发请求
- 桶空了 → 限流
- 限流上限 = 桶容量(最大突发量)
面试加分: Nginx 用漏桶(limit_req),Guava RateLimiter 用令牌桶(SmoothBursty),Sentinel 用滑动窗口。
Q11:分布式限流怎么做?
记忆点:单机限流用内存计数器即可,分布式限流需要集中式存储——通常用 Redis + Lua 脚本实现原子性操作。
1
2
3
4
5
6
Redis 滑动窗口限流(Lua 脚本):
1. ZADD key timestamp timestamp(用有序集合记录每次请求的时间戳)
2. ZREMRANGEBYSCORE key 0 (now - window)(清理窗口外的记录)
3. ZCARD key(统计窗口内请求数)
4. 如果 < 阈值则放行,否则拒绝
5. EXPIRE key window(设置过期时间)
第五部分:缓存策略
Q12:缓存的常见模式有哪些?
记忆点:Cache Aside(旁路缓存,最常用)、Read/Write Through(穿透读写)、Write Behind(异步写回)。
| 模式 | 读 | 写 | 特点 |
|---|---|---|---|
| Cache Aside | 先查缓存→未命中查 DB→回填缓存 | 先更新 DB→删除缓存 | 最常用,应用层控制 |
| Read Through | 缓存未命中时由缓存层自动加载 | — | 对应用透明 |
| Write Through | — | 同步写缓存和 DB | 数据一致但写入延迟高 |
| Write Behind | — | 先写缓存,异步批量写 DB | 写入性能好但可能丢数据 |
Q13:多级缓存架构怎么设计?
记忆点:L1 本地缓存(Caffeine/Guava,微秒级)→ L2 分布式缓存(Redis,毫秒级)→ L3 数据库。本地缓存容量小但极快,分布式缓存容量大但有网络开销。
1
2
3
4
5
6
7
8
9
10
11
12
13
请求 → L1 本地缓存(进程内,Caffeine)
↓ miss
L2 Redis(分布式,毫秒级)
↓ miss
L3 数据库(MySQL)
↓ 回填
写入 L2 → 写入 L1 → 返回
注意事项:
1. L1 缓存的一致性:多个应用实例的本地缓存可能不一致
→ 用 Redis Pub/Sub 或 MQ 通知其他实例清除本地缓存
2. L1 容量有限,只缓存热点数据
3. L1 过期时间要短(比如 30 秒),L2 过期时间可以长些
Q14:热点 Key 问题怎么解决?
记忆点:单个 Key 的访问量极高,导致 Redis 单节点压力过大。方案:本地缓存兜底、Key 分片(加后缀打散到多个 Key)、永不过期+逻辑过期。
1
2
3
4
5
6
7
8
9
10
11
12
方案一:本地缓存
热点 Key 缓存在应用本地(L1),多数请求不打 Redis
方案二:Key 分片
将 hot_key 拆分为 hot_key_1, hot_key_2, ..., hot_key_8
读取时随机选一个,写入时全部更新
→ 请求分散到 Redis 集群的不同节点
方案三:永不过期 + 异步更新
Key 不设 TTL,value 中带逻辑过期时间
读取时发现逻辑过期 → 异步线程更新缓存
→ 避免缓存击穿
第六部分:消息队列
Q15:为什么需要消息队列?
记忆点:三大作用——解耦(系统间不直接调用)、异步(非关键操作异步处理)、削峰(流量高峰期缓冲请求)。
1
2
3
4
5
6
7
8
不用 MQ(同步调用,强耦合):
下单 → 扣库存 → 生成订单 → 发短信 → 发邮件 → 推优惠券
(链路长、耦合重、一个挂全挂)
用 MQ(异步解耦):
下单 → 扣库存 → 生成订单 → 发消息到 MQ → 返回成功
↓
短信服务 / 邮件服务 / 优惠券服务 各自消费
Q16:Kafka 的架构和核心概念?
记忆点:Kafka = 分布式日志系统。核心概念——Topic(消息分类)、Partition(分区,并行度)、Offset(消费位点)、Consumer Group(消费组,一个分区同组只一个消费者)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Kafka 架构:
Producer → Topic(多个 Partition)→ Consumer Group
├─ Consumer1(消费 Partition 0,1)
└─ Consumer2(消费 Partition 2,3)
Partition 0: [msg0, msg1, msg2, msg3, ...] → 顺序写入,不可变
Partition 1: [msg0, msg1, msg2, ...]
Partition 2: [msg0, msg1, ...]
关键特点:
1. 分区内有序,分区间无序
2. 消息持久化到磁盘(顺序写,性能极高)
3. 消费者拉模式(Pull),消费者记录 Offset
4. 支持消费者组:同组内一个分区只能被一个消费者消费
Q17:如何保证消息不丢失?
记忆点:三个环节都可能丢消息——生产端(发送失败)、Broker 端(宕机丢数据)、消费端(消费后未确认就崩了)。每个环节都要有保障。
| 环节 | 丢失原因 | 解决方案 |
|---|---|---|
| 生产端 | 网络抖动发送失败 | 重试 + acks=all(Kafka)/ publisher confirm(RabbitMQ) |
| Broker | 宕机,数据在内存未刷盘 | 同步刷盘 或 多副本(Kafka: min.insync.replicas=2) |
| 消费端 | 消费后 ack 前崩溃 | 手动 ack(处理完业务再确认),而非自动 ack |
Kafka 不丢消息配置:
1
2
3
4
5
6
7
8
9
10
Producer:
acks=all # Leader 和所有 ISR 都写入才确认
retries=3 # 失败重试
Broker:
replication.factor=3 # 3 副本
min.insync.replicas=2 # 至少 2 个 ISR 同步
Consumer:
enable.auto.commit=false # 手动提交 offset
Q18:如何保证消息的顺序性?
记忆点:全局有序很难(性能差),通常只需局部有序——把需要保序的消息发到同一个分区(按业务 Key 路由)。
1
2
3
4
5
场景:订单状态变更必须有序 → 创建 → 支付 → 发货 → 签收
方案:按 order_id 做 Partition Key
hash(order_id) % partitionCount → 同一订单的所有消息进同一分区
同一分区内消息有序 → 消费者按顺序消费
Q19:如何处理重复消费(幂等性)?
记忆点:网络抖动/重试可能导致同一条消息被消费多次。解决方案——消费端做幂等,而不是靠 MQ 保证 exactly-once。
1
2
3
4
5
6
7
8
9
10
11
12
13
幂等性方案:
1. 唯一 ID + 去重表
→ 消息带唯一 ID,消费前查是否已处理
→ INSERT INTO processed_msgs(msg_id) ... ON DUPLICATE KEY IGNORE
2. 数据库唯一约束
→ 利用唯一索引防止重复插入
3. 乐观锁 / 版本号
→ UPDATE ... SET status=paid, version=version+1 WHERE id=? AND version=?
4. Redis Set 去重
→ SADD processed:msg_id → 返回 1(新消息)/ 0(已处理过)
第七部分:微服务
Q20:微服务和单体架构各自的优缺点?
记忆点:微服务不是银弹。小团队用单体更高效,团队大了/业务复杂了才需要微服务。微服务的核心价值是”独立部署、独立扩展、技术异构”。
| 维度 | 单体 | 微服务 |
|---|---|---|
| 开发复杂度 | 低(一个代码库) | 高(多服务协调、分布式事务) |
| 部署 | 整体部署 | 独立部署 |
| 扩展 | 整体扩展(浪费) | 按需扩展单个服务 |
| 技术栈 | 统一 | 可异构(Java、Go、Python 各用各的) |
| 团队 | 适合小团队(< 10 人) | 适合大团队(按服务划分) |
| 运维 | 简单 | 复杂(需要服务发现、链路追踪、配置中心等基础设施) |
Q21:服务发现怎么做?
记忆点:服务实例动态变化(弹性扩缩容),调用方需要知道可用实例地址。两种方式——客户端发现(Consumer 从注册中心拉列表自己选)、服务端发现(经过 LB/Gateway 路由)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
客户端发现(Eureka/Nacos):
Service-A → 注册中心 → 拿到 Service-B 的实例列表
→ 自己做负载均衡(Ribbon)
→ 直接调用 Service-B 实例
服务端发现(Kubernetes Service / Nginx):
Service-A → Load Balancer / API Gateway → Service-B
↑
注册中心(或 DNS)
常见注册中心对比:
Eureka: AP,Netflix 出品,适合 Spring Cloud
Nacos: AP/CP 可切换,阿里出品,支持配置中心
Consul: CP,HashiCorp 出品,Go 实现
etcd: CP,Kubernetes 使用,Raft 协议
Q22:微服务之间如何通信?
记忆点:同步调用(HTTP/gRPC)适合需要立即响应的场景;异步消息(MQ)适合解耦和削峰。gRPC 比 HTTP/JSON 性能好,适合内部高频调用。
| 方式 | 协议 | 序列化 | 适用场景 |
|---|---|---|---|
| RESTful HTTP | HTTP/1.1 | JSON | 对外 API、简单内部调用 |
| gRPC | HTTP/2 | Protobuf(二进制) | 内部高频调用、流式通信 |
| 消息队列 | AMQP/Kafka | 自定义 | 异步解耦、事件驱动 |
Q23:什么是服务熔断?和降级有什么区别?
记忆点:熔断 = “保险丝”,当下游服务故障率过高时自动切断调用,防止级联雪崩。降级 = “退而求其次”,返回兜底数据或简化逻辑。熔断是自动触发的,降级可以是主动策略。
1
2
3
4
5
6
7
8
9
10
11
12
13
熔断器三种状态(Circuit Breaker):
Closed(正常)→ 失败率超阈值 → Open(熔断,快速失败)
↓ 超时后
Half-Open(试探性放行少量请求)
↓ 成功率达标
Closed(恢复正常)
↓ 仍然失败
Open(继续熔断)
降级策略示例:
- 推荐服务不可用 → 返回热门商品列表(兜底数据)
- 评论服务超时 → 不显示评论区,其他功能正常
- 搜索服务降级 → 从本地缓存返回结果
常用工具: Hystrix(Netflix,已停止维护)、Resilience4j(推荐)、Sentinel(阿里)。
第八部分:负载均衡
Q24:负载均衡有哪些算法?
记忆点:轮询(Round Robin)、加权轮询、随机、最少连接数、一致性哈希、IP 哈希。
| 算法 | 原理 | 适用场景 |
|---|---|---|
| 轮询 | 依次分配 | 服务器配置相同 |
| 加权轮询 | 按权重比例分配 | 服务器配置不同 |
| 随机 | 随机选一个 | 大量请求下趋近均匀 |
| 最少连接 | 选当前连接最少的 | 请求处理时间差异大 |
| 一致性哈希 | 按 Key 哈希到固定节点 | 有状态服务、缓存 |
| IP 哈希 | 同一 IP 固定到同一节点 | 会话保持 |
Q25:四层负载均衡和七层负载均衡的区别?
记忆点:四层在传输层(TCP/UDP),只转发连接/数据包,性能高;七层在应用层(HTTP),能解析 URL/Header,灵活路由。
| 维度 | 四层(L4) | 七层(L7) |
|---|---|---|
| 工作层 | 传输层(TCP/UDP) | 应用层(HTTP/HTTPS) |
| 路由依据 | IP + Port | URL、Header、Cookie |
| 性能 | 高(不解析内容) | 较低(需要解析 HTTP) |
| 功能 | 简单转发 | URL 路由、请求改写、SSL 终止 |
| 代表 | LVS、F5 | Nginx、HAProxy、ALB |
第九部分:高可用设计
Q26:如何设计一个高可用系统?
记忆点:高可用 = 消除单点故障 + 故障检测 + 自动恢复。核心手段:冗余(多副本/多实例)、故障转移(自动切换)、降级限流(保核心链路)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
高可用设计清单:
1. 无单点故障
→ 服务多实例部署,数据库主从/多副本
→ 负载均衡器本身也要高可用(Keepalived + VIP)
2. 故障检测与自动转移
→ 健康检查(HTTP/TCP),失败自动摘除
→ 数据库主从自动切换(MHA/Orchestrator)
→ Kubernetes 自动重启/调度
3. 容灾
→ 多机房/多可用区部署
→ 异地多活(同城双活、两地三中心)
4. 保护机制
→ 限流(令牌桶)、熔断(Sentinel)、降级(兜底数据)
→ 超时控制(connect timeout + read timeout)
5. 可观测性
→ 日志(ELK)、指标(Prometheus+Grafana)、链路追踪(Jaeger/SkyWalking)
→ 告警(PagerDuty/飞书机器人)
Q27:什么是异地多活?怎么做?
记忆点:多个数据中心同时提供服务(而非传统的主从备份),任一机房故障不影响整体服务。核心难点是数据同步。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
常见架构:
同城双活:
机房A ←→ 机房B (专线连接,延迟 < 2ms)
流量按用户维度分片,A/B 各处理一半
数据双向同步
两地三中心:
同城: 机房A(主) + 机房B(热备,同步复制)
异地: 机房C(灾备,异步复制)
正常时 A 服务,A 挂了切 B(RPO=0),AB 都挂了切 C(可能丢少量数据)
实施要点:
1. 数据分片:按用户 ID 将数据划分到不同机房,避免跨机房写入
2. 数据同步:DTS(数据传输服务)做双向同步,处理冲突
3. 流量调度:DNS / GSLB 按地域路由
4. 单元化:每个机房是独立单元,包含完整的服务和数据
Q28:超时和重试怎么设计?
记忆点:超时必须设(避免线程无限等待),重试要有上限和退避策略(避免重试风暴)。非幂等操作不能随意重试。
1
2
3
4
5
6
7
8
9
10
11
超时设计:
连接超时(connect timeout):通常 1-3 秒
读超时(read timeout):根据下游服务的 P99 延迟设置,通常 3-10 秒
总超时(总链路超时):各环节超时之和不能超过用户等待上限
重试设计:
1. 重试次数有限(通常 2-3 次)
2. 指数退避(Exponential Backoff):第 1 次 100ms,第 2 次 200ms,第 3 次 400ms
3. 加随机抖动(Jitter):防止多个客户端同时重试(惊群效应)
4. 幂等性检查:非幂等操作(如扣款)不重试,或做幂等改造后重试
5. 熔断配合:当重试率过高时触发熔断,停止重试
第十部分:经典系统设计题
Q29:设计一个短链系统(URL Shortener)
记忆点:这是面试最经典的系统设计题。核心思路——长链 → 生成唯一短码 → 存映射关系 → 短码 → 302 重定向到长链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
API 设计:
POST /api/shorten { "url": "https://..." } → { "short_url": "https://s.cn/abc123" }
GET /abc123 → 302 redirect to original URL
短码生成方案:
方案一:哈希(MD5/MurmurHash)→ 取前 6-8 位 Base62 编码
优点:简单,同一长链生成相同短码
缺点:哈希冲突需要处理
方案二:自增 ID → Base62 编码
优点:无冲突,有序
缺点:分布式环境需要全局唯一 ID(Snowflake)
方案三:预生成 Key 池(KGS)
提前生成大量唯一短码存入数据库,用一个取一个
Base62 编码:[0-9a-zA-Z],6 位 = 62^6 ≈ 568 亿种组合
架构:
Client → API Server → Redis 缓存 → MySQL
↓
短码生成服务(Snowflake ID + Base62)
读写比极高(100:1),重点优化读:
→ Redis 缓存热点短链映射
→ 301 vs 302:301 浏览器缓存,减少服务器压力但无法统计点击量;302 每次经过服务器,能统计
Q30:设计一个秒杀系统
记忆点:秒杀的核心挑战是”瞬间高并发 + 库存不超卖”。核心思路——多级过滤(前端→CDN→网关→应用层→Redis 扣库存)+ Redis 原子扣减 + 异步下单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
分层过滤架构:
前端 → CDN 静态资源
↓ 按钮点亮时才能点击,防止重复提交
网关层 → 限流(令牌桶)+ 用户去重
↓ 过滤 90% 请求
应用层 → Redis 库存预扣减(Lua 脚本原子操作)
↓ 只有扣减成功的请求继续
消息队列 → 异步下单
↓
订单服务 → MySQL 落单 → 支付
Redis 库存扣减(Lua 脚本):
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
end
防超卖要点:
1. Redis 预加载库存,Lua 脚本保证原子性
2. 数据库层乐观锁兜底:UPDATE stock SET count=count-1 WHERE id=? AND count>0
3. 一个用户一个商品只能下一单(Redis Set 去重)
高可用要点:
1. 静态资源全走 CDN
2. 接入层限流(QPS 上限)
3. 服务降级:非核心功能关闭
4. 独立部署:秒杀服务单独集群,不影响主站
面试高频追问
Q:如何估算系统容量(Capacity Estimation)?
面试中系统设计题通常需要先做容量估算。掌握这个公式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
日活用户 DAU × 每人每天操作次数 = 日请求量
日请求量 / 86400 = 平均 QPS
峰值 QPS = 平均 QPS × 峰值系数(通常 2-5 倍)
存储估算:
单条数据大小 × 日新增量 × 保留天数 = 总存储需求
带宽估算:
QPS × 每请求大小 = 带宽需求
示例(短链系统):
写入: 1000 万条/天 → 115 QPS → 峰值 350 QPS
读取: 10 亿次/天 → 11574 QPS → 峰值 35000 QPS
存储: 100 bytes × 1000万 × 365天 × 5年 = 1.8 TB
Q:数据库选型怎么考虑?SQL vs NoSQL?
| 维度 | SQL(MySQL/PostgreSQL) | NoSQL |
|---|---|---|
| 数据模型 | 结构化、关系型 | 灵活(KV/文档/列族/图) |
| 事务 | ACID | 通常最终一致性(部分支持事务) |
| 扩展 | 垂直扩展为主(分库分表复杂) | 天生水平扩展 |
| 查询 | SQL 灵活查询 | 查询能力有限 |
| 适用场景 | 复杂查询、事务要求高 | 大数据量、高吞吐、灵活 Schema |
1
2
3
4
5
6
7
常见 NoSQL 选型:
Redis → 缓存、计数器、排行榜、分布式锁
MongoDB → 文档存储、灵活 Schema、快速迭代
Cassandra → 大规模写入、时序数据、日志
HBase → 大数据分析、海量稀疏表
Neo4j → 社交关系、推荐系统
Elasticsearch → 全文搜索、日志分析
Q:如何面对系统设计面试?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
回答框架(SDLC):
1. 需求澄清(5 分钟)
→ 功能需求 + 非功能需求(QPS、延迟、可用性)
→ 估算规模(用户量、数据量、读写比)
2. 高层设计(10 分钟)
→ 画出核心组件和数据流
→ API 设计
3. 详细设计(15 分钟)
→ 数据库 Schema 设计
→ 关键组件深入(缓存策略、消息队列、分片方案)
4. 扩展与优化(10 分钟)
→ 瓶颈分析 + 扩展方案
→ 高可用、容灾
常见陷阱:
× 一上来就画细节
× 不和面试官沟通需求
× 过度设计(杀鸡用牛刀)
× 只说方案不说 trade-off