引言
系统设计面试已经成为中高级工程师面试的标准环节。与算法题不同,系统设计没有"标准答案"——它考察的是你分析问题、权衡取舍和沟通协作的能力。
本文将从方法论入手,建立一套可复用的答题框架,然后通过 URL 短链接、聊天系统和限流器三个经典案例,演示如何在实际面试中运用这些方法。
一、系统设计面试方法论
答题框架:4 步法
code
1. 需求澄清(5-10 分钟)
├─ 功能需求:系统要做什么?
├─ 非功能需求:QPS、延迟、可用性要求?
└─ 边界条件:用户量级、数据规模、读写比例?
2. 高层设计(10-15 分钟)
├─ 画出核心组件图
├─ 确定数据流方向
└─ 选择通信协议(HTTP、WebSocket、gRPC、消息队列)
3. 深入细节(15-20 分钟)
├─ 数据模型设计
├─ 关键算法选型
├─ 扩展策略(分片、缓存、复制)
└─ 瓶颈分析与优化
4. 总结回顾(5 分钟)
├─ 系统容量估算
├─ 可能的故障点
└─ 未来演进方向
核心原则
| 原则 | 说明 |
|---|---|
| 先广度后深度 | 不要一开始就钻进某个细节,先画出整体架构 |
| 用数字说话 | 做任何技术决策前,先估算数据量和 QPS |
| 没有银弹 | 每个方案都有 trade-off,主动分析利弊 |
| 由简入繁 | 从单体 → 微服务,从单库 → 分片,逐步演进 |
| 提及监控 | 展示工程成熟度:日志、指标、告警 |
容量估算速查
面试中常用的数量级参考:
| 操作类型 | 延迟参考 |
|---|---|
| L1 缓存引用 | 0.5 ns |
| L2 缓存引用 | 7 ns |
| 主存引用 | 100 ns |
| SSD 随机读 | 100 μs |
| 内存 1MB 顺序读 | 3 μs |
| SSD 1MB 顺序读 | 1 ms |
| 同数据中心往返 | 0.5 ms |
| 跨洲网络往返 | 150 ms |
二、案例一:URL 短链接系统(TinyURL)
需求分析
功能需求:
- 用户输入长 URL,生成短 URL
- 访问短 URL 时重定向到原始长 URL
- 短链接支持自定义别名(可选)
- 短链接有过期时间(可选)
非功能需求:
- 高可用:重定向必须是实时的,延迟 < 50ms
- 高并发:假设日生成 1 亿个短链接(~1,200 QPS 写入)
- 重定向 QPS 远高于写入(约 10:1 读写比 → ~12,000 QPS 读取)
短码生成算法
这是 URL 短链接系统的核心。我们来分析几种方案:
方案 A:Hash 函数
code
长 URL → MD5/SHA256 → 取前 7 位 Base62 → 短码
优点:实现简单,同 URL 生成同短码
缺点:Hash 碰撞需要额外处理
python
import hashlib
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def generate_short_code(url: str) -> str:
hash_hex = hashlib.md5(url.encode()).hexdigest()
# 取前 8 个十六进制字符转为整数
num = int(hash_hex[:8], 16)
# 转为 Base62
chars = []
for _ in range(7):
chars.append(BASE62[num % 62])
num //= 62
return ''.join(reversed(chars))方案 B:分布式 ID 生成器
code
Snowflake ID → Base62 编码 → 短码
优点:无碰撞,可排序,包含时间戳信息
缺点:短码不固定长度
python
# 简易 Snowflake 实现
import time
class SnowflakeID:
def __init__(self, worker_id: int, datacenter_id: int):
self.worker_id = worker_id
self.datacenter_id = datacenter_id
self.sequence = 0
self.last_timestamp = -1
self.epoch = 1700000000000 # 自定义起始时间
def next_id(self) -> int:
timestamp = int(time.time() * 1000)
if timestamp < self.last_timestamp:
raise Exception('时钟回拨')
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 0xFFF
if self.sequence == 0:
while timestamp <= self.last_timestamp:
timestamp = int(time.time() * 1000)
else:
self.sequence = 0
self.last_timestamp = timestamp
return (
((timestamp - self.epoch) << 22) |
(self.datacenter_id << 17) |
(self.worker_id << 12) |
self.sequence
)数据模型
sql
CREATE TABLE short_urls (
id BIGINT PRIMARY KEY,
short_code VARCHAR(10) UNIQUE NOT NULL,
original_url TEXT NOT NULL,
user_id BIGINT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
click_count BIGINT DEFAULT 0,
INDEX idx_short_code (short_code),
INDEX idx_expires_at (expires_at)
);架构设计
code
┌──────────────┐
│ CDN/DNS │
└──────┬───────┘
│
┌──────▼───────┐
│ Load Balancer│
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌────────▼───┐ ┌─────▼────┐ ┌────▼──────┐
│ API Server │ │ API Srv │ │ API Server │
└────────┬───┘ └─────┬────┘ └────┬──────┘
│ │ │
└────────────┼────────────┘
│
┌────────────┼────────────┐
│ │ │
┌────────▼───┐ ┌─────▼─────┐ ┌───▼────────┐
│ Redis │ │ MySQL │ │ Kafka │
│ (缓存热点) │ │ (持久化) │ │ (异步统计) │
└────────────┘ └───────────┘ └────────────┘
缓存策略
python
import redis
from functools import lru_cache
cache = redis.Redis(host='localhost', port=6379, decode_responses=True)
async def resolve_short_url(short_code: str) -> str | None:
# 1. 先查本地缓存(L1)
# 2. 再查 Redis(L2)
original_url = cache.get(f"url:{short_code}")
if original_url:
return original_url
# 3. 查数据库
original_url = await db.fetch_one(
"SELECT original_url FROM short_urls WHERE short_code = ?",
short_code
)
if original_url:
# 写入 Redis,TTL 根据过期时间或默认 7 天
cache.setex(f"url:{short_code}", 604800, original_url)
return original_url
return None关键权衡
| 决策点 | 选项 A | 选项 B | 选择 |
|---|---|---|---|
| 短码生成 | Hash | ID 生成器 | ID 生成器(无碰撞) |
| 存储 | MySQL | NoSQL | MySQL(关系简单) |
| 缓存 | Redis | CDN | Redis + CDN |
| 清理策略 | 定时任务 | TTL 过期 | TTL + 懒删除 |
三、案例二:实时聊天系统
需求分析
功能需求:
- 一对一聊天和群聊
- 消息已读/未读状态
- 在线状态显示
- 历史消息查询
非功能需求:
- 消息送达延迟 < 200ms
- 支持 100 万并发连接
- 消息可靠性:不丢、不重、有序
- 已发送消息可永久存储
通信协议选型
| 协议 | 方向 | 适用场景 | 特点 |
|---|---|---|---|
| HTTP 短轮询 | 客户端 → 服务端 | 不推荐 | 浪费资源 |
| HTTP 长轮询 | 双向 | 过渡方案 | 兼容性好,但效率低 |
| WebSocket | 双向 | 推荐 | 全双工,低延迟 |
| SSE | 服务端 → 客户端 | 单向通知 | 自动重连,走 HTTP |
| WebRTC | P2P | 音视频 | NAT 穿透,UDP |
推荐:WebSocket 作为主要通道 + HTTP 用于历史消息拉取和文件上传。
架构设计
code
┌─────────┐ ┌─────────────┐
│ Client │──── WebSocket ────────►│ Gateway │
│ A │◄─── connection ───────│ Server │
└─────────┘ └──────┬───────┘
│
┌─────────┐ ┌──────▼───────┐
│ Client │──── WebSocket ────────►│ Gateway │
│ B │◄─── connection ───────│ Server │
└─────────┘ └──────┬───────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────────▼───┐ ┌──────▼────┐ ┌─────▼──────┐
│ Kafka │ │ Redis │ │ PostgreSQL │
│ (消息队列) │ │ (在线状态) │ │ (持久化存储) │
└────────────┘ └───────────┘ └────────────┘
消息可靠性保障
typescript
// 客户端消息确认机制
interface ChatMessage {
id: string; // 客户端生成的唯一 ID
senderId: string;
receiverId: string;
content: string;
timestamp: number;
sequenceNo: number; // 会话级递增序列号
}
class ChatClient {
private pendingMessages: Map<string, ChatMessage> = new Map();
private lastAckedSeq: Map<string, number> = new Map();
async sendMessage(message: ChatMessage): Promise<void> {
// 1. 乐观地先显示消息
this.displayMessage(message, 'sending');
// 2. 存入待确认队列
this.pendingMessages.set(message.id, message);
// 3. 通过 WebSocket 发送
this.ws.send(JSON.stringify({ type: 'message', payload: message }));
// 4. 设置超时重发(指数退避)
this.scheduleRetry(message.id, 0);
}
private scheduleRetry(messageId: string, attempt: number): void {
const timeout = Math.min(1000 * Math.pow(2, attempt), 30000);
setTimeout(() => {
if (this.pendingMessages.has(messageId)) {
const msg = this.pendingMessages.get(messageId)!;
this.ws.send(JSON.stringify({ type: 'message', payload: msg }));
this.scheduleRetry(messageId, attempt + 1);
}
}, timeout);
}
handleAck(messageId: string): void {
const message = this.pendingMessages.get(messageId);
if (message) {
this.displayMessage(message, 'sent');
this.pendingMessages.delete(messageId);
}
}
}消息持久化策略
code
消息写入路径(保证不丢):
Client → Gateway → Kafka → Message Service → PostgreSQL + 推送
│ │
└── 持久化到 Kafka 即返回 ACK ◄────────┘
异步写入 DB + 推送给接收方
优势:
- Kafka 保证消息持久化和顺序
- 解耦网关与存储层
- 支持消息回放和多个消费者
一致性哈希分配用户连接
python
import hashlib
class ConsistentHash:
def __init__(self, servers: list[str], virtual_nodes: int = 150):
self.virtual_nodes = virtual_nodes
self.ring: dict[int, str] = {}
self.sorted_keys: list[int] = []
for server in servers:
self._add_server(server)
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def _add_server(self, server: str):
for i in range(self.virtual_nodes):
node_key = f"{server}:vnode:{i}"
hash_val = self._hash(node_key)
self.ring[hash_val] = server
self.sorted_keys.append(hash_val)
self.sorted_keys.sort()
def get_server(self, user_id: str) -> str:
"""查找用户连接应该路由到哪个服务器"""
hash_val = self._hash(user_id)
for key in self.sorted_keys:
if hash_val <= key:
return self.ring[key]
return self.ring[self.sorted_keys[0]]四、案例三:分布式限流器
算法选型
| 算法 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口 | 每个时间窗口内固定次数 | 实现简单 | 边界突发问题 |
| 滑动窗口 | 平滑的时间窗口 | 均匀限流 | 内存开销较大 |
| 漏桶 | 恒定速率输出 | 平滑流量 | 应对突发能力差 |
| 令牌桶 | 固定速率生成令牌 | 允许合理突发 | 实现稍复杂 |
推荐:令牌桶算法——既能平滑限流,又允许合理突发流量。
令牌桶实现
python
import time
import asyncio
from dataclasses import dataclass
@dataclass
class TokenBucket:
capacity: int # 桶容量
refill_rate: float # 每秒填充令牌数
tokens: float = 0.0
last_refill: float = 0.0
def __post_init__(self):
self.tokens = float(self.capacity)
self.last_refill = time.monotonic()
def _refill(self):
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
def allow_request(self, tokens: int = 1) -> bool:
self._refill()
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False分布式限流:Redis + Lua
lua
-- rate_limiter.lua
-- KEYS[1]: 令牌桶 key
-- ARGV[1]: 桶容量
-- ARGV[2]: 每秒填充速率
-- ARGV[3]: 本次请求需要的令牌数
local bucket_key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = redis.call('TIME')[1] -- 秒级时间戳
local bucket = redis.call('HMGET', bucket_key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- 补充令牌
local elapsed = now - last_refill
if elapsed > 0 then
tokens = math.min(capacity, tokens + elapsed * refill_rate)
end
-- 判断是否可以放行
if tokens >= requested then
tokens = tokens - requested
redis.call('HMSET', bucket_key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', bucket_key, 60)
return 1 -- 放行
else
redis.call('HMSET', bucket_key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', bucket_key, 60)
return 0 -- 限流
endpython
import redis.asyncio as redis
class DistributedRateLimiter:
def __init__(self, redis_client: redis.Redis):
self.redis = redis_client
# 加载 Lua 脚本
with open('rate_limiter.lua') as f:
self.script = self.redis.register_script(f.read())
async def is_allowed(
self,
user_id: str,
endpoint: str,
capacity: int = 100,
refill_rate: float = 10.0,
tokens: int = 1,
) -> bool:
key = f"rate_limit:{user_id}:{endpoint}"
result = await self.script(
keys=[key],
args=[capacity, refill_rate, tokens],
)
return result == 1多级限流架构
code
┌──────────────────┐
│ API Gateway │
│ (全局限流: IP级) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Service Mesh │
│ (服务限流: 接口级) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Application │
│ (业务限流: 用户级) │
└──────────────────┘
五、面试常见问题与应对
"系统宕机了怎么办?"
展示你考虑了高可用设计:
- 冗余:每个服务至少 2 个副本,跨可用区部署
- 故障转移:数据库主从切换、负载均衡器健康检查
- 熔断与降级:非核心功能在高峰期降级,核心链路保证可用
- 监控与告警:Prometheus + Grafana + PagerDuty
"系统如何扩展?"
按以下维度逐一分析:
- 读扩展:加缓存(CDN → Redis → 本地缓存),加只读副本
- 写扩展:数据分片(按 user_id、按时间等维度)
- 混合扩展:CQRS 读写分离
"数据库怎么选?"
| 需求 | 推荐 | 原因 |
|---|---|---|
| 关系型、事务 | PostgreSQL / MySQL | ACID,JOIN,成熟工具 |
| 文档存储、灵活 Schema | MongoDB | JSON 原生支持 |
| 键值、高并发读写 | Redis / DynamoDB | 极低延迟 |
| 时序数据 | TimescaleDB / InfluxDB | 时间窗口优化 |
| 全文搜索 | Elasticsearch | 倒排索引 |
| 图关系 | Neo4j | 图遍历优化 |
结语
系统设计面试考察的不是你能记住多少架构图,而是你分析问题的思维方式。
四个关键能力:
- 需求驱动设计——先理解约束(QPS、数据量、延迟要求),再推导架构
- 渐进式思考——从最简单的可行方案出发,逐步升级复杂度
- 量化权衡——用数据支撑你的技术选型,"SQL 比 NoSQL 更合适,因为……"
- 全局视角——不只停留在写代码,考虑部署、监控、扩展和故障恢复
建议每周选择一个系统(消息队列、分布式文件系统、推荐引擎等)进行设计练习,在 45 分钟内完成从需求到架构的全过程。练习中保持出声思考,因为面试官想看到的不仅是你的最终方案,更是你的思考过程。
好的系统设计,是工程约束下做出的一系列合理权衡。