系统设计方法与案例
系统设计不是把缓存、消息队列和微服务堆在一张图上,而是从需求出发,说明数据怎样流动、容量从哪里来、失败时系统做什么。面试中的白板题和真实项目都遵循同一套顺序:先明确问题,再估算,再画最小链路,最后逐个消除瓶颈。
1. 先澄清需求
跳转到“1. 先澄清需求”1.1 功能范围
跳转到“1.1 功能范围”把“做一个聊天系统”“做一个电商平台”拆成有限的功能:
- 谁使用,有哪些角色?
- 核心操作是什么?
- 哪些功能必须第一版就有,哪些可以之后做?
- 数据保留多久,是否允许删除和导出?
问题越模糊,越容易画出一个什么都能做、但不知道重点的架构。先选一条主链路,例如“用户发送消息,另一个在线用户收到”,而不是一开始就设计完整社交平台。
1.2 非功能要求
跳转到“1.2 非功能要求”| 维度 | 要问什么 |
|---|---|
| 规模 | 日活、请求量、数据量、读写比例 |
| 延迟 | 哪些操作必须快,目标是多少分位 |
| 可用性 | 允许多久不可用,哪些功能优先 |
| 一致性 | 哪些数据不能旧,允许多久最终一致 |
| 安全 | 身份、权限、审计、敏感数据 |
| 成本 | 预算、基础设施上限、单位经济模型 |
| 运维 | 团队能否维护这些组件 |
“高可用、高并发、低延迟”不是需求,只是愿望。必须给出数量、分位和失败容忍度,才能做取舍。
2. 容量估算
跳转到“2. 容量估算”估算不要求精确,但数量级要能指导设计。
2.1 常用换算
跳转到“2.1 常用换算”1 天 ≈ 86,400 秒,约 10^5 秒1 百万日请求 ≈ 11.6 QPS 平均值1 亿日请求 ≈ 1,157 QPS 平均值峰值通常是平均值的数倍。如果业务集中在几小时,峰值会更高。例如 100 万日请求平均约 12 QPS,但 80% 流量集中在 4 小时,则峰值平均约 0.8 × 1,000,000 / (4 × 3600) ≈ 56 QPS,再乘突发系数。
2.2 并发请求数
跳转到“2.2 并发请求数”用 Little 定律估算:
并发请求数 ≈ 到达 QPS × 平均处理时间(秒)100 QPS、平均处理 200 ms,约有 20 个请求同时处理。若依赖变慢到 2 秒,并发会增长到 200,连接池和线程池可能先耗尽。
2.3 存储
跳转到“2.3 存储”每日新增数据 ≈ 每日记录数 × 单条平均字节总存储 ≈ 每日新增 × 保留天数 × 副本/索引膨胀系数例如每天 100 万条记录,每条 1 KB,保留 365 天,原始约 365 GB;加上索引、副本和备份,可能需要数 TB。还要考虑增长率和归档。
2.4 带宽
跳转到“2.4 带宽”带宽 ≈ 每秒请求数 × 平均响应大小1,000 QPS、每响应 50 KB,需要约 50 MB/s 出站带宽,峰值和重传会更高。大文件或媒体流必须单独估算。
3. 接口与数据模型
跳转到“3. 接口与数据模型”接口定义系统边界,数据模型定义事实结构。
3.1 API 先行
跳转到“3.1 API 先行”用少量核心接口描述主流程:
POST /messagesGET /conversations/{id}/messages?cursor=...GET /users/{id}为每个接口写清:
- 输入、输出和错误码。
- 身份、权限和租户边界。
- 分页、幂等和超时。
- 数据一致性要求。
3.2 数据模型
跳转到“3.2 数据模型”User 1 ─── N ConversationMember N ─── 1 ConversationConversation 1 ─── N MessageUser 1 ─── N Message先确定主键、时间、状态和索引,再考虑缓存。用户、消息和会话分别以什么为事实来源?删除消息是物理删除、软删除还是只对某些人隐藏?这些语义直接影响 API 和存储。
4. 画最小主链路
跳转到“4. 画最小主链路”一个完整的请求链路通常包括:
客户端 → CDN/WAF → 负载均衡/API 网关 → 应用服务 → 缓存/数据库/队列 → 外部依赖先画最小版本,再问每个组件的必要性:
- 没有 CDN,静态资源从哪里来?
- 没有缓存,数据库能否承受?
- 没有队列,长任务是否阻塞请求?
- 没有副本,单机故障怎样恢复?
每个组件都增加部署、监控和故障面。能用一个数据库解决时,不必先上分布式搜索引擎。
5. 找瓶颈与故障点
跳转到“5. 找瓶颈与故障点”5.1 读写比例
跳转到“5.1 读写比例”读多写少可以缓存、副本和 CDN;写多读少更关注批处理、队列、分片和索引成本。不要给所有接口套同一种缓存。
5.2 热点
跳转到“5.2 热点”热门内容、热门用户和大租户会造成单键或单分区压力。解决方式可能包括本地缓存、请求合并、读副本、热点拆分和限流。分片键设计要避免所有流量落到同一片。
5.3 长尾
跳转到“5.3 长尾”总吞吐够,但少数请求很慢。需要超时、并发限制、降级和可观测性。一个慢查询占用连接池,会拖慢同实例其他请求。
5.4 失败传播
跳转到“5.4 失败传播”依赖变慢时,上游线程和连接被占满,最终自身也失败。通过超时、熔断、舱壁和背压阻断传播,而不是无限重试。
6. 案例:短链接服务
跳转到“6. 案例:短链接服务”6.1 需求
跳转到“6.1 需求”- 用户提交长 URL,得到短码。
- 访问短链接时重定向到原始 URL,返回
301/302。 - 支持自定义短码、过期时间、访问统计和删除。
- 目标可用性 99.9%,读取远多于写入。
- 不允许任意用户查看或修改他人的私有链接。
6.2 估算
跳转到“6.2 估算”假设每天新增 100 万条短链,平均每条记录 500 字节,保留 5 年:
原始数据 ≈ 100 万 × 500 B × 365 × 5 ≈ 0.9 TB写入 QPS 平均 ≈ 12,峰值按 10 倍约 120读取如果为写入的 100 倍,平均约 1,200 QPS读取量明显更大,适合缓存和 CDN/边缘优化;写入需要短码唯一性与防重复策略。
6.3 数据模型
跳转到“6.3 数据模型”CREATE TABLE links ( id BIGINT PRIMARY KEY, short_code VARCHAR(16) NOT NULL UNIQUE, long_url TEXT NOT NULL, owner_id BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ);短码可以随机生成并重试,也可以把唯一 ID 编码为短字符串。随机短码要检查冲突;顺序 ID 可预测,私有链接仍必须鉴权,不能靠“别人猜不到”当安全。
6.4 访问路径
跳转到“6.4 访问路径”GET /{code} → 读取缓存 → 未命中查数据库 → 检查过期/删除/状态 → 记录异步统计 → 返回重定向统计写入不应阻塞重定向主链路,可以进队列批量聚合。缓存键包含短码和版本;删除或更新时主动失效。恶意短码、开放重定向和钓鱼目标需要审核与黑名单。
6.5 扩展
跳转到“6.5 扩展”- 读多:CDN、只读副本、进程内/分布式缓存。
- 写多:按 owner 或短码哈希分片。
- 热点:本地缓存与请求合并。
- 统计:事件流、批处理、预聚合。
- 可用性:多实例、数据库故障转移、缓存降级。
6.6 失败模式
跳转到“6.6 失败模式”| 故障 | 行为 |
|---|---|
| 缓存不可用 | 回源限流,避免数据库被击穿 |
| 数据库主库故障 | 暂停写,读取按一致性要求降级 |
| 统计队列积压 | 重定向继续,统计延后或采样 |
| 恶意目标 URL | 黑名单、人工/自动审核、封禁 |
| 短码冲突 | 唯一约束加重试,不能覆盖旧记录 |
7. 案例:异步任务平台
跳转到“7. 案例:异步任务平台”7.1 需求
跳转到“7.1 需求”- 用户提交耗时任务,立即得到任务 ID。
- 任务有排队、运行、成功、失败、取消状态。
- 结果保存一段时间,允许查询和下载。
- 高峰期不能无限占用资源。
7.2 结构
跳转到“7.2 结构”API → 任务表 → 队列 → Worker ↑ │ └──── 状态更新 ──┘用户 → 查询/SSE/轮询 → 状态与结果任务表是事实来源,队列负责调度。任务 ID 作为幂等键;Worker 领取任务时使用可见性超时和心跳,防止任务丢失或重复执行。
7.3 关键问题
跳转到“7.3 关键问题”- 任务重复投递:处理器幂等。
- Worker 崩溃:租约过期后重新排队。
- 队列积压:限制提交、按优先级调度、扩容。
- 结果存储:对象存储 + 元数据,设置生命周期。
- 取消:标记意图,Worker 在安全点检查。
- 超大输出:流式写入,不全部放内存。
8. 安全、观测与上线
跳转到“8. 安全、观测与上线”系统设计不能最后再补安全:
- 所有资源访问检查对象级权限。
- 上传、URL、回调和第三方输入不可信。
- 密钥通过受控方式注入,日志脱敏。
- 对外接口限流、审计和错误边界。
观测至少包括请求量、错误率、延迟分位、饱和度、队列积压和依赖健康。设计时就要说明每个关键状态用哪个指标或 trace 证明。
上线要支持灰度、回滚和数据迁移。新版本写出的数据是否兼容旧版本?回滚后新字段和消息怎么办?这些问题在设计阶段比发布后便宜。
9. 用 ADR 记录取舍
跳转到“9. 用 ADR 记录取舍”标题:短链接读取缓存方案背景:读取 QPS 高,数据库主库容量有限方案:Cache-Aside + 短 TTL + 主动失效理由:允许短暂陈旧,回源可控风险:删除后短暂读到旧链接缓解:删除时主动失效,敏感链接不做长缓存验证:缓存命中率、回源 QPS、陈旧读比例架构图展示“是什么”,ADR 解释“为什么”。没有取舍记录的方案,下一次故障时很难判断是否应该坚持。
检查清单
跳转到“检查清单”- 功能范围、角色和主链路是否明确。
- 规模、延迟、可用性、一致性是否有数字目标。
- 容量估算是否覆盖 QPS、并发、存储和带宽。
- 接口是否有权限、幂等、分页和错误契约。
- 数据模型是否明确主键、时间、状态和事实来源。
- 每个组件是否有存在理由,是否说明代价。
- 是否分析热点、长尾、依赖故障和过载。
- 是否处理删除、过期、审计和访问控制。
- 是否有灰度、迁移、回滚与兼容方案。
- 关键假设和取舍是否有 ADR 或设计记录。