跳转到内容

系统设计方法与案例

系统设计不是把缓存、消息队列和微服务堆在一张图上,而是从需求出发,说明数据怎样流动、容量从哪里来、失败时系统做什么。面试中的白板题和真实项目都遵循同一套顺序:先明确问题,再估算,再画最小链路,最后逐个消除瓶颈。

把“做一个聊天系统”“做一个电商平台”拆成有限的功能:

  • 谁使用,有哪些角色?
  • 核心操作是什么?
  • 哪些功能必须第一版就有,哪些可以之后做?
  • 数据保留多久,是否允许删除和导出?

问题越模糊,越容易画出一个什么都能做、但不知道重点的架构。先选一条主链路,例如“用户发送消息,另一个在线用户收到”,而不是一开始就设计完整社交平台。

维度 要问什么
规模 日活、请求量、数据量、读写比例
延迟 哪些操作必须快,目标是多少分位
可用性 允许多久不可用,哪些功能优先
一致性 哪些数据不能旧,允许多久最终一致
安全 身份、权限、审计、敏感数据
成本 预算、基础设施上限、单位经济模型
运维 团队能否维护这些组件

“高可用、高并发、低延迟”不是需求,只是愿望。必须给出数量、分位和失败容忍度,才能做取舍。

估算不要求精确,但数量级要能指导设计。

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,再乘突发系数。

用 Little 定律估算:

并发请求数 ≈ 到达 QPS × 平均处理时间(秒)

100 QPS、平均处理 200 ms,约有 20 个请求同时处理。若依赖变慢到 2 秒,并发会增长到 200,连接池和线程池可能先耗尽。

每日新增数据 ≈ 每日记录数 × 单条平均字节
总存储 ≈ 每日新增 × 保留天数 × 副本/索引膨胀系数

例如每天 100 万条记录,每条 1 KB,保留 365 天,原始约 365 GB;加上索引、副本和备份,可能需要数 TB。还要考虑增长率和归档。

带宽 ≈ 每秒请求数 × 平均响应大小

1,000 QPS、每响应 50 KB,需要约 50 MB/s 出站带宽,峰值和重传会更高。大文件或媒体流必须单独估算。

接口定义系统边界,数据模型定义事实结构。

用少量核心接口描述主流程:

POST /messages
GET /conversations/{id}/messages?cursor=...
GET /users/{id}

为每个接口写清:

  • 输入、输出和错误码。
  • 身份、权限和租户边界。
  • 分页、幂等和超时。
  • 数据一致性要求。
User 1 ─── N ConversationMember N ─── 1 Conversation
Conversation 1 ─── N Message
User 1 ─── N Message

先确定主键、时间、状态和索引,再考虑缓存。用户、消息和会话分别以什么为事实来源?删除消息是物理删除、软删除还是只对某些人隐藏?这些语义直接影响 API 和存储。

一个完整的请求链路通常包括:

客户端
→ CDN/WAF
→ 负载均衡/API 网关
→ 应用服务
→ 缓存/数据库/队列
→ 外部依赖

先画最小版本,再问每个组件的必要性:

  • 没有 CDN,静态资源从哪里来?
  • 没有缓存,数据库能否承受?
  • 没有队列,长任务是否阻塞请求?
  • 没有副本,单机故障怎样恢复?

每个组件都增加部署、监控和故障面。能用一个数据库解决时,不必先上分布式搜索引擎。

读多写少可以缓存、副本和 CDN;写多读少更关注批处理、队列、分片和索引成本。不要给所有接口套同一种缓存。

热门内容、热门用户和大租户会造成单键或单分区压力。解决方式可能包括本地缓存、请求合并、读副本、热点拆分和限流。分片键设计要避免所有流量落到同一片。

总吞吐够,但少数请求很慢。需要超时、并发限制、降级和可观测性。一个慢查询占用连接池,会拖慢同实例其他请求。

依赖变慢时,上游线程和连接被占满,最终自身也失败。通过超时、熔断、舱壁和背压阻断传播,而不是无限重试。

  • 用户提交长 URL,得到短码。
  • 访问短链接时重定向到原始 URL,返回 301/302。
  • 支持自定义短码、过期时间、访问统计和删除。
  • 目标可用性 99.9%,读取远多于写入。
  • 不允许任意用户查看或修改他人的私有链接。

假设每天新增 100 万条短链,平均每条记录 500 字节,保留 5 年:

原始数据 ≈ 100 万 × 500 B × 365 × 5 ≈ 0.9 TB
写入 QPS 平均 ≈ 12,峰值按 10 倍约 120
读取如果为写入的 100 倍,平均约 1,200 QPS

读取量明显更大,适合缓存和 CDN/边缘优化;写入需要短码唯一性与防重复策略。

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 可预测,私有链接仍必须鉴权,不能靠“别人猜不到”当安全。

GET /{code}
→ 读取缓存
→ 未命中查数据库
→ 检查过期/删除/状态
→ 记录异步统计
→ 返回重定向

统计写入不应阻塞重定向主链路,可以进队列批量聚合。缓存键包含短码和版本;删除或更新时主动失效。恶意短码、开放重定向和钓鱼目标需要审核与黑名单。

  • 读多:CDN、只读副本、进程内/分布式缓存。
  • 写多:按 owner 或短码哈希分片。
  • 热点:本地缓存与请求合并。
  • 统计:事件流、批处理、预聚合。
  • 可用性:多实例、数据库故障转移、缓存降级。
故障 行为
缓存不可用 回源限流,避免数据库被击穿
数据库主库故障 暂停写,读取按一致性要求降级
统计队列积压 重定向继续,统计延后或采样
恶意目标 URL 黑名单、人工/自动审核、封禁
短码冲突 唯一约束加重试,不能覆盖旧记录

7. 案例:异步任务平台

跳转到“7. 案例:异步任务平台”
  • 用户提交耗时任务,立即得到任务 ID。
  • 任务有排队、运行、成功、失败、取消状态。
  • 结果保存一段时间,允许查询和下载。
  • 高峰期不能无限占用资源。
API → 任务表 → 队列 → Worker
↑ │
└──── 状态更新 ──┘
用户 → 查询/SSE/轮询 → 状态与结果

任务表是事实来源,队列负责调度。任务 ID 作为幂等键;Worker 领取任务时使用可见性超时和心跳,防止任务丢失或重复执行。

  • 任务重复投递:处理器幂等。
  • Worker 崩溃:租约过期后重新排队。
  • 队列积压:限制提交、按优先级调度、扩容。
  • 结果存储:对象存储 + 元数据,设置生命周期。
  • 取消:标记意图,Worker 在安全点检查。
  • 超大输出:流式写入,不全部放内存。

系统设计不能最后再补安全:

  • 所有资源访问检查对象级权限。
  • 上传、URL、回调和第三方输入不可信。
  • 密钥通过受控方式注入,日志脱敏。
  • 对外接口限流、审计和错误边界。

观测至少包括请求量、错误率、延迟分位、饱和度、队列积压和依赖健康。设计时就要说明每个关键状态用哪个指标或 trace 证明。

上线要支持灰度、回滚和数据迁移。新版本写出的数据是否兼容旧版本?回滚后新字段和消息怎么办?这些问题在设计阶段比发布后便宜。

标题:短链接读取缓存方案
背景:读取 QPS 高,数据库主库容量有限
方案:Cache-Aside + 短 TTL + 主动失效
理由:允许短暂陈旧,回源可控
风险:删除后短暂读到旧链接
缓解:删除时主动失效,敏感链接不做长缓存
验证:缓存命中率、回源 QPS、陈旧读比例

架构图展示“是什么”,ADR 解释“为什么”。没有取舍记录的方案,下一次故障时很难判断是否应该坚持。

  • 功能范围、角色和主链路是否明确。
  • 规模、延迟、可用性、一致性是否有数字目标。
  • 容量估算是否覆盖 QPS、并发、存储和带宽。
  • 接口是否有权限、幂等、分页和错误契约。
  • 数据模型是否明确主键、时间、状态和事实来源。
  • 每个组件是否有存在理由,是否说明代价。
  • 是否分析热点、长尾、依赖故障和过载。
  • 是否处理删除、过期、审计和访问控制。
  • 是否有灰度、迁移、回滚与兼容方案。
  • 关键假设和取舍是否有 ADR 或设计记录。