Files
sproutlink/shortlink-design-summary.md

228 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 短链系统设计方案对比与融合总结
## 一、核心目标
短链系统本质只做两件事:
1. **压缩**:把长网址映射成短字符串。
2. **跳转**:用户访问短链时,准确、快速地返回长链。
围绕这两件事,不同场景会衍生出容量、并发、成本、安全、业务规则等差异。
---
## 二、方案 ACloudflare Sproutlink当前项目
### 2.1 架构组成
| 组件 | 说明 |
|------|------|
| 边缘运行时 | Cloudflare Worker |
| 数据库 | Cloudflare D1SQLite |
| 静态前端 | 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. **自定义短码**:管理员可指定 132 位 `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、设备等访问控制能力。
---
## 四、两套方案核心差异对比
| 维度 | SproutlinkCloudflare | 大规模分布式方案 |
|------|--------------------------|------------------|
| **短码长度** | 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
```
---
## 六、选型建议
| 场景 | 推荐方案 |
|------|----------|
| 个人博客、小团队、私域运营、预算敏感 | **SproutlinkCloudflare** |
| 公开互联网、大厂、营销短信、微博 | **大规模分布式方案6 位 + 发号器 + Redis** |
| 希望初期低成本、后期可平滑扩展 | **融合方案**Worker/D1 起步,用户量增长后迁移到 MySQL + Redis |
---
## 七、一句话总结
> **Sproutlink 是“小而美、功能全、低成本”的私域短链方案;大规模分布式方案是“高容量、高并发、抗攻击”的公开短链标准架构。最优实践是把后者的容量与并发能力,与前者的访问规则引擎结合起来,按需分层演进。**