系统设计:URL 短链服务
设计类似 bit.ly 的短链服务,支持:
- 缩短长 URL
- 根据短码重定向到原始 URL
- 高可用、低延迟(< 100ms)
1. 需求分析
功能性需求
- 短链生成:输入长 URL,返回短码
- 重定向:短码 301/302 跳转至长 URL
- 自定义短码(可选)
- 失效时间(可选)
非功能性需求
- 高可用:99.99%
- 低延迟:读写 < 100ms
- 高并发:10 亿次/日 ≈ 11,574 QPS,峰值 100,000 QPS
- 短码长度:6-8 字符(62 进制:a-zA-Z0-9)
2. 容量估算
- 日新增:1000 万条
- 年新增:36.5 亿条
- 存储:每条 ≈ 500 字节(短码 + 长 URL + 元数据)
- 年存储:3650 万 × 500B ≈ 18.25 GB
3. 数据库设计
CREATE TABLE urls (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(8) UNIQUE NOT NULL,
long_url VARCHAR(2048) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
click_count BIGINT DEFAULT 0,
INDEX idx_short_code (short_code)
);
4. 短码生成策略
方案一:Base62 编码(发号器)
import string
BASE62 = string.ascii_letters + string.digits
def encode_base62(num):
if num == 0:
return BASE62[0]
result = []
while num > 0:
result.append(BASE62[num % 62])
num //= 62
return ''.join(reversed(result))
def decode_base62(s):
result = 0
for char in s:
result = result * 62 + BASE62.index(char)
return result
发号器:
- 单机:自增 ID
- 分布式:Snowflake(避免单点)、号段模式(数据库批量取号)
方案二:MD5 哈希取前 6 位
import hashlib
def short_code_md5(url):
return hashlib.md5(url.encode()).hexdigest()[:6]
问题:哈希冲突,需处理碰撞。
方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 发号器 | 无冲突、趋势递增 | 可能预测 |
| 哈希 | 分布均匀 | 冲突处理复杂 |
5. 系统架构
Client → CDN/边缘缓存 → API Gateway → Load Balancer
↓
┌─────────────┼─────────────┐
↓ ↓ ↓
Read Cache Write API Analytics
↓ ↓
Redis MySQL (Master-Slave)
↓ ↓
Bloom Filter Sharding
缓存策略
读取优化:
- 短码 → 长 URL 映射缓存在 Redis(TTL 24h)
- 命中缓存直接返回,延迟 < 1ms
- 未命中查数据库并回填缓存
布隆过滤器:
- 防止缓存穿透
- 不存在的短码快速返回 404
6. 代码实现
from fastapi import FastAPI, HTTPException, RedirectResponse
import redis
import hashlib
app = FastAPI()
redis_client = redis.Redis(host='localhost', port=6379, db=0)
@app.post("/shorten")
def shorten_url(long_url: str, custom_code: str = None):
if custom_code:
short_code = custom_code
else:
# 使用哈希 + 冲突处理
for i in range(10):
candidate = hashlib.md5(f"{long_url}:{i}".encode()).hexdigest()[:6]
if not redis_client.exists(candidate):
short_code = candidate
break
else:
raise HTTPException(500, "无法生成短码")
redis_client.setex(short_code, 86400, long_url)
return {"short_url": f"https://short.ly/{short_code}"}
@app.get("/{short_code}")
def redirect(short_code: str):
long_url = redis_client.get(short_code)
if not long_url:
raise HTTPException(404, "短码不存在")
return RedirectResponse(url=long_url.decode(), status_code=301)
7. 性能优化
| 策略 | 效果 |
|---|---|
| Redis 缓存 | 读 QPS 提升 1000x |
| CDN 边缘缓存 | 全球延迟降低至 < 50ms |
| 数据库读写分离 | 写不阻塞读 |
| 分片(Sharding) | 数据库水平扩展 |
8. 面试常见问题
Q: 301 vs 302 重定向的区别?
- 301:永久重定向,浏览器缓存,减少后续请求
- 302:临时重定向,每次都访问短链服务(便于统计)
Q: 如何处理哈希冲突?
- 开放寻址:尝试下一个槽位
- 链地址法:同一桶内链表存储
- 再哈希:换一个哈希函数
Q: 短码长度为什么是 6 位?
62^6 ≈ 568 亿,足够覆盖大部分场景,同时保持 URL 简洁。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。