cswork / 系统设计

第 3 章 / 共 57 章 · 约 7 分钟

案例 1:短链接服务

本页目录
  1. 1. 需求澄清
  2. 2. 容量估算
  3. 3. API
  4. 4. 数据模型
  5. 5. 高层架构
  6. 6. 深入讨论
  7. 7. 关键取舍
  8. 8. 常见追问
  9. 9. 回答分级
  10. 10. 不同级别的期望
  11. 11. 面试节奏建议
  12. 章节测验

先花 10 分钟自己设计:需求、核心实体、API 和高层架构,各写几行。写完或者跳过之后,下面的讲解才会展开。

先写你的思路

只存在这台设备上;登录后会存进你的账号。

短链接是系统设计面试里最常见的“热身题”。业务逻辑几乎为零,所以面试官真正考的是:你能不能把一个简单需求拆清楚,算清读写比,讲明白 ID 怎么生成,以及热路径怎么做到毫秒级。

1. 需求澄清

功能需求

  • 输入长链接,返回短链接;可选自定义别名(custom alias)和过期时间
  • 访问短链接时重定向到原始地址
  • (可选)点击统计:次数、来源、地区

非功能需求

  • 重定向延迟低:服务端 p99 在 10~20 ms 级别(不含公网往返)
  • 高可用:短链一旦失效,别人发出去的推文、邮件里的链接全部变成死链
  • 读远多于写,假设 100:1
  • 统计数据允许最终一致;短码不应被轻易顺序枚举

同时说清楚不做什么:用户体系、链接编辑、付费功能都放到范围外。这一步能帮你把时间留给真正的考点。

2. 容量估算

指标估算
新建1 亿条/天 ≈ 1.2k QPS,峰值按 3 倍 ≈ 3.5k
重定向100 亿次/天 ≈ 115k QPS,峰值 ≈ 350k
单条大小长 URL + 元数据 ≈ 500 B
存储50 GB/天,5 年 ≈ 90 TB(不含副本)

短码长度:Base62(a-z、A-Z、0-9)7 位可表示 62^7 ≈ 3.5 万亿个;5 年总量约 1800 亿,只占约 5%,足够。估算的目的是得出结论:单机数据库存不下,读路径必须靠缓存扛。

3. API

POST /v1/links
  body: { long_url, custom_alias?, expires_at? }
  201 → { code, short_url }

GET /{code}
  302 Location: <long_url>    // 不存在或已过期返回 404

301 还是 302? 301 是永久重定向,浏览器会缓存,之后的点击不再经过你的服务器:省流量,但统计不到,也无法中途下线恶意链接。302/307 每次都回源,方便统计和管控。大多数商业短链选 302,这是一个值得主动说出来的取舍。

4. 数据模型

links
  code        string     PK   -- 7 位 Base62
  long_url    string
  owner_id    string
  created_at  timestamp
  expires_at  timestamp  NULL

访问模式只有“按 code 点查”,没有 join,也没有范围查询,因此 KV 存储(DynamoDB、Cassandra)或按 hash(code) 分片的 MySQL 都合适。说清选择理由比选哪个更重要。

5. 高层架构

短链接:写路径与读路径
滚动到此处后加载图表…
图表可横向滚动,展开后可缩放查看。
查看图表源码
flowchart LR
  accTitle: 短链接:写路径与读路径
  c1["Client(创建)"] --> lb["LB"] --> linksvc["Link Service"] --> gen["短码生成"] --> kv[("KV Store")]
  c2["Client(点击)"] --> cdn["CDN/Edge"] --> redirect["Redirect Service"] --> redis[("Redis")]
  redis -->|"miss"| kv
  redirect --> evt["点击事件"] --> kafka["Kafka"] --> stats["统计管道"]

写服务和重定向服务分开部署:两者 QPS 相差两个数量级,扩缩容策略完全不同。

读路径最前面的 CDN/Edge 要和统计、下线放在一起考虑。CDN 一旦缓存了 302,命中边缘的点击根本到不了 Redirect Service,统计会漏掉;下线恶意链接也得先清 CDN。三种做法任选其一,并在面试里说清楚:

  1. 重定向响应带 Cache-Control: private, no-store,CDN 只负责 TLS 终止和抗 DDoS,不缓存重定向
  2. 允许边缘缓存,但 TTL 很短(比如 60 秒),点击统计改用 CDN 访问日志或边缘函数上报
  3. 下线链接时调用 CDN 的 purge 接口主动清除

本文默认第 1 种:每次点击都经过 Redirect Service,由它投递点击事件。

6. 深入讨论

6.1 短码生成(核心考点)

方案做法优点问题
哈希截断MD5(url) 取若干位转 Base62,截成 7 位无状态必然有碰撞,要查重重试;同一 URL 总得到同一码,按用户区分需加盐
全局自增 + Base62自增 ID 转 62 进制无碰撞中心化计数器是单点;码连续,可被枚举
号段(range)每台机器从 ZooKeeper/DB 领一段,如 1000 个无碰撞,几乎不需协调机器宕机浪费号段(可接受);仍需打散
随机 + 条件写随机 7 位,PUT IF NOT EXISTS实现最简单,不可预测占用率越高碰撞越多,需监控

推荐答案之一:号段计数器 + 可逆打散。对 ID 做一次双射置换(例如乘以一个与 62^7 互质的数再取模,或用小型 Feistel 网络),既保证唯一,又让相邻 ID 生成的码看起来随机。注意这不是加密,只是防止被肉眼顺序枚举。另一种同样站得住的答案是“随机 + 条件写”:5% 占用率下首次碰撞概率约 5%,重试一次基本就能成功。

6.2 读路径

  • Cache-aside:先查 Redis,未命中再查 KV 并回填。按二八分布,缓存 20% 的热点链接就能覆盖大部分流量。
  • 负缓存:不存在的 code 也缓存一个短 TTL 的空值,或在存储前放布隆过滤器,防止有人用随机 code 打穿数据库。布隆过滤器的规模要先算:5 年 1800 亿个 key、1% 误判率,大约需要 200 GB,必须按 code 分片;标准布隆过滤器也不支持删除,过期的短码要靠 counting 布隆过滤器或定期重建来清掉。对异常来源 IP 限流可以同时使用。
  • 爆款链接:进程内 LRU 兜底,避免单个 Redis 分片过热。

6.3 自定义别名:依赖存储的唯一约束(条件写)解决并发抢注;为避免与生成码冲突,可要求别名长度不等于 7,或使用保留前缀。

6.4 过期:读取时检查 expires_at 保证正确性;后台依靠存储自带 TTL 或批处理任务删除。

6.5 统计:重定向服务只往 Kafka 异步投递事件,绝不能同步写统计库,否则统计系统一抖动就会拖垮主链路。

6.6 防滥用:创建接口限流;对长链接做异步恶意网址检测,命中后封禁 code 并清除 Redis 缓存,如果 CDN 也缓存了重定向还要 purge(这也是选 302 的理由之一)。

6.7 多区域:重定向是全球流量,可以在每个区域部署只读副本和缓存,写入集中到主区域;或者给每个区域分配互不重叠的号段,让各区域独立生成短码而不冲突。新建链接复制到其他区域可能有几百毫秒延迟,本地读不到时回源主区域即可。

7. 关键取舍

  • 301 省成本 vs 302 可控、可统计
  • 计数器无碰撞 vs 随机码不可预测
  • 别名唯一性要强一致 vs 统计数据最终一致

8. 常见追问

  • 同一个长链接重复提交,要返回同一个短码吗? 取决于产品。若要去重,就需要一张 hash(long_url) → code 的反向索引,多一次写入,也多一个一致性问题;多数场景直接生成新码更简单,也方便按用户分别统计。
  • 怎么统计独立访客(UV)? 精确去重成本高,可以用 HyperLogLog 按天近似计数,约 1% 的误差在统计场景完全可以接受。
  • 存储挂了还能重定向吗? 热点链接仍在缓存里,大部分流量不受影响;可以适当拉长缓存 TTL,存储故障期间继续返回旧值(serve stale),先保可用。

9. 回答分级

Bad:开口就是“MD5 存 MySQL”;没有估算;不提碰撞;统计在重定向时同步写库;缓存只有一句“加个 Redis”。

Good:需求清晰;算出 7 位 Base62 够用;读写服务分离;cache-aside;讲清 301/302;至少对比两种短码生成方案。

Great:用数字论证选型(碰撞率、号段大小);主动提出枚举风险和可逆打散;考虑负缓存、热点链接;说明号段服务宕机时各节点仍持有剩余号段可继续工作;也清楚哪些地方不值得过度设计。

10. 不同级别的期望

级别期望
Mid(L4)在提示下给出完整可用的设计,API、数据模型、缓存都正确
Senior(L5)自己驱动节奏,量化论证生成方案,主动找出读路径瓶颈并深入 1~2 个点
Staff(L6+)从业务出发谈取舍(统计 vs 成本、滥用治理),覆盖多区域部署、故障模式和演进路径,并说明哪里刻意保持简单

11. 面试节奏建议

45 分钟里,需求和估算控制在 8 分钟以内,API 与数据模型约 5 分钟,高层架构约 10 分钟,剩下的时间留给短码生成和读路径这两个深入点。估算的作用是支撑结论,不是展示算术,算到能做决定就停。

章节测验

6 道选择题
  1. 为什么多数商业短链服务选择 302 而不是 301?
  2. 7 位 Base62 短码大约能表示多少个不同的码?
  3. “全局自增 ID + Base62”方案最主要的问题是?
  4. 有爬虫在大量请求随机且不存在的短码,最直接有效的防护是?
  5. 点击统计数据应该怎么写入?
  6. “随机 7 位 + 条件写”方案中,短码空间占用约 5% 时,哪项描述正确?

章节讨论

正在加载讨论…