第一个短链接
某个周末,我在社交媒体上分享一个链接。那个 URL 长成这样:
https://www.example.com/search?q=系统设计课程&category=计算机&price=100-500&sort=sales
80 多个字符,占了半行字。发出去之后,整条动态的观感极差。
我盯着屏幕想了几秒——自己做一个短链接工具?反正周末没事。
理想中的效果:
https://short.url/a1b2c3
干净利落。
我的原则:先跑起来再说。
第一版只解决两个动作:创建短链接,访问短链接。只要这两个动作能跑通,后面所有优化都有落点。
第一个短链接的最小闭环
full生成短码并保存映射
按短码查找目标 URL
过期、封禁、权限都在这里生效
异步写入统计事件
返回 302 给浏览器
数据库用 SQLite,最简单的表结构,存长 URL 和短码的映射:
数据设计要点
- 核心是在
url_mapping里保存业务事实,而不是把规则散落在应用逻辑里。- 关键字段包括
id、short_code、long_url、created_at,它们决定了后续查询和管理能力。
最小映射表
| 字段 | 类型 | 说明 |
|---|---|---|
id | INTEGER | 自增主键 |
short_code | VARCHAR(10) | 短码,唯一索引 |
long_url | VARCHAR(2048) | 原始 URL |
created_at | TIMESTAMP | 创建时间 |
短码怎么生成?我第一反应是 MD5 哈希——对长 URL 取哈希,截取前几位当短码:
落地思路
- 输入是长 URL,取 MD5 后截取前若干位十六进制字符作为短码。
- 这一版不查重:同一个 URL 每次生成同一个短码,不同 URL 是否撞车完全靠运气。
- 兜底只有一条——插入时依赖数据库唯一约束报冲突,冲突后没有任何重试逻辑。
插入和查询也很直白:
数据设计要点
- 查询目标是用短码在
url_mapping上定位唯一一条记录,跳转路径上不能有全表扫描。- 写入时要保证
short_code唯一、long_url完整,created_at记下创建时间,方便后续按时间排序和清理。
一个下午,第一版就跑起来了。
上线之后我用了几天,发现 6 位十六进制的短码看起来不够”短链接”。那些十六进制字符(0-9, a-f)只用了 16 个符号,浪费了大量编码空间。
研究了一下,我发现可以用 Base62 编码——把 0-9、a-z、A-Z 这 62 个字符全部用上。
落地思路
- 做法是把十六进制的哈希值先当成一个整数,再按 62 进制重新编码成短码。
- 字符集从 0-9a-f 扩到 0-9a-zA-Z,同样的位数能表达的取值范围大了 3000 多倍。
- 这一步只改编码、不改取值来源,所以碰撞风险没有变化,只是短码更好看了。
这比直接截取 MD5 好在哪?算一笔账就明白了:
- 16 进制(MD5 截取):每位 16 种可能,6 位 = 16^6 = 1677 万种组合
- 62 进制(Base62):每位 62 种可能,6 位 = 62^6 = 568 亿种组合
同样的 6 位字符,容量差了 3000 多倍。
我的做法是把 MD5 哈希的前 8 位十六进制转成十进制整数,再用 Base62 编码:
落地思路
- 输入取 MD5 前 8 位十六进制(32 位整数),转成十进制后再做 Base62 编码。
- 编码结果按数值补齐到固定长度,位数不够前面补 0,保证短码长度一致。
- 得到的短码是大小写混合的,URL 里不需要转义,可以直接贴进社交平台。
这样生成的短码又短又紧凑。
做完 Base62 版本后,有天夜里我躺在床上突然想到一个问题:不同 URL 可能产生相同的短码。
MD5 输出 128 位,我只取了前 32 位(8 个十六进制字符),再转成 Base62。这意味着我把庞大的哈希空间压缩到了很小的范围。
根据生日悖论,当存储的链接数达到一定规模后,冲突的概率远比直觉想象的要高。
两个不同 URL 碰撞出同一个短码——用户点击短链接,跳到了错误的页面。这是不可接受的事故。
我想了几种应对方案:
方案 A:冲突后加随机盐重试。 如果生成的短码已被占用,就给 URL 加一个随机后缀重新哈希,直到不冲突。
方案 B:自增 ID + Base62。 用数据库的自增 ID 转成 Base62,从根源上杜绝冲突:
落地思路
- 取数据库自增主键,转成 Base62 就是短码;主键唯一,短码就唯一。
- 代价是短码可反推:拿到 d7f3b2 就能算出对应 ID,进而推算平台的总链接数。
- 冲突概率从「哈希截断」变成「零」,但暴露了业务量——这两件事当时只能选一个。
自增 ID 方案简单、不会冲突,但有个缺点:暴露业务量——知道短码就能反推数据库 ID,推算出我总共有多少链接。
对当时的我来说,这不是什么大问题。但这个选择背后的取舍,我记住了。
短链接生成了,还需要实现跳转。这里有个看似简单实则影响深远的选择:用 301 还是 302 重定向?
落地思路
- 301 的语义是「永久搬家」,浏览器会把重定向关系缓存下来,之后直接访问目标 URL。
- 302 的语义是「临时带路」,每次点击都要回到短链服务,由它决定跳到哪里。
- 跳转路径上只改响应状态码和 Location 头,真正影响业务的是「这次请求还会不会回到服务端」。
- 能不能统计、能不能改目标 URL,取决于服务端还有没有机会看到这次点击。
两者的区别:
| 特性 | 301 永久重定向 | 302 临时重定向 |
|---|---|---|
| SEO 权重 | 传递给目标 URL | 保留在短链接 |
| 浏览器缓存 | 会缓存 | 不缓存 |
| 统计准确性 | 不准确(缓存后不再请求服务器) | 准确(每次都请求服务器) |
| 适用场景 | 永久不变的链接 | 可能变化的链接 |
我选了 302。理由很简单:
- 我需要准确的点击统计——每次点击都能被记录
- 长链接可能会变——用户应该能修改指向
- SEO 权重不是我关心的事
把上面所有东西拼在一起,我写了第一版完整的短链接服务:
落地思路
- 创建链路:校验 URL → 生成短码 → 写入
url_mapping→ 返回短链。- 跳转链路:取路径里的短码 → 查
url_mapping→ 命中则返回 302 和 Location,未命中返回 404。- 两条链路共用同一张表,这张表此刻既是事实来源也是唯一的查询入口。
第一版很简陋,但每个短链接的生成、存储、跳转流程都走通了。
这个阶段的架构没有任何花活:一个应用进程,一张 SQLite 映射表,一个 302 跳转接口。它很简陋,但已经把短链接系统最关键的创建链路和跳转链路分出来了。
第 1 版:单机短链接服务
先跑通创建和跳转闭环,所有复杂问题先不处理。
用户
应用
存储
我把服务部署到一台 1 核 2G 的云服务器上。
成本账本:
| 项目 | 费用 |
|---|---|
| 云服务器(1 核 2G) | ¥10/月 |
| 域名 | ¥50/年 |
| 收入 | ¥0 |
月成本 ¥10 多一点,我还能接受。
我把项目发到了一个技术社区。写了一篇帖子,介绍我的短链接服务,附上源码链接。
第一天:0 个用户。
又发到第二个社区。还是 0 个用户。
没人关心一个无名开发者的周末小项目。说实话,有点灰心。
回过头看,第一版做的事很简单:
| 组件 | 实现 |
|---|---|
| 短链接生成 | MD5 哈希 + Base62 编码 |
| 存储 | SQLite |
| 重定向 | 302 临时重定向 |
| API | RESTful 风格 |
但问题也很明显,只是当时的流量还不足以暴露它们:
- 性能:每次查询都打数据库,没有缓存
- 哈希冲突:短码空间有限,迟早会撞
- 高并发:SQLite 写锁会卡死
- 统计功能:没有点击数据,连谁在用都不知道
第一版跳转链路监控
mvp第一批真实访问
SQLite 查询开始抖动
还没有缓存层
那时候我以为这个项目就这样了。上线没人用,也就不用操心这些。
没想到两周后……
思考 1
为什么短链接用 62 进制(0-9a-zA-Z)而不用 10 进制?我最初只是照着别人的方案做,后来才理解背后的道理。
解题思路:
短码字符集的选择本质是”在多少个字符里取几位”的问题。要从三个角度论证 62 是好选择:组合数够不够大、字符在 URL 里安不安全、以及长度相同时信息密度高不高。
详细步骤:
使用 62 进制的原因:
-
更短的链接长度
- 62 进制:62^6 ≈ 568 亿种组合(6 位字符)
- 10 进制:10^6 = 100 万种组合(6 位字符)
- 相同长度下,62 进制能表示更多不同的值
-
字符集友好
- 数字(0-9):10 个
- 小写字母(a-z):26 个
- 大写字母(A-Z):26 个
- 共 62 个字符,都是 URL 安全字符
-
URL 兼容性
- 这 62 个字符在 URL 中不需要编码
- 可以直接在浏览器、邮件、短信中使用
- 不会产生特殊字符问题
最终答案:
使用 62 进制有三个原因。一是链接更短:62 进制 6 位约有 568 亿种组合,而 10 进制 6 位只有 100 万种,相同长度下 62 进制能表示多得多的不同值。二是字符集友好:数字 0-9 共 10 个、小写字母 a-z 共 26 个、大写字母 A-Z 共 26 个,合计 62 个字符。三是 URL 兼容:这 62 个字符在 URL 中都不需要编码,可以直接在浏览器、邮件、短信里使用,不会产生特殊字符问题。
思考 2
如果要支持 10 亿个短链接,至少需要多少位 62 进制字符?我在做容量规划的时候算过这笔账。
解题思路:
这题是纯粹的对数估算:要装下 10 亿条记录,求最小的 n 使 62 的 n 次方不小于 10 亿。解法是两边取对数,再用 5 位和 6 位的结果反向验证。
详细步骤:
计算过程:
需要表示 10 亿 = 1,000,000,000 个不同的值
62 进制 n 位字符可以表示:62^n 种组合
求解:62^n ≥ 1,000,000,000
取对数:n ≥ log(1,000,000,000) / log(62) n ≥ 9 / log10(62) n ≥ 9 / 1.792 n ≥ 5.02
最终答案:
至少需要 6 位 62 进制字符。
验证:
- 5 位:62^5 = 916,132,832(不足 10 亿)
- 6 位:62^6 = 56,800,235,584(超过 10 亿)
思考 3
回头看,302 是对的选择吗?如果重来一次,我会不会选 301?
解题思路:
这题问的是短链接服务为什么该选 302。要抓住两个致命需求:点击统计和目标可变性。301 的缓存语义恰好和这两个需求直接冲突,所以结论不是”哪个性能好”,而是”哪个语义对得上业务”。
详细步骤:
302 仍然是更好的选择:
1. 统计准确性
- 302 不会被浏览器缓存,每次都会请求服务器
- 可以准确记录每次点击
- 对于需要 click tracking 的短链接服务至关重要
2. URL 可变性
- 短链接对应的长 URL 可能会变化
- 302 表示”临时”重定向,允许后续修改
- 301 表示”永久”,修改后浏览器可能仍使用缓存
3. 业务需求
- 短链接服务需要知道谁在什么时候访问了链接
- 需要地域、设备、referrer 等统计信息
- 302 保证每次请求都能被记录
什么时候用 301?
- 确定 URL 永远不会变化
- 不关心点击统计
- 希望减少服务器请求(利用浏览器缓存)
最终答案:
短链接服务应继续用 302。一、统计准确性:302 不会被浏览器缓存,每次访问都会请求服务器,能准确记录每次点击,这对需要 click tracking 的短链接服务至关重要。二、URL 可变性:短链接对应的长 URL 可能会变,302 表示”临时”重定向允许后续修改,而 301 表示”永久”,修改后浏览器可能仍用缓存里的旧地址。三、业务需求:服务需要知道谁在什么时候访问了链接,需要地域、设备、referrer 等统计信息,302 保证每次请求都能被记录。只有在确定 URL 永远不会变化、不关心点击统计、希望靠浏览器缓存减少服务器请求这三者同时成立时,才用 301。
发现内容有问题?
告诉我们哪里需要改进。
