第一个短链接

长链接太丑了

某个周末,我在社交媒体上分享一个链接。那个 URL 长成这样:

https://www.example.com/search?q=系统设计课程&category=计算机&price=100-500&sort=sales

80 多个字符,占了半行字。发出去之后,整条动态的观感极差。

我盯着屏幕想了几秒——自己做一个短链接工具?反正周末没事。

理想中的效果:

https://short.url/a1b2c3

干净利落。

最简实现

我的原则:先跑起来再说。

第一版只解决两个动作:创建短链接,访问短链接。只要这两个动作能跑通,后面所有优化都有落点。

数据库用 SQLite,最简单的表结构,存长 URL 和短码的映射:

数据设计要点

  • 核心是在 url_mapping 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 关键字段包括 id、short_code、long_url、created_at,它们决定了后续查询和管理能力。
URL Mapping

最小映射表

mvp
字段类型说明
idINTEGER自增主键
short_codeVARCHAR(10)短码,唯一索引
long_urlVARCHAR(2048)原始 URL
created_atTIMESTAMP创建时间

短码怎么生成?我第一反应是 MD5 哈希——对长 URL 取哈希,截取前几位当短码:

落地思路

  • 输入是长 URL,取 MD5 后截取前若干位十六进制字符作为短码。
  • 这一版不查重:同一个 URL 每次生成同一个短码,不同 URL 是否撞车完全靠运气。
  • 兜底只有一条——插入时依赖数据库唯一约束报冲突,冲突后没有任何重试逻辑。

插入和查询也很直白:

数据设计要点

  • 查询目标是用短码在 url_mapping 上定位唯一一条记录,跳转路径上不能有全表扫描。
  • 写入时要保证 short_code 唯一、long_url 完整,created_at 记下创建时间,方便后续按时间排序和清理。

一个下午,第一版就跑起来了。

发现 Base62 编码

上线之后我用了几天,发现 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,推算出我总共有多少链接。

对当时的我来说,这不是什么大问题。但这个选择背后的取舍,我记住了。

302 还是 301?

短链接生成了,还需要实现跳转。这里有个看似简单实则影响深远的选择:用 301 还是 302 重定向?

落地思路

  • 301 的语义是「永久搬家」,浏览器会把重定向关系缓存下来,之后直接访问目标 URL。
  • 302 的语义是「临时带路」,每次点击都要回到短链服务,由它决定跳到哪里。
  • 跳转路径上只改响应状态码和 Location 头,真正影响业务的是「这次请求还会不会回到服务端」。
  • 能不能统计、能不能改目标 URL,取决于服务端还有没有机会看到这次点击。

两者的区别:

特性301 永久重定向302 临时重定向
SEO 权重传递给目标 URL保留在短链接
浏览器缓存会缓存不缓存
统计准确性不准确(缓存后不再请求服务器)准确(每次都请求服务器)
适用场景永久不变的链接可能变化的链接

我选了 302。理由很简单:

  1. 我需要准确的点击统计——每次点击都能被记录
  2. 长链接可能会变——用户应该能修改指向
  3. SEO 权重不是我关心的事

完整流程

把上面所有东西拼在一起,我写了第一版完整的短链接服务:

落地思路

  • 创建链路:校验 URL → 生成短码 → 写入 url_mapping → 返回短链。
  • 跳转链路:取路径里的短码 → 查 url_mapping → 命中则返回 302 和 Location,未命中返回 404。
  • 两条链路共用同一张表,这张表此刻既是事实来源也是唯一的查询入口。

第一版很简陋,但每个短链接的生成、存储、跳转流程都走通了。

当前架构

这个阶段的架构没有任何花活:一个应用进程,一张 SQLite 映射表,一个 302 跳转接口。它很简陋,但已经把短链接系统最关键的创建链路和跳转链路分出来了。

上线

我把服务部署到一台 1 核 2G 的云服务器上。

成本账本:

项目费用
云服务器(1 核 2G)¥10/月
域名¥50/年
收入¥0

月成本 ¥10 多一点,我还能接受。

我把项目发到了一个技术社区。写了一篇帖子,介绍我的短链接服务,附上源码链接。

第一天:0 个用户。

又发到第二个社区。还是 0 个用户。

没人关心一个无名开发者的周末小项目。说实话,有点灰心。

这个阶段的技术账本

回过头看,第一版做的事很简单:

组件实现
短链接生成MD5 哈希 + Base62 编码
存储SQLite
重定向302 临时重定向
APIRESTful 风格

但问题也很明显,只是当时的流量还不足以暴露它们:

  1. 性能:每次查询都打数据库,没有缓存
  2. 哈希冲突:短码空间有限,迟早会撞
  3. 高并发:SQLite 写锁会卡死
  4. 统计功能:没有点击数据,连谁在用都不知道

第一版跳转链路监控

mvp
QPS12

第一批真实访问

P95 延迟180ms

SQLite 查询开始抖动

缓存命中率0%

还没有缓存层

那时候我以为这个项目就这样了。上线没人用,也就不用操心这些。

没想到两周后……

我的思考

思考 1

为什么短链接用 62 进制(0-9a-zA-Z)而不用 10 进制?我最初只是照着别人的方案做,后来才理解背后的道理。

短链接长度

解题思路:

短码字符集的选择本质是”在多少个字符里取几位”的问题。要从三个角度论证 62 是好选择:组合数够不够大、字符在 URL 里安不安全、以及长度相同时信息密度高不高。

详细步骤:

使用 62 进制的原因:

  1. 更短的链接长度

    • 62 进制:62^6 ≈ 568 亿种组合(6 位字符)
    • 10 进制:10^6 = 100 万种组合(6 位字符)
    • 相同长度下,62 进制能表示更多不同的值
  2. 字符集友好

    • 数字(0-9):10 个
    • 小写字母(a-z):26 个
    • 大写字母(A-Z):26 个
    • 共 62 个字符,都是 URL 安全字符
  3. 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。

发现内容有问题?

告诉我们哪里需要改进。