Initial commit: Sproutlink short URL system

This commit is contained in:
2026-06-18 20:08:22 +08:00
commit 45875e9feb
40 changed files with 11485 additions and 0 deletions

227
shortlink-design-summary.md Normal file
View File

@@ -0,0 +1,227 @@
# 短链系统设计方案对比与融合总结
## 一、核心目标
短链系统本质只做两件事:
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 是“小而美、功能全、低成本”的私域短链方案;大规模分布式方案是“高容量、高并发、抗攻击”的公开短链标准架构。最优实践是把后者的容量与并发能力,与前者的访问规则引擎结合起来,按需分层演进。**