跳到主要内容

让你设计一个短链系统,怎么设计?

短链系统的本质是 长 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/urlsbody: 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 + Base62DB/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、异步
限流熔断与降级防刷

面试速记​

  1. 本质: 短码 ↔ 长 URL 映射;读远大于写。
  2. 短码: 雪花 + Base62 为主;hash 要处理碰撞。
  3. 读路径: Redis → DB → 302;统计异步 Kafka。
  4. 存储: 唯一索引、分片、过期与去重。
  5. 别忘: 限流、恶意 URL、302 vs 301、空值防穿透。