228 lines
8.5 KiB
Markdown
228 lines
8.5 KiB
Markdown
# 短链系统设计方案对比与融合总结
|
||
|
||
## 一、核心目标
|
||
|
||
短链系统本质只做两件事:
|
||
|
||
1. **压缩**:把长网址映射成短字符串。
|
||
2. **跳转**:用户访问短链时,准确、快速地返回长链。
|
||
|
||
围绕这两件事,不同场景会衍生出容量、并发、成本、安全、业务规则等差异。
|
||
|
||
---
|
||
|
||
## 二、方案 A:Cloudflare Sproutlink(当前项目)
|
||
|
||
### 2.1 架构组成
|
||
|
||
| 组件 | 说明 |
|
||
|------|------|
|
||
| 边缘运行时 | Cloudflare Worker |
|
||
| 数据库 | Cloudflare D1(SQLite) |
|
||
| 静态前端 | Vite React SPA,通过 `ASSETS` 绑定由 Worker 直接服务 |
|
||
| 短码生成 | D1 单表原子自增 + 线性同余混洗 + Base62 编码 |
|
||
| 部署 | Serverless,静态资源不计入 Worker Bundle |
|
||
|
||
### 2.2 短码生成原理
|
||
|
||
1. **原子自增序列**:`link_seq` 表保存 `next_id`,每次创建时 `UPDATE ... SET next_id = next_id + 1 RETURNING next_id`。
|
||
2. **线性同余混洗**:把顺序的 `seq` 映射到 `[0, 62^4)` 空间,公式为 `index = (A × seq + B) mod 62^4`。
|
||
- `A`:与 `62^4` 互质的乘数(`LINK_MULTIPLIER` secret,默认内置大质数)。
|
||
- `B`:偏移量(`LINK_OFFSET` secret)。
|
||
- 双射特性保证唯一,同时让短码看起来“不连续”。
|
||
3. **Base62 编码**:字符集 `0-9A-Za-z`,固定 **4 位**,容量 `62^4 ≈ 1477 万`。
|
||
4. **自定义短码**:管理员可指定 1–32 位 `a-zA-Z0-9_-`,但不可为恰好 4 位纯英数字,避免与自动码池冲突。
|
||
|
||
### 2.3 跳转与规则校验
|
||
|
||
- 使用 **HTTP 302 临时重定向**,确保每次点击都经过 Worker,便于统计。
|
||
- 访问流程:查 D1 → 校验过期/点击上限 → 校验 Geo/设备/浏览器/IP/密码 → 302 跳转并原子递增 `clicks`。
|
||
|
||
### 2.4 业务规则能力
|
||
|
||
| 规则 | 实现 |
|
||
|------|------|
|
||
| 有效期 | `expires_at` 字段 |
|
||
| 点击上限 | `max_clicks` 字段 |
|
||
| 密码保护 | PBKDF2 哈希,密码页表单验证 |
|
||
| Geo 限制 | `request.cf.country` |
|
||
| 设备/浏览器 | `User-Agent` + `Sec-CH-UA*` |
|
||
| IP 白/黑名单 | `CF-Connecting-IP`,支持 IPv4 CIDR |
|
||
| 软删除 | `deleted` 字段 |
|
||
|
||
### 2.5 优势特点
|
||
|
||
- **成本极低**:Serverless + D1 + 静态托管,免费/低价套餐即可运行。
|
||
- **部署极简**:单人可维护,无需维护服务器、Redis、MySQL 集群。
|
||
- **短码最短**:4 位 Base62,对二维码、短信字数敏感场景友好。
|
||
- **访问规则丰富**:密码、Geo、设备、浏览器、IP 规则开箱即用,适合私域/营销场景。
|
||
- **短码防扫号**:线性同余混洗让相邻创建的短码不是连续递增,提高被遍历的难度。
|
||
|
||
### 2.6 局限
|
||
|
||
- **容量有限**:4 位码仅 1477 万,公开大规模服务很快耗尽。
|
||
- **读并发依赖 D1**:无独立缓存层,热点短链会直接把压力打到数据库。
|
||
- **无缓存穿透防护**:缺少布隆过滤器或空值缓存,面对恶意遍历存在一定风险。
|
||
- **Cloudflare 生态绑定**:迁移或私有化部署成本较高。
|
||
|
||
---
|
||
|
||
## 三、方案 B:大规模分布式短链设计(经典大厂方案)
|
||
|
||
### 3.1 架构组成
|
||
|
||
| 组件 | 说明 |
|
||
|------|------|
|
||
| 发号器 | 分布式 ID 发号器(MySQL 号段模式 / Redis 自增) |
|
||
| 数据库 | MySQL 持久化存储 `code → target_url` |
|
||
| 缓存 | Redis 读缓存 |
|
||
| 防护 | 布隆过滤器 + 空值缓存 + 网关限流 |
|
||
| 跳转状态码 | HTTP 302 |
|
||
|
||
### 3.2 短码生成原理
|
||
|
||
1. **分布式 ID 发号器**:为每个长链接分配一个全局唯一、单调递增的数字 ID。
|
||
2. **Base62 编码**:把十进制 ID 转换成 62 进制,固定 **6 位**。
|
||
- 字符集 `0-9a-zA-Z`。
|
||
- 容量 `62^6 ≈ 568 亿`,足够公开服务长期运行。
|
||
3. **唯一索引兜底**:MySQL 对 `code` 加唯一索引,防止并发冲突。
|
||
|
||
### 3.3 读链路(高并发重点)
|
||
|
||
```
|
||
用户点击 /abc123
|
||
↓
|
||
网关限流 + WAF
|
||
↓
|
||
Redis 查 sl:abc123
|
||
├─ 命中 → 直接返回 302
|
||
└─ 未命中 → 布隆过滤器判断
|
||
├─ 一定不存在 → 直接 404
|
||
└─ 可能存在 → 查 MySQL
|
||
├─ 存在 → 写入 Redis → 302
|
||
└─ 不存在 → 写短时空值到 Redis → 404
|
||
```
|
||
|
||
### 3.4 写链路
|
||
|
||
```
|
||
用户提交长链
|
||
↓
|
||
参数校验、黑名单域名过滤
|
||
↓
|
||
分布式发号器取号
|
||
↓
|
||
Base62 编码
|
||
↓
|
||
MySQL 插入(唯一索引防冲突)
|
||
↓
|
||
写入 Redis + 布隆过滤器
|
||
↓
|
||
返回短链
|
||
```
|
||
|
||
### 3.5 优势特点
|
||
|
||
- **容量巨大**:6 位码 568 亿,远超 4 位方案。
|
||
- **读性能强**:Redis 缓存扛住读多写少特性,适合热点营销短信/微博场景。
|
||
- **生产级健壮**:布隆过滤器防缓存穿透、空值缓存防击穿、网关限流防刷。
|
||
- **去 Cloudflare 依赖**:技术栈通用,易于私有化或多云部署。
|
||
|
||
### 3.6 局限
|
||
|
||
- **成本高**:需要维护 MySQL、Redis、发号器、网关等多套组件。
|
||
- **短码更长**:6 位比 4 位在短信/二维码场景略逊。
|
||
- **业务规则薄弱**:标准方案通常只讲“跳转”,缺少密码、Geo、设备等访问控制能力。
|
||
|
||
---
|
||
|
||
## 四、两套方案核心差异对比
|
||
|
||
| 维度 | Sproutlink(Cloudflare) | 大规模分布式方案 |
|
||
|------|--------------------------|------------------|
|
||
| **短码长度** | 4 位 | 6 位 |
|
||
| **容量** | 62⁴ ≈ 1477 万 | 62⁶ ≈ 568 亿 |
|
||
| **发号方式** | D1 原子自增 + 线性同余混洗 | 分布式 ID 发号器 |
|
||
| **存储** | Cloudflare D1 | MySQL + Redis |
|
||
| **读缓存** | 无 | Redis |
|
||
| **防缓存穿透** | 无 | 布隆过滤器 + 空值缓存 |
|
||
| **并发能力** | 中等,依赖 D1 | 高,依赖 Redis |
|
||
| **访问规则** | 丰富(Geo/设备/IP/密码) | 通常无或简单 |
|
||
| **部署成本** | 极低 | 较高 |
|
||
| **适用场景** | 私域、小团队、个人、轻量营销 | 公开互联网、大厂、海量用户 |
|
||
|
||
---
|
||
|
||
## 五、融合后的推荐设计
|
||
|
||
如果让我从零设计一个**可大可小、可进化的短链系统**,我会把两套方案取长补短:
|
||
|
||
### 5.1 用大规模方案做底座
|
||
|
||
- **短码长度**:默认 6 位 Base62,容量 568 亿。
|
||
- **发号器**:采用 **MySQL 号段模式**(Segment),每个实例本地批量领号,减少 DB 压力。
|
||
- **存储**:MySQL 持久化 + Redis 读缓存。
|
||
- **防护**:布隆过滤器 + 空值缓存 + 网关限流。
|
||
- **状态码**:统一使用 **302**,保证点击量可统计。
|
||
|
||
### 5.2 吸收 Sproutlink 的规则引擎
|
||
|
||
| 规则 | 实现位置 |
|
||
|------|----------|
|
||
| 有效期 | MySQL `expires_at` + 定时清理任务 |
|
||
| 点击上限 | MySQL 原子递增 `clicks` / `max_clicks` |
|
||
| 密码保护 | Worker / API 网关,密码页验证后 302 |
|
||
| Geo 限制 | CDN 边缘(Cloudflare、阿里云 CDN 等)或网关解析 `CF-IPCountry` |
|
||
| 设备/浏览器 | 解析 `User-Agent` + Client Hints |
|
||
| IP 白/黑名单 | 网关层,支持 IPv4 / IPv6 / CIDR |
|
||
| 自定义短码 | 管理员接口,与自动 6 位码池隔离 |
|
||
|
||
### 5.3 可选:保留短码混洗能力
|
||
|
||
在 6 位方案中,可以**可选地**对发号器拿到的顺序 ID 做一次可逆混洗(如线性同余),让短码看起来不连续,提升防扫号能力。注意混洗函数必须保证双射。
|
||
|
||
### 5.4 最终数据流
|
||
|
||
**写(生成短链)**:
|
||
|
||
```
|
||
提交长链
|
||
→ 校验 + 黑名单过滤
|
||
→ 号段发号器取唯一 ID
|
||
→ 可选混洗
|
||
→ Base62 编码
|
||
→ MySQL 写入
|
||
→ 写入 Redis + 布隆过滤器
|
||
→ 返回 6 位短链
|
||
```
|
||
|
||
**读(访问短链)**:
|
||
|
||
```
|
||
用户访问 /abc123
|
||
→ 网关限流/WAF
|
||
→ Redis 查缓存
|
||
├─ 命中 → 规则校验 → 302
|
||
└─ 未命中 → 布隆过滤器
|
||
├─ 不存在 → 404
|
||
└─ 可能存在 → MySQL 查询
|
||
├─ 存在 → 写 Redis → 规则校验 → 302
|
||
└─ 不存在 → 写空值缓存 → 404
|
||
```
|
||
|
||
---
|
||
|
||
## 六、选型建议
|
||
|
||
| 场景 | 推荐方案 |
|
||
|------|----------|
|
||
| 个人博客、小团队、私域运营、预算敏感 | **Sproutlink(Cloudflare)** |
|
||
| 公开互联网、大厂、营销短信、微博 | **大规模分布式方案(6 位 + 发号器 + Redis)** |
|
||
| 希望初期低成本、后期可平滑扩展 | **融合方案**:Worker/D1 起步,用户量增长后迁移到 MySQL + Redis |
|
||
|
||
---
|
||
|
||
## 七、一句话总结
|
||
|
||
> **Sproutlink 是“小而美、功能全、低成本”的私域短链方案;大规模分布式方案是“高容量、高并发、抗攻击”的公开短链标准架构。最优实践是把后者的容量与并发能力,与前者的访问规则引擎结合起来,按需分层演进。**
|