系统设计方法论¶
核心问题:面对"设计一个 XXX 系统"的问题,如何系统化地思考和回答?有哪些经典的系统设计案例可以参考?
它解决了什么问题?¶
系统设计不是凭感觉画架构图,而是有一套结构化的方法论。掌握这套方法论,你可以:
- 面对任何系统设计问题,都有清晰的分析框架
- 从需求出发,逐步推导出合理的架构方案
- 在关键决策点上做出有理有据的技术选型
一、系统设计答题框架¶
1.1 标准流程¶
flowchart TD
A["1. 需求澄清<br>功能需求 + 非功能需求"] --> B["2. 容量估算<br>QPS / 存储 / 带宽"]
B --> C["3. 整体架构设计<br>画架构图"]
C --> D["4. 数据库设计<br>表结构 / 索引 / 分库分表"]
D --> E["5. 接口设计<br>RESTful API / gRPC"]
E --> F["6. 核心流程详解<br>关键链路时序图"]
F --> G["7. 扩展性讨论<br>高可用 / 高并发 / 容灾"] 1.2 各步骤要点¶
| 步骤 | 关键动作 | 常见陷阱 |
|---|---|---|
| 需求澄清 | 明确核心功能、用户规模、性能要求 | 没有澄清就开始设计,方向跑偏 |
| 容量估算 | 估算 QPS、存储量、带宽,确定量级 | 忽略峰值流量,只按平均值设计 |
| 架构设计 | 画出核心组件和数据流向 | 一上来就画细节,缺少全局视角 |
| 数据库设计 | 确定表结构、索引、读写分离策略 | 不考虑数据量增长,缺少分库分表方案 |
| 接口设计 | 定义核心 API,明确输入输出 | 接口粒度不合理,过粗或过细 |
| 核心流程 | 用时序图描述关键链路 | 只画正常流程,忽略异常处理 |
| 扩展性 | 讨论高可用、高并发、容灾方案 | 过度设计,引入不必要的复杂度 |
1.3 容量估算速查¶
日活用户 (DAU) → 每日请求量 → QPS → 峰值 QPS
常用换算:
- 1 天 ≈ 86400 秒 ≈ 10^5 秒(方便估算)
- 峰值 QPS ≈ 平均 QPS × 2~5 倍
- 1 亿次/天 → 平均 QPS ≈ 1000,峰值 ≈ 3000~5000
存储估算:
- 每条数据大小 × 每日新增量 × 保留天数
- 1KB × 1亿/天 × 365天 ≈ 36TB/年
带宽估算:
- QPS × 每次响应大小
- 3000 QPS × 10KB = 30MB/s ≈ 240Mbps
二、经典系统设计案例¶
案例一:短链接系统(TinyURL)¶
需求澄清¶
- 功能需求:长链转短链、短链跳转原链接、统计点击量
- 非功能需求:低延迟(< 50ms)、高可用(99.99%)、短链不可预测
容量估算¶
写入:每天 100 万条新短链 → 写 QPS ≈ 12
读取:每天 1 亿次跳转 → 读 QPS ≈ 1200,峰值 ≈ 5000
存储:每条 500B × 100万/天 × 365天 × 5年 ≈ 900GB
架构设计¶
flowchart TB
Client["客户端"] --> LB["负载均衡<br>Nginx"]
LB --> App["短链服务集群"]
App --> Cache["Redis 集群<br>缓存热点短链"]
App --> DB["MySQL 主从<br>持久化存储"]
App --> Counter["点击统计<br>Kafka → 异步聚合"] 核心算法:短码生成¶
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 哈希截取 | MD5/SHA256 取前 6 位 | 实现简单 | 有碰撞风险,需要检测 |
| 自增 ID + Base62 | 数据库自增 ID 转 Base62 | 无碰撞,有序 | ID 可预测,有安全风险 |
| 分布式 ID + Base62 | Snowflake 生成 ID 转 Base62 | 无碰撞,不可预测 | 短码较长(10-11 位) |
| 预生成 + 随机分配 | 预先生成短码池,用时分配 | 无碰撞,长度可控 | 需要维护短码池 |
推荐方案:自增 ID + Base62 编码(62^6 ≈ 560 亿,6 位短码足够),配合随机偏移量防止 ID 可预测。
关键优化¶
- 缓存策略:热点短链缓存到 Redis,TTL 24h,命中率可达 80%+
- 301 vs 302:301 永久重定向(浏览器缓存,减少服务端压力)vs 302 临时重定向(每次经过服务端,方便统计)
- 高可用:MySQL 主从 + Redis 集群 + 多机房部署
案例二:秒杀系统¶
需求澄清¶
- 功能需求:商品秒杀、库存扣减、订单生成
- 非功能需求:超高并发(10 万+ QPS)、不超卖、不少卖、用户体验流畅
- 核心挑战:瞬时流量极高,库存是共享资源,需要防止超卖
容量估算¶
架构设计:层层削峰¶
flowchart TB
subgraph "第一层:客户端"
C["按钮置灰 + 倒计时<br>防止重复点击"]
end
subgraph "第二层:CDN/Nginx"
N["静态资源 CDN<br>Nginx 限流<br>过滤 80% 请求"]
end
subgraph "第三层:网关层"
GW["令牌桶限流<br>用户去重<br>过滤 90% 请求"]
end
subgraph "第四层:服务层"
S["Redis 预扣库存<br>Lua 脚本原子操作"]
end
subgraph "第五层:消息队列"
MQ["Kafka 异步下单<br>削峰填谷"]
end
subgraph "第六层:数据库"
DB["MySQL 最终扣减<br>乐观锁防超卖"]
end
C --> N --> GW --> S --> MQ --> DB 核心方案:Redis + Lua 预扣库存¶
-- Redis Lua 脚本:原子性扣减库存
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
return -1 -- 库存不足
end
redis.call('DECR', KEYS[1])
return stock - 1 -- 返回剩余库存
防超卖三道防线¶
| 防线 | 机制 | 说明 |
|---|---|---|
| 第一道 | Redis Lua 原子扣减 | 单线程执行,天然防并发 |
| 第二道 | 消息队列串行化 | 下单请求排队处理 |
| 第三道 | MySQL 乐观锁 | UPDATE SET stock = stock - 1 WHERE stock > 0 |
案例三:分布式 ID 生成器¶
需求澄清¶
- 功能需求:生成全局唯一 ID,支持多机房多实例
- 非功能需求:高性能(> 10 万/秒)、趋势递增(方便数据库索引)、高可用
常见方案对比¶
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| UUID | 128 位随机数 | 本地生成,无依赖 | 无序,索引性能差,太长 | 非数据库主键场景 |
| 数据库自增 | AUTO_INCREMENT | 简单,有序 | 单点瓶颈,性能有限 | 低并发场景 |
| 号段模式 | 批量获取 ID 段,本地分配 | 高性能,DB 压力小 | 重启可能浪费号段 | 中高并发,如美团 Leaf |
| Snowflake | 时间戳 + 机器 ID + 序列号 | 高性能,趋势递增 | 依赖时钟,时钟回拨有风险 | 高并发分布式系统 |
| Redis INCR | Redis 原子自增 | 简单,高性能 | 依赖 Redis,持久化风险 | 中等并发场景 |
Snowflake 算法详解¶
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
│ │ │ │ │
│ └── 41位时间戳(毫秒级,可用69年) │ │ │
│ │ │ └── 12位序列号(每毫秒4096个)
│ │ └── 5位机器ID(32台)
│ └── 5位数据中心ID(32个)
└── 1位符号位(固定为0)
时钟回拨问题解决方案:
- 等待追上:检测到回拨时,等待时钟追上上次时间戳
- 备用机器 ID:切换到备用机器 ID 继续生成
- 扩展位:预留几位作为时钟回拨序列
三、系统设计通用模式¶
3.1 高并发三板斧¶
flowchart LR
A["缓存<br>减少 DB 压力"] --> B["异步<br>削峰填谷"]
B --> C["分治<br>分库分表/分片"] | 模式 | 手段 | 适用场景 |
|---|---|---|
| 缓存 | Redis、本地缓存、CDN | 读多写少,热点数据 |
| 异步 | 消息队列、异步线程池 | 非核心链路,允许延迟 |
| 分治 | 分库分表、ES 分片、Redis 集群 | 单机容量/性能不足 |
3.2 高可用核心策略¶
| 策略 | 说明 | 示例 |
|---|---|---|
| 冗余 | 多副本,消除单点 | MySQL 主从、Redis 集群 |
| 熔断 | 下游故障时快速失败 | Sentinel、Hystrix |
| 降级 | 非核心功能关闭,保核心 | 秒杀时关闭推荐功能 |
| 限流 | 控制请求速率 | 令牌桶、滑动窗口 |
| 超时重试 | 设置合理超时,失败重试 | 重试需要幂等保证 |
3.3 数据一致性方案¶
| 方案 | 一致性级别 | 复杂度 | 适用场景 |
|---|---|---|---|
| 本地事务 | 强一致 | 低 | 单库操作 |
| 2PC/XA | 强一致 | 高 | 跨库但对一致性要求极高 |
| TCC | 最终一致 | 高 | 资金类业务 |
| Saga | 最终一致 | 中 | 长事务,多步骤流程 |
| 消息队列 | 最终一致 | 低 | 异步场景,允许短暂不一致 |
| 本地消息表 | 最终一致 | 中 | 可靠消息投递 |
四、工作中常见错误与避坑指南¶
| 场景 | 常见错误 | 正确做法 | 根本原因 |
|---|---|---|---|
| 服务拆分 | 拆分过细,每个接口都跨服务调用 | 按业务域拆分,高内聚低耦合 | 过度拆分导致网络开销和分布式事务问题 |
| 分布式事务 | 用本地事务处理跨服务数据一致性 | 使用 Saga 模式或最终一致性 | 跨服务无法使用数据库事务 |
| CAP 选型 | 强一致性场景选了 AP 系统 | 根据业务需求选择 CP 或 AP | 不了解 CAP 理论,选型错误 |
| 缓存设计 | 缓存和 DB 双写不一致 | Cache Aside 模式:先更新 DB,再删缓存 | 缓存更新策略不当 |
| 代码设计 | Service 类几千行,违反 SRP | 按职责拆分,使用领域事件解耦 | 缺乏 SOLID 意识,代码越来越难维护 |
| 测试 | 只写 E2E 测试,不写单元测试 | 遵循测试金字塔,单元测试为主 | E2E 测试慢且脆弱,无法快速反馈 |
| 发布 | 手动部署,无回滚方案 | 建立 CI/CD 流水线,蓝绿/金丝雀发布 | 手动操作不可靠,出错无法快速回滚 |
| 容量规划 | 只按平均流量设计 | 按峰值 3-5 倍预留,配合弹性扩缩容 | 峰值流量可能是平均的 10 倍以上 |
五、常见问题¶
Q:系统设计面试中最重要的是什么?
不是给出"完美"的方案,而是展示你的思考过程:如何分析需求、如何做取舍、为什么选择这个方案而不是那个。面试官更看重你的思维方式,而不是记住了多少架构图。
Q:如何估算系统容量?
从日活用户(DAU)出发:DAU → 每日请求量(DAU × 每人每日操作次数)→ 平均 QPS(÷ 86400)→ 峰值 QPS(× 3~5)。存储 = 每条数据大小 × 日增量 × 保留天数。带宽 = QPS × 响应大小。
Q:秒杀系统如何防止超卖?
三道防线:① Redis Lua 脚本原子扣减库存(第一道);② 消息队列串行化下单请求(第二道);③ MySQL 乐观锁
WHERE stock > 0(第三道)。核心思路是层层削峰,尽早过滤无效请求。
Q:分布式 ID 用什么方案?
低并发用数据库自增;中等并发用号段模式(如美团 Leaf);高并发用 Snowflake 算法。UUID 不适合做数据库主键(无序,索引性能差)。选择时需要考虑:唯一性、有序性、性能、可用性。
Q:如何保证缓存和数据库的一致性?
推荐 Cache Aside 模式:读时先查缓存,未命中查 DB 并回填缓存;写时先更新 DB,再删除缓存。不要用"先更新缓存再更新 DB",因为并发场景下会导致数据不一致。对于强一致性要求,可以用延迟双删或订阅 binlog 更新缓存。