面试题:设计短链接系统——短码怎么生成才不碰撞、跳转用 301 还是 302、读写比 100:1 的存储与缓存怎么选?
这是「每日一题」专栏的第十八篇。每天一道面试题,后端 + AI 混合路线,从原理到代码一次讲清。上一篇讲了 分布式限流怎么做,今天按轮换回到系统设计方向,兑现昨天的预告:短链接系统。8 月 12 日已经写过一版 基础篇,今天这版是进阶篇——聚焦预告点名的三个硬核考点:短码怎么生成才不碰撞、跳转到底用 301 还是 302、读写比悬殊的存储与缓存怎么选,顺便把基础篇里「跳转一律用 302」的简化结论升级成完整决策框架。
一、题目与三个考点
需求老规矩先澄清,但这里只列影响架构的几条:
- 写入 1 亿条/天,读取 100 亿次/天,读写比约 100:1,读峰值约 35 万 QPS;
- 短码 7 位 Base62(62 的 7 次方 ≈ 3.5 万亿组合,容量估算详见基础篇);
- 短链永久有效,跳转延迟目标低于 100ms。
三个考点正好对应三层:算法层(短码生成不碰撞)、协议层(301/302 的取舍)、存储层(读写比悬殊的选型)。下面逐层拆。
二、短码生成:不碰撞的三个流派
「不碰撞」是硬需求——短码就是主键,撞了等于数据丢失。三个流派各有取舍:
流派一:随机短码 + 唯一索引重试
每次生成 7 位随机 Base62 串,靠数据库唯一索引兜底:
import random, string
ALPHABET = string.digits + string.ascii_letters # 62 个字符
def random_code(length=7):
return ''.join(random.choice(ALPHABET) for _ in range(length))
def create_short_url(url, db):
for _ in range(5): # 最多重试 5 次
code = random_code()
try:
db.insert(code, url) # 唯一索引冲突直接抛异常
return code
except UniqueViolation:
continue # 撞了换一个重试
raise TooManyCollisions
两个关键细节:
- 不先 SELECT 再 INSERT。先查再插有 TOCTOU 竞态——两个请求同时查都说不存在,然后都插入,还是撞。正确姿势是直接 INSERT、依赖唯一索引捕获冲突异常重试,让「检测 + 插入」由数据库原子完成。
- 重试次数要封顶。7 位码空间 3.5 万亿,亿级数据下碰撞概率远低于 1%(生日悖论),5 次重试绰绰有余;但上限必须有,防止极端情况死循环。
流派二:发号器 + Base62(工业界主流)
不碰概率,从源头消灭碰撞:先取一个全局唯一 ID,再编码成短码。
# 伪代码:号段式发号器
# Redis INCR / DB 号段 / Snowflake 变体,取到全局唯一 ID
code = base62_encode(get_next_id()) # base62 实现见基础篇
# 7 位 Base62 空间约 3.5 万亿,号段分配下绰绰有余
# 示例:ID=100 → "1C",ID=62^7 - 1 → "ZZZZZZZ"
优势:零碰撞重试,写入路径干净,高并发下没有异常风暴;代价是短码可被顺序推算(ID 递增,业务量泄漏)。缓解手段:编码后对字符做一次固定置换(乱序表),或号段内加随机步长。
流派三:预生成池
后台线程批量生成短码、查重后灌入 Redis 队列,写入时 RPOP 直接取——O(1) 出码、零等待。适合写入有瞬时峰值的场景(比如活动期的建链洪峰)。代价是池子要维护水位监控,池子空了会退化回碰撞重试。
三流派对比
| 流派 | 碰撞处理 | 写入代价 | 幂等性 | 复杂度 |
|---|---|---|---|---|
| 随机码 + 唯一索引重试 | 概率低 + 重试兜底 | 异常路径重试 | 不幂等 | 低 |
| 发号器 + Base62 | 源头消灭 | 一次取号 | 不幂等 | 中 |
| 预生成池 | 预查重 | O(1) 出码 | 不幂等 | 中高 |
加分点:答完三流派后主动补一句「同一 URL 重复创建要幂等」——对长 URL 做哈希,命中直接返回已有短码,避免垃圾数据。这是区分背题和真懂的分水岭。
三、301 还是 302:先反问产品需求
这是预告点名的考点,也是基础篇被简化掉的地方。先把语义讲死:
- 301 永久重定向:浏览器、代理、CDN 都会缓存这个响应,下次访问直接跳目标地址,不再回源;
- 302 临时重定向:每次访问都要回源问一次短链服务。
所以这不是技术判断题,是产品判断题:
| 产品诉求 | 选择 | 原因 |
|---|---|---|
| 要点击统计、要动态分流 | 302 | 每次回源才能记账、才能按设备/地区/时间动态决定目标 |
| 纯工具型、极致省成本 | 301 | 热点短链被浏览器和 CDN 缓存,服务器压力骤降,跳转更快 |
| 营销链接要统计 + 长尾要省成本 | 混合 | 统计类走 302,普通链接走 301 + CDN 日志离线分析 |
两个进阶细节:
- 301 也能做统计——短链域名接入 CDN,CDN 访问日志本身就是全量点击数据,离线分析即可,不需要每次回源。统计需求不是 302 的充分条件。
- 过期短链的语义:永久链接指向的资源失效时,专业做法是返回 410 Gone(明确的「资源已删除」,搜索引擎会摘除索引),而不是 302 到一个错误页。
四、读写比 100:1:存储层与缓存层怎么选
1. 存储层:这是 KV 语义,不是关系语义
短链映射是教科书级的 KV 负载:code → url,无 join、无范围查询、单点读写。基础篇给的 MySQL 分片方案能用,但生产上更常见的答案其实是 KV 存储(DynamoDB / Cassandra / 自研分片 KV):
- 按 code 哈希分区,读放大极小(一次点查);
- 天然水平扩展,容量从 TB 级到 PB 级无感;
- 只追加写入,无二级索引维护成本。
选 MySQL 也不丢分,但要说清楚「为什么关系型在这里是杀鸡用牛刀」——面试官想听的是你分得清负载类型。
一致性:用户刚创建的短链必须立即可访问。MySQL 主从有同步延迟,读从库可能读到不存在——所以写路径要写穿透:写主库的同时写/删缓存,读路径优先命中缓存,天然规避从库延迟。
2. 缓存层:Cache-Aside + 三个经典坑
读多写少、100:1 的读写比,缓存是必选项,模式就是 Cache-Aside(读未命中回填、写时先写库再删缓存)。但短链场景有三个必须主动讲的坑:
坑一:缓存击穿——热点短码回源风暴。一条被大 V 转发的短链,缓存过期瞬间可能有几十万 QPS 同时回源打 DB。解法:单 key 重建互斥锁——Redis SETNX 拿锁,拿到锁的请求查库回填,其余请求短暂等待或返回旧值(逻辑过期方案)。
坑二:缓存穿透——不存在的短码。攻击者拿随机码批量探测,全部未命中、全部打 DB。解法:布隆过滤器前置(内存判存在性,不存在直接拒绝),或对空结果做短 TTL 缓存。
坑三:缓存雪崩——批量过期。大量 key 同一时刻过期(比如同时批量创建的营销短链),回源流量叠加成峰。解法:TTL 加随机抖动(基础 TTL 加减 10%~30% 的随机量)。
3. 多级缓存:短链是最适合 CDN 缓存的负载之一
短链映射只读稳定、无个性化,301 响应本身就是可缓存的静态资源。完整链路是四层:
浏览器/客户端
│ 命中即跳转,零回源
CDN 边缘(301 响应缓存,热点流量直接扛住)
│
网关本地缓存(Caffeine/进程内,微秒级)
│
Redis 集群(全局共享,毫秒级)
│
KV 存储(兜底,最终一致)
CDN 层是真正的杀器:把 301 响应按短码缓存到边缘节点,热点短链的流量根本到不了后端。这也是「301 + CDN 日志统计」组合能成立的原因——缓存和统计双赢。
五、面试追问合集
| 追问 | 回答要点 |
|---|---|
| 碰撞重试会不会打爆数据库? | 碰撞概率极低,重试是异常路径不是常态;发号器流派直接消灭 |
| 301 被浏览器缓存了还怎么统计? | CDN 访问日志全量离线分析;或对统计类链接单独走 302 |
| 缓存全挂了怎么办? | 降级直连 KV 存储 + 限流保护(fail-open + 本地兜底,见 限流篇) |
| 自定义短码(用户指定 alias)怎么处理? | 唯一索引校验 + 保留字黑名单 + 长度限制 |
| 短码空间用完了怎么办? | 7 位 3.5 万亿远够;真用完就加长到 8 位,老码兼容 |
| 怎么防滥用刷接口? | 令牌桶限流 + 恶意域名检测 + 创建配额 |
六、总结
短链接这道题,从基础篇到进阶篇串起来就是三层答案:
- 算法层:短码生成三流派——随机码 + 唯一索引重试(简单可靠)、发号器 + Base62(工业界主流、零碰撞)、预生成池(峰值吞吐);核心是「不先查再插,直接依赖唯一索引」;
- 协议层:301/302 是产品判断题——要统计走 302,要省成本走 301 + CDN 缓存 + 日志分析;
- 存储层:KV 语义选 KV 存储,Cache-Aside 兜底,防击穿/穿透/雪崩三件套,CDN 边缘缓存是终极大杀器。
能答出「为什么是 KV 而不是 MySQL」「301 缓存了怎么统计」「缓存挂了怎么降级」这三个追问,这道题就是满分。
明日预告:AI/LLM 方向——「投机解码(Speculative Decoding):草稿模型怎么选、为什么能提速 2~3 倍、和 KV Cache 怎么配合?」