文章

系统设计面试题 —— 从分布式基础到高可用架构的深度问答

覆盖 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 追求高可用+最终一致。

维度ACIDBASE
一致性模型强一致性最终一致性
适用场景单机数据库、金融交易分布式系统、互联网应用
可用性可能因加锁降低优先保证可用
典型系统MySQL InnoDBDynamoDB、Cassandra

最终一致性的常见实现:

  1. 读时修复(Read Repair):读到不一致数据时触发修复
  2. 写时修复(Write Repair / Anti-Entropy):后台异步比对修复
  3. 异步复制 + 冲突解决(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)。

维度PaxosRaft
提出者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(可靠但性能略低)、数据库(简单但不推荐高并发场景)。

方案实现原理优点缺点
RedisSET key uuid NX PX timeout性能高、实现简单主从切换可能丢锁
ZooKeeper创建临时顺序节点可靠、有 watch 机制性能比 Redis 低
数据库INSERT ... ON DUPLICATE KEYFOR 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 的锁

两大难题:

  1. 锁过期问题:业务执行时间超过锁的过期时间 → 看门狗机制(Redisson),后台线程每隔 1/3 过期时间续期一次
  2. 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 HTTPHTTP/1.1JSON对外 API、简单内部调用
gRPCHTTP/2Protobuf(二进制)内部高频调用、流式通信
消息队列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 + PortURL、Header、Cookie
性能高(不解析内容)较低(需要解析 HTTP)
功能简单转发URL 路由、请求改写、SSL 终止
代表LVS、F5Nginx、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
本文由作者按照 CC BY 4.0 进行授权