大厂系统设计面试:如何用30分钟设计出让面试官满意的架构
大厂系统设计面试:如何用30分钟设计出让面试官满意的架构
适读人群:Java高级开发、架构师 | 难度:★★★★★ | 出现频率:极高
开篇故事
系统设计面试是大厂P7及以上职级的标配考题,也是最能拉开候选人差距的题目。
我做面试官的时候,系统设计题通常给30-45分钟。这段时间里,我不只在听候选人说了什么,更在观察他怎么思考、怎么提问、怎么推导、怎么取舍。
有个候选人一开口就直接画架构图,Redis、Kafka、MySQL往上一排,说"我的方案是这样"。
我问:你的QPS预估是多少?
他一愣:……没想过。
我问:你这个设计能支撑多大的规模,是10万用户还是10亿用户?
他说:……差不多就行吧。
这样的回答,说明他没有系统设计的思维,只是在秀技术栈。
好的系统设计,是从需求出发,逐步推导到技术选型,每一个技术决策都有明确的原因。今天我把这套方法论完整地教给你,并用"设计一个短链接系统"作为例子,走完完整的30分钟流程。
一、高频考点拆解
系统设计面试考察的不是你知道多少技术,而是:
第一:需求理解和澄清 能快速识别核心需求,主动提问澄清模糊点,而不是假设
第二:容量估算 能用数字说话,知道自己的设计要撑多大规模
第三:架构设计 能分层设计,每层的技术选型有合理的依据
第四:深度讨论 对每个关键设计决策,能说出为什么这样选、有什么权衡取舍
二、30分钟系统设计方法论
2.1 标准流程(时间分配)
2.2 第一步:需求澄清(0-5分钟)
这是最重要的一步,很多候选人忽视了。
主动提问,把模糊的需求变成具体的:
功能需求:
- 核心功能是什么?(必须有)
- 哪些是加分功能?(有时间再说)
- 有没有需要特别处理的边界情况?
非功能需求(QPS/规模):
- 预计用户规模?(DAU是多少)
- 读多写多还是差不多?
- 对延迟有什么要求?
- 数据一致性要求是强一致还是最终一致?
- 高可用要求是几个9?
2.3 第二步:容量估算(5-10分钟)
用数字推导出设计的边界:
QPS估算:
DAU(日活用户): 1亿
每用户每天操作次数: 10次
每天请求总数: 1亿 × 10 = 10亿
平均QPS: 10亿 / (24 × 3600) ≈ 12000 QPS
峰值QPS(通常是平均的5倍): 60000 QPS存储估算:
每条记录大小: 100 Bytes
每天新增记录: 100万条
每天新增存储: 100万 × 100B = 100MB/天
5年存储: 100MB × 365 × 5 ≈ 180GB带宽估算:
平均QPS: 12000,每次响应 1KB
带宽: 12000 × 1KB = 12MB/s
峰值带宽: 60MB/s知道了这些数字,你的设计就有了量化目标:要撑6万QPS,要存180GB数据。
三、案例实战:设计短链接系统(URL Shortener)
3.1 需求澄清
功能需求:
- 输入长URL,生成短URL(如
bit.ly/abc123) - 访问短URL,重定向到原始长URL
- 短链有效期(可选:默认永久,支持自定义过期时间)
- 访问统计(可选:点击次数统计)
非功能需求:
- DAU:1亿用户
- 读写比:100:1(读多写少,大量访问短链,少量创建)
- 延迟要求:重定向 < 100ms(用户体验关键)
- 高可用:99.99%(四个9)
- 短URL要尽量短(6-8位字母数字)
3.2 容量估算
创建短链:
- 每天新创建量:1亿 × 1/30 ≈ 330万次/天(假设每人每月创建1次)
- 写QPS:330万 / 86400 ≈ 38 QPS(很低)
- 峰值写QPS:约200 QPS
访问短链:
- 每天访问量:330万 × 10 = 3300万次/天(每条短链平均被访问10次)
- 读QPS:3300万 / 86400 ≈ 380 QPS
- 峰值读QPS:约2000 QPS
存储:
- 每条短链记录:短URL(10B) + 长URL(100B) + 元数据(50B) ≈ 160B
- 5年存储:330万/天 × 365天 × 5年 × 160B ≈ 1TB结论:这是一个读远多于写的系统,峰值读QPS约2000,存储需求1TB,不算太大的规模。
3.3 高层架构设计
关键技术决策:
为什么用Redis缓存? 读写比100:1,热门短链会被频繁访问,Cache命中率会很高(假设80%+)。读QPS2000,Cache命中1600次,实际打到DB的只有400次,大幅降低DB压力。同时Redis的GET操作< 1ms,满足100ms的延迟要求。
为什么用消息队列写访问日志? 访问统计是非关键路径,不应该阻塞重定向的核心链路。访问统计的MQ异步化,让核心链路只做两件事:Redis缓存查询 + HTTP重定向,极简路径保证低延迟。
3.4 短URL生成算法
这是系统设计的核心问题。
方案一:哈希截断
// 取URL的MD5,截取前6位
String hash = DigestUtils.md5Hex(longUrl).substring(0, 6);
// 问题:哈希冲突(不同URL可能生成相同的短码)
// 解决:冲突时追加字符串重新哈希方案二:Base62编码(推荐)
/**
* Base62编码:将自增ID转换为6位短码
* 62^6 = 568亿,足够用很久
* 使用数字+小写字母+大写字母共62个字符
*/
public class Base62Encoder {
private static final String CHARS =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int BASE = 62;
public static String encode(long id) {
StringBuilder sb = new StringBuilder();
while (id > 0) {
sb.append(CHARS.charAt((int)(id % BASE)));
id /= BASE;
}
// 补足6位
while (sb.length() < 6) {
sb.append('0');
}
return sb.reverse().toString();
}
public static long decode(String shortCode) {
long id = 0;
for (char c : shortCode.toCharArray()) {
id = id * BASE + CHARS.indexOf(c);
}
return id;
}
public static void main(String[] args) {
// 测试
for (long i = 1; i <= 10; i++) {
String code = encode(i);
System.out.println(i + " -> " + code + " -> " + decode(code));
}
// 1 -> 000001 -> 1
// 62 -> 000010 -> 62
// 3521614606208L -> zzzzzz -> 3521614606208 (最大6位)
}
}自增ID的生成:分布式环境下,需要全局唯一的自增ID。方案:
- 数据库自增(单点,低并发可用)
- Redis INCR(高性能,重启后需要持久化保证)
- 分布式ID(Snowflake算法)
@Service
public class ShortUrlService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ShortUrlMapper mapper;
private static final String COUNTER_KEY = "short_url:counter";
private static final String CACHE_PREFIX = "short:";
// 创建短链
public String createShortUrl(String longUrl, Integer expireDays) {
// 1. 先查是否已经有这个长URL的短链(去重)
String existing = mapper.findShortByLong(longUrl);
if (existing != null) return existing;
// 2. 生成唯一ID(用Redis INCR)
Long id = redisTemplate.opsForValue().increment(COUNTER_KEY);
String shortCode = Base62Encoder.encode(id);
// 3. 存储到DB
ShortUrl record = new ShortUrl();
record.setId(id);
record.setShortCode(shortCode);
record.setLongUrl(longUrl);
record.setExpireTime(expireDays != null ?
LocalDateTime.now().plusDays(expireDays) : null);
mapper.insert(record);
// 4. 写入Redis缓存
String cacheKey = CACHE_PREFIX + shortCode;
if (expireDays != null) {
redisTemplate.opsForValue().set(cacheKey, longUrl, expireDays, TimeUnit.DAYS);
} else {
redisTemplate.opsForValue().set(cacheKey, longUrl);
}
return "https://short.example.com/" + shortCode;
}
// 访问短链,重定向
public String getLongUrl(String shortCode) {
String cacheKey = CACHE_PREFIX + shortCode;
// 1. 先查Redis缓存
String longUrl = redisTemplate.opsForValue().get(cacheKey);
if (longUrl != null) {
// 异步记录访问(不阻塞重定向)
asyncRecordAccess(shortCode);
return longUrl;
}
// 2. 缓存未命中,查DB
ShortUrl record = mapper.findByShortCode(shortCode);
if (record == null) return null;
if (record.isExpired()) return null;
// 3. 写回缓存
redisTemplate.opsForValue().set(cacheKey, record.getLongUrl());
asyncRecordAccess(shortCode);
return record.getLongUrl();
}
@Async
void asyncRecordAccess(String shortCode) {
// 异步发消息到MQ,统计服务消费
mqTemplate.asyncSend("short_url_access", shortCode);
}
}3.5 高可用设计
四、面试官追问
追问1:如果系统访问量突然增大10倍,怎么扩容?
我的回答:分层扩容。API服务层是无状态的,直接水平扩展,加机器加容器,Nginx负载均衡会自动分流。Redis Cluster支持在线扩容,增加Slot的分配范围,按需扩展。MySQL的读压力可以通过增加从库来分担,写压力需要考虑分库分表(按用户ID或短码哈希分片)。如果流量激增是暂时的(如营销活动),可以提前做容量规划,提前扩容,活动结束后缩容。关键是整个系统要有监控告警,QPS/延迟/错误率异常时立刻告警,人工介入或自动扩容。
追问2:如何防止缓存穿透(查询不存在的短码)?
我的回答:有几种方案。第一,对不存在的短码也缓存空值:查DB返回空后,往Redis写入shortCode → "NOT_FOUND",过期时间设短一些(如1分钟),后续相同短码的查询直接从缓存返回"不存在",不打DB。第二,布隆过滤器:启动时把所有存在的短码加入布隆过滤器,查询时先用布隆过滤器判断短码是否存在,不存在直接返回404,不查Redis也不查DB。布隆过滤器有少量误判(存在的判断为不存在不可能,不存在的判断为存在概率很小),但基本能解决穿透问题。
追问3:短链的去重如何优化?创建时检查长URL是否已有短链,如何高效查询?
我的回答:在MySQL的long_url字段上建索引,SELECT short_code FROM short_url WHERE long_url = ?,加上索引后查询O(logN)。但如果长URL很长,索引效率低(全文匹配)。优化:存储长URL的哈希值(MD5/SHA1),对哈希值建索引,查询时用哈希值匹配,哈希固定长度,索引效率高。注意需要同时校验原始URL是否一致(处理哈希碰撞)。
五、同类题目举一反三
还有哪些常见的系统设计题?答题框架是通用的。
常见题目:
- 设计微博/Twitter(动态发布、关注、时间线)
- 设计YouTube(视频上传、分发、推荐)
- 设计微信聊天(消息存储、推送、群聊)
- 设计抖音推荐系统(用户行为、召回、排序)
- 设计支付系统(对账、幂等、分布式事务)
- 设计搜索引擎(爬取、索引、排序)
每道题用同一套框架:需求澄清 → 容量估算 → 高层架构 → 核心模块深入 → 扩展性和高可用讨论。
六、踩坑实录
坑一:上来就画图,忘了澄清需求
有次系统设计面试,面试官说"设计一个消息系统",我直接开始画架构。画了一半,面试官说:"我想要的是站内私信,你设计的是什么?"浪费了10分钟。
教训:先问清楚需求,再动笔。即使觉得需求很清楚,也要主动确认核心场景。
坑二:说了一堆技术,但没有说为什么选这个技术
"我用Kafka做消息队列,用Redis做缓存,用MySQL做存储。"
面试官问:为什么用Kafka不用RocketMQ?为什么用MySQL不用MongoDB?
如果只是堆技术栈而不说选型理由,面试官会认为你在套模板,没有真正思考。每个技术选择都要说出原因:Kafka的高吞吐量适合日志场景、RocketMQ的事务消息适合金融场景……
坑三:只设计了Happy Path,没有考虑失败情况
系统设计不只是正常流程,还要考虑:
- 组件宕机怎么降级?
- 数据不一致怎么补偿?
- 流量突增怎么限流?
- 慢查询怎么熔断?
把这些容错机制想清楚,是架构师思维的体现,也是面试官最想看到的。
七、总结
30分钟系统设计面试的黄金流程:
5分钟:澄清需求,确认核心功能和规模 5分钟:容量估算,推导QPS、存储、带宽 10分钟:高层架构,画出核心组件和数据流 10分钟:核心模块深入,聊关键设计决策和权衡
记住面试官不是在考你的技术知识积累,而是在观察你如何用系统化的方法解决不确定的问题。
主动提问、量化分析、清晰表达设计决策、诚实说出权衡和缺陷,这才是让面试官刮目相看的方式。
技术选型本身没有对错,能说清楚"为什么这样选,有什么代价,什么情况下要换方案",就是高水平的架构思维。
