mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2041 字
5 分钟
系统设计面试题
2024-07-12

系统设计是面试中的重难点,考察候选人的架构思维、技术广度和工程实践经验。本文整理了常见的高频系统设计题目,并提供详细的解题思路。

一、短 URL 系统设计#

1.1 题目描述#

设计一个短链接服务,将长 URL 转换为短 URL,要求支持高并发访问。

1.2 解题思路#

  1. 短链生成:长 URL → 短 URL
  2. 短链跳转:短 URL → 长 URL(301/302 重定向)
  3. 数据存储:映射关系持久化

设计方案:

flowchart LR A[用户请求] --> B[API网关] B --> C{请求类型} C -->|生成短链| D[长URL哈希计算] D --> E[Base62编码] E --> F[存储映射] F --> G[返回短链] C -->|访问短链| H[查询缓存] H --> I{缓存命中?} I -->|是| J[返回长URL] I -->|否| K[查询数据库] K --> J

短链生成算法:

方案优点缺点
哈希算法(MD5/SHA)生成速度快可能冲突,需处理
自增 ID + Base62无冲突,短ID 递增可预测
雪花算法无冲突,分布式友好长度稍长
随机数 + 冲突检测分布式友好需多次尝试

存储设计:

  • 缓存层:Redis 缓存热点短链映射
  • 存储层:MySQL 分库分表或 NoSQL(如 MongoDB)
  • 容量估算:假设 100 亿条记录,每条记录 500B,约需 5TB 存储

性能优化:

  • 使用布隆过滤器快速判断短链是否存在
  • 多级缓存:本地缓存 → Redis → 数据库
  • 异步写入:生成短链后异步持久化

1.3 代码示例#

import hashlib
import base62
class URLShortener:
def __init__(self, cache, db):
self.cache = cache
self.db = db
def shorten(self, long_url: str) -> str:
# 计算哈希并编码
hash_value = hashlib.md5(long_url.encode()).hexdigest()[:8]
short_code = base62.encode(int(hash_value, 16))
# 存储映射关系
self.cache.set(short_code, long_url, ex=86400)
self.db.save(short_code, long_url)
return f"https://s.url/{short_code}"
def expand(self, short_code: str) -> str:
# 先查缓存
long_url = self.cache.get(short_code)
if long_url:
return long_url
# 再查数据库
long_url = self.db.find(short_code)
if long_url:
self.cache.set(short_code, long_url, ex=86400)
return long_url

二、Feed 流系统设计#

2.1 题目描述#

设计一个类似微博/朋友圈的 Feed 流系统,用户可以发布动态,粉丝可以实时看到关注人的动态。

2.2 解题思路#

模式原理适用场景优缺点
推模式(写扩散)发布时写入所有粉丝收件箱粉丝少的大V读快写慢
拉模式(读扩散)读取时聚合所有关注人动态粉丝多的用户写快读慢

混合方案设计:

flowchart TD A[用户发布动态] --> B{粉丝数量} B -->|< 1000| C[推模式] B -->|>= 1000| D[拉模式] C --> E[写入所有粉丝收件箱] D --> F[仅写入发件箱] G[用户拉取Feed] --> H{是否大V粉丝} H -->|是| I[从发件箱拉取] H -->|否| J[从收件箱读取]

存储设计:

  • 用户关系表:存储关注关系(user_id, follower_id)
  • 动态表:存储发布内容(post_id, user_id, content, timestamp)
  • 收件箱表:存储推模式的动态副本(user_id, post_id, timestamp)

关键问题解决:

  1. 分页问题:使用游标分页代替 offset 分页
  2. 重复内容:对重复动态去重(基于 post_id)
  3. 实时性:使用消息队列异步推送
  4. 热点用户:大 V 采用拉模式,普通用户采用推模式

架构示意:

发布流程:
用户发布 → API服务 → 消息队列 → 消费者 → 写入存储
拉取流程:
用户请求 → API服务 → 缓存查询 → 数据库查询 → 聚合排序 → 返回

2.3 面试要点#

  • 为什么选择推/拉模式?根据读写比例决定
  • 如何处理大 V 粉丝多的场景?采用混合模式
  • 如何保证实时性?消息队列 + 推送通知
  • 如何处理缓存一致性问题?延迟双删 + 过期时间

三、延迟任务队列设计#

3.1 题目描述#

设计一个延迟任务队列,支持在指定时间后执行任务,如订单超时取消、定时消息推送等。

3.2 解题思路#

方案实现方式优点缺点
数据库轮询定时扫描到期任务简单可靠性能差,延迟高
Redis ZSetscore 存执行时间高效,支持范围查询需额外处理可靠性
时间轮环形数组 + 指针O(1) 入队出队内存限制
消息队列延迟RocketMQ 延迟消息可靠性高功能受限

Redis ZSet 方案详解:

flowchart LR A[生产者] --> B[Redis ZSet] B --> C[定时器轮询] C --> D{到期任务} D --> E[取出执行] E --> F[消费者处理]

核心实现:

public class DelayQueue {
private final RedisTemplate<String, Object> redis;
private final String QUEUE_KEY = "delay:queue";
// 添加延迟任务
public void offer(String taskId, long delayMs) {
long executeTime = System.currentTimeMillis() + delayMs;
redis.opsForZSet().add(QUEUE_KEY, taskId, executeTime);
}
// 消费到期任务
public void consume() {
while (true) {
long now = System.currentTimeMillis();
// 查询到期任务
Set<Object> tasks = redis.opsForZSet()
.rangeByScore(QUEUE_KEY, 0, now, 0, 100);
if (tasks == null || tasks.isEmpty()) {
Thread.sleep(100);
continue;
}
for (Object task : tasks) {
// CAS 删除,保证只被消费一次
Long removed = redis.opsForZSet()
.remove(QUEUE_KEY, task);
if (removed > 0) {
executeTask(task);
}
}
}
}
}

可靠性保证:

  1. 任务持久化:任务存储在 Redis,宕机不丢失
  2. 幂等消费:CAS 删除保证任务只被执行一次
  3. 重试机制:执行失败的任务重新入队
  4. 监控告警:监控积压任务数量和延迟时间

时间轮方案:

时间轮结构:
[0] → [1] → [2] → ... → [N-1] → [0](循环)
↓ ↓ ↓ ↓
任务链表 任务链表 任务链表 任务链表
每个槽位:存储该时刻执行的任务
指针移动:每秒移动一格,执行当前槽位的任务

四、秒杀系统设计#

4.1 题目描述#

设计一个秒杀系统,支持高并发场景下的库存扣减,保证不超卖、不少卖。

4.2 解题思路#

  • 高并发:瞬时流量巨大(QPS 可达 10 万+)
  • 一致性:库存扣减不能出错
  • 高可用:系统稳定运行

架构设计:

flowchart TD A[用户请求] --> B[CDN静态资源] A --> C[网关限流] C --> D[答题/验证码] D --> E[Redis库存预扣] E --> F{库存充足?} F -->|否| G[返回失败] F -->|是| H[消息队列] H --> I[数据库扣减] I --> J[创建订单]

关键设计:

1. 多级限流

第一层:CDN 静态资源 + 动态请求过滤
第二层:网关限流(令牌桶/漏桶)
第三层:应用层限流(Sentinel)
第四层:Redis 原子操作限流

2. 库存扣减方案

// Redis Lua 脚本保证原子性
String luaScript = """
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) <= 0 then
return 0
end
redis.call('DECR', KEYS[1])
return 1
""";
// 执行库存扣减
Long result = redis.execute(luaScript, Arrays.asList("stock:sku_001"));
if (result == 1) {
// 扣减成功,发送消息异步创建订单
mq.send("order.create", orderInfo);
}

3. 数据库优化

  • 独立数据库:秒杀表单独库,隔离影响
  • 行级锁优化UPDATE stock SET count = count - 1 WHERE id = ? AND count > 0
  • 异步落库:先扣 Redis,后异步同步到数据库

4. 防超卖机制

层级方案说明
应用层预扣库存内存预减,快速失败
缓存层Lua 原子操作保证 Redis 操作原子性
数据库层乐观锁/条件更新WHERE count > 0

5. 防刷机制

  • 验证码/答题:分散请求,防止机器刷单
  • 用户限购:Redis 记录购买次数
  • IP 限流:同一 IP 请求频率限制
  • 黑名单机制:异常用户加入黑名单

4.3 性能优化要点#

读优化:
- 静态资源 CDN 加速
- 商品详情页静态化
- Redis 缓存热点数据
写优化:
- 消息队列异步削峰
- 批量写入数据库
- 分库分表

五、分布式 ID 生成器#

5.1 题目描述#

设计一个分布式 ID 生成器,要求全局唯一、有序、高性能。

5.2 解题思路#

方案格式优点缺点
UUID32位十六进制简单,无依赖无序,过长
数据库自增数字递增简单有序单点瓶颈
号段模式数字递增高性能依赖数据库
雪花算法64位数字有序分布式时钟回拨问题
Redis 生成数字递增高性能依赖 Redis

雪花算法详解:

64位结构:
0 - 41位时间戳 - 10位机器ID - 12位序列号
| 1位符号位 | 41位时间戳 | 10位机器ID | 12位序列号 |
|----------|-----------|-----------|-----------|
| 0 | 毫秒级时间 | 数据中心+机器 | 毫秒内序列 |
- 时间戳:41位可使用 69 年
- 机器ID:支持 1024 台机器
- 序列号:每毫秒可生成 4096 个 ID
- 理论 QPS:400万+/秒

实现代码:

public class SnowflakeIdGenerator {
private final long epoch = 1288834974657L; // 起始时间戳
private final long workerIdBits = 10L; // 机器ID位数
private final long sequenceBits = 12L; // 序列号位数
private final long maxWorkerId = ~(-1L << workerIdBits); // 1023
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << (workerIdBits + sequenceBits))
| (workerId << sequenceBits)
| sequence;
}
}

时钟回拨解决方案:

  1. 等待回拨:短暂回拨(<5ms)可等待时钟追上
  2. 借用未来时间:回拨时使用未来时间戳
  3. 备用机器ID:回拨时切换到备用 workerId
  4. 直接报错:严重回拨直接拒绝服务

号段模式(Leaf):

每次从数据库取一个号段:
[1000, 2000] → 应用内存分配 → 分配完毕再取下一号段
优点:
- 减少 DB 访问次数
- 高性能(QPS 可达 10 万+)
缺点:
- ID 不连续(重启可能丢失)
- 依赖 DB

六、一致性哈希与负载均衡#

6.1 题目描述#

设计一个分布式缓存系统,要求能够动态扩缩容,且缓存迁移量最小。

6.2 解题思路#

传统哈希:hash(key) % N
问题:当节点数 N 变化时,几乎所有 key 的映射都会改变
扩容:N=4 → N=5,约 80% 的 key 需要迁移

一致性哈希原理:

graph LR A[哈希环 0~2^32-1] --> B[节点均匀分布] B --> C[Key 顺时针寻找最近节点] C --> D[节点变更只影响相邻节点]

核心特性:

特性说明
均衡性数据均匀分布到各节点
单调性新增节点不影响已分配数据
分散性相同 key 映射到相同节点
负载均衡避免节点负载不均

虚拟节点优化:

问题:节点少时,数据分布可能不均匀
解决:每个物理节点对应多个虚拟节点
实现:
Node A → Node A#1, Node A#2, ..., Node A#150
Node B → Node B#1, Node B#2, ..., Node B#150
虚拟节点映射到哈希环上,使数据分布更均匀

代码实现:

public class ConsistentHash {
private final TreeMap<Long, String> ring = new TreeMap<>();
private final int virtualNodes = 150;
// 添加节点
public void addNode(String node) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNode = node + "#" + i;
long hash = hash(virtualNode);
ring.put(hash, node);
}
}
// 获取 key 对应的节点
public String getNode(String key) {
if (ring.isEmpty()) {
return null;
}
long hash = hash(key);
// 顺时针查找第一个大于等于 hash 的节点
Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
if (entry == null) {
entry = ring.firstEntry(); // 环形,回到起点
}
return entry.getValue();
}
private long hash(String key) {
return CRC16.crc16(key.getBytes()) & 0xFFFFFFFFL;
}
}

负载均衡策略对比:

策略算法适用场景优缺点
轮询Round Robin服务端性能相近简单,不考虑差异
加权轮询Weighted RR服务端性能不同需配置权重
随机Random无状态服务分布可能不均
最少连接Least Conn长连接场景需维护连接数
一致性哈希Consistent Hash缓存场景扩缩容友好

实际应用:

  • Redis Cluster:槽位机制(16384 个槽)
  • Memcached:Ketama 一致性哈希
  • Nginx:支持一致性哈希负载均衡
  • Dubbo:支持一致性哈希路由

七、总结#

系统设计面试的核心考察点:

  1. 需求分析:明确功能需求和非功能需求
  2. 架构设计:画出系统架构图,说明各组件职责
  3. 容量估算:计算 QPS、存储、带宽等
  4. 瓶颈分析:识别系统瓶颈并提出解决方案
  5. 权衡取舍:说明方案的优缺点和选择理由

答题技巧:

  • 先问清楚需求再动手设计
  • 从简单方案开始,逐步优化
  • 用数字说话(QPS、延迟、容量)
  • 考虑边界情况和异常处理
  • 提及监控、报警、容灾等

推荐阅读:

  • 《Designing Data-Intensive Applications》
  • 《系统设计面试指南》
  • 各大厂技术博客(美团、阿里、字节)

提示:系统设计没有标准答案,重要的是展示你的思考过程和权衡能力。面试中要主动和面试官沟通,确认需求细节,然后给出适合场景的解决方案。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

系统设计面试题
https://blog.souloss.com/posts/interview/system-design/
作者
Souloss
发布于
2024-07-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时