让你设计一个短链系统,怎么设计?
短链系统的本质是 长 URL ↔ 短码 的双向映射,线上 读多写少(一次生成、多次点击)。面试按 澄清 → 估算 → 架构 → 细节 → 扩展 答;ID 与一致性见 分布式系统基础;缓存见 Redis;API 语义见 API 设计与 HTTP 实践。
Step 1:澄清需求
| 问题 | 影响设计 |
|---|---|
| 日新增 / 读 QPS? | 存储容量、缓存、是否分片 |
| 短码长度、字符集? | 62^7 ≈ 3.5 万亿,一般 6~8 位 base62 |
| 自定义 alias? | 唯一索引冲突、鉴权 |
| 过期 TTL? | 定时删除 / lazy expire |
| 点击统计? | 异步日志、Kafka 聚合 |
| 302 vs 301? | 统计是否准确、SEO、缓存行为 |
| 恶意 URL? | 审核、黑名单、限流 |
默认假设(可口述): 无自定义码;读:写 ≈ 100:1;要 PV/UV 统计;302 跳转。
Step 2:粗算规模(示例)
| 项 | 假设 | 量级 |
|---|---|---|
| 日新增短链 | 1000 万 | ~116 QPS 写峰值 ×3 ≈ 350 写 QPS |
| 日跳转 | 10 亿 | ~12k 读 QPS 均值,峰值 ×5~10 |
| 存 5 年 | 1000万×365×5 | ~180 亿行 → 分库分表 |
| 单行 | short_code + long_url + meta | ~500B~2KB(看 URL 长度) |
结论:读路径必须缓存;写路径可异步削峰;存储按 short_code 分片。
Step 3:核心 API
| API | 说明 |
|---|---|
POST /api/v1/urls | body: long_url, 可选 ttl → 返回 short_url |
GET /{short_code} | 302 Found + Location: long_url(或 301) |
GET /api/v1/urls/{code}/stats | 可选:点击量、时间分布 |
REST 与状态码见 API 设计与 HTTP 实践。创建成功 201;短码不存在 404;重复自定义码 409。
Step 4:架构总览
┌─────────────┐
POST 缩短 ───────►│ API 服务 │──► 生成 short_code
└──────┬──────┘
│ 写 DB + 写 Cache
▼
┌─────────────┐
│ MySQL/PG │ 映射真源(分片)
│ short→long │
└──────┬──────┘
│
GET /{code} ───► ┌───────▼───────┐ miss
│ Redis 缓存 │────────► 回源 DB
└───────┬───────┘
│ 302
▼
客户端浏览器
点击事件 ──► Kafka ──► 统计 / 实时 PV(异步,不挡跳转)
Step 5:短码怎么生成
| 方案 | 做法 | 优缺点 |
|---|---|---|
| 自增 ID + Base62 | DB/Redis 取号 → 编码为 aZ9xK2 | 简单、无碰撞;需全局唯一 ID(雪花) |
| Hash 截断 | MD5(url) 取前 7 位 | 同 URL 复用;碰撞要查库重试 |
| 随机 + 查重 | 随机 base62,冲突则重 roll | 实现简单;高占用率时重试多 |
| 预生成池 | 后台批量生成空码入池 | 写路径快;池子要运维 |
面试推荐答: 雪花/号段发号 + Base62 编码,天然唯一;表上对 short_code 建唯一索引兜底。
id = snowflake.next() → code = base62(id) → INSERT mapping
Step 6:存储模型
-- 逻辑表(分片键:short_code 或 id)
CREATE TABLE url_mapping (
id BIGINT PRIMARY KEY,
short_code VARCHAR(16) NOT NULL UNIQUE,
long_url VARCHAR(2048) NOT NULL,
long_url_hash CHAR(64), -- 可选:同长链去重
created_at TIMESTAMP,
expire_at TIMESTAMP NULL,
user_id BIGINT NULL
);
| 点 | 说明 |
|---|---|
| 分片 | 按 short_code hash 或 range;读跳转只带 code,均匀 hash 分片常见 |
| 去重 | 同一 long_url_hash 返回已有短链,省存储 |
| 过期 | expire_at + 定时任务删;读时 lazy 删缓存 |
Step 7:读路径(性能关键)
Cache-Aside:
GET code
→ Redis GET url:{code}
→ hit:302 Location
→ miss:DB SELECT → 回写 Redis(TTL)→ 302
| 实践 | 原因 |
|---|---|
| TTL + 随机抖动 | 防雪崩 |
| 空值缓存短 TTL | 防穿透(不存在的码) |
| 热 key | 本地缓存 / 多副本 ——见 Redis |
| 302 默认 | 便于统计每次点击;301 会被浏览器缓存少计 PV |
跳转链路 尽量短:API 网关 → 跳转专用服务 → Redis,少调下游。
Step 8:写路径
POST long_url
→ 校验 URL / 恶意检测
→ 去重(optional)
→ 取号 + base62
→ INSERT(唯一索引防并发重复)
→ 写 Redis(可选预热)
→ 201 返回 short_url
| 手段 | 说明 |
|---|---|
| 幂等 | Idempotency-Key 或业务去重 |
| 限流 | 防刷 —— 限流熔断与降级 |
| 异步 | 统计、审核放 MQ,不阻塞创建 |
Step 9:统计与分析
跳转成功 → 发 Kafka(code, ts, ip, ua, referer)
→ Flink / 消费组聚合 → Redis INCR 或 OLAP
不在跳转同步路径写 DB 计数——会拖慢 302。允许 最终一致 的 PV。
Step 10:安全与滥用
| 风险 | 手段 |
|---|---|
| 钓鱼/恶意长链 | URL 黑名单、第三方安全 API、人工审核 |
| 刷接口 | 网关限流、验证码、用户级配额 |
| 遍历短码 | 足够长的随机空间;敏感跳转鉴权 |
| 开放重定向漏洞 | 只允许 http(s);禁止 javascript: |
扩展追问(按需答)
| 追问 | 方向 |
|---|---|
自定义 go/my-brand | 单独表 + 用户鉴权 + 409 冲突 |
| 全球低延迟 | 多地 Redis + DB 多主/单元化;302 就近 |
| 短链预览 | 中间页展示目标域名再跳转 |
| 高可用 | 无状态服务多副本;Redis Cluster;DB 主从 + 故障切换 |
常见坑
| 坑 | 说明 |
|---|---|
| 只用 hash 不处理碰撞 | 小概率撞码导致错跳 |
| 跳转路径同步写统计 | P99 爆炸 |
| 301 与统计矛盾 | 要说清产品选 302 的原因 |
| 忽略恶意 URL | 短链成钓鱼帮凶 |
| 单表扛到百亿 | 早做分片与归档 |
面试参考答法(90 秒)
我先澄清 QPS 和要不要统计。短链是读多写少,核心是 short_code 到 long_url 的映射。
写上用雪花发号加 Base62 生成短码,落 MySQL 并建唯一索引,可选同 URL 去重。读路径 GET 短码:先 Redis,miss 再 DB,返回 302 到长链;读 QPS 高所以缓存必选,热 key 可本地再挡一层。
统计用 Kafka 异步,不挡跳转。安全上加限流、恶意 URL 检测。数据量大按 short_code hash 分库分表,过期用 TTL 加定时清理。API 上 POST 创建返回 201,跳转 404 表示码不存在。
延伸阅读
| 篇 | 关系 |
|---|---|
| 分布式系统基础 | 雪花 ID、幂等、CAP |
| Redis | 缓存、INCR 计数 |
| 缓存与消息队列 | Cache-Aside、异步 |
| 限流熔断与降级 | 防刷 |
面试速记
- 本质: 短码 ↔ 长 URL 映射;读远大于写。
- 短码: 雪花 + Base62 为主;hash 要处理碰撞。
- 读路径: Redis → DB → 302;统计异步 Kafka。
- 存储: 唯一索引、分片、过期与去重。
- 别忘: 限流、恶意 URL、302 vs 301、空值防穿透。