Base62 编码
上一章我选了 MD5 哈希 + 截取的方案来生成短链接码。思路没问题,但 MD5 输出是 16 进制的——每个位置只有 16 种可能(0-9, a-f)。这意味着 6 位 16 进制最多只能表示 1677 万个短链接。
1677 万。听起来不少,但 Reddit 每天就有数百万个新链接。一个稍有名气的服务,几个月就耗尽了。
我需要一个更”紧凑”的编码方式:用同样 6 个字符的长度,塞进更多的信息。答案就是 Base62。
但首先我得搞清楚一个问题——为什么是 62?不是 64,不是 36,偏偏是 62?
URL 里能安全使用的字符有哪些?我列了一下:
- 数字:
0-9,共 10 个 - 小写字母:
a-z,共 26 个 - 大写字母:
A-Z,共 26 个
10 + 26 + 26 = 62。
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
│← 10 个数字 →││← 26 个小写字母 →││← 26 个大写字母 →│
这 62 个字符在 URL 的路径部分完全安全,不需要任何转义。不多不少,刚好 62 个。
那为什么不用更多字符?因为剩下的字符——+、/、=、?、&、#、%——在 URL 中都有特殊含义。用了它们,浏览器会自动编码,短链接反而变长了。
为什么不用更少字符?比如只用 36 个(0-9 + a-z)?当然可以,但效率会降低。我列了一张对比表:
| 进制 | 字符集 | 6 位能表示的数量 |
|---|---|---|
| 10 进制 | 0-9 | 10^6 = 100 万 |
| 16 进制 | 0-9, a-f | 16^6 ≈ 1677 万 |
| 36 进制 | 0-9, a-z | 36^6 ≈ 21 亿 |
| 62 进制 | 0-9, a-z, A-Z | 62^6 ≈ 568 亿 |
所以 62 是 URL 安全字符的上限。在保证安全的前提下,它能用最少的位数表示最大的数值。
我一开始对这个数字没什么直觉。让我算一下 62 的幂次,一步步感受它的增长:
62^1 = 62
62^2 = 3,844
62^3 = 238,328
62^4 = 14,776,336
62^5 = 916,132,832 (约 9.16 亿——已经快赶上中国人口了)
62^6 = 56,800,235,584 (约 568 亿)
568 亿。全球人口约 80 亿,这意味着我可以给地球上的每个人分配 7 个短链接,还绰绰有余。而 16 进制 6 位只能表示 1677 万——差了 3000 多倍。
这让我放心了——即使我的服务做到全球级别,6 位 Base62 也完全够用。
原理很简单:和十进制转二进制一样,不断除以 62 取余数,只是”数字”变成了 62 个字符。
落地思路
- 编码的输入是一个非负整数,输出是 Base62 字符串:用字符表
0-9、a-z、A-Z这 62 个字符。- 算法是不断除以 62 取余数:余数决定当前最低位是哪个字符,商继续下一轮,直到商为 0。
- 取余的顺序是从低位到高位,所以最后要把字符列表整体反转一次,才是正确的高位在前。
- 兜底是 0 要单独处理:0 的余数循环不收敛,编码结果是字符表第 0 位,不是空字符串。
逻辑是通的:num 不断除以 62,余数就是当前位的字符,商继续下一轮。和手算十进制转二进制完全一样的思路。
解码就是反过来:从左到右,每一位乘以 62 再加当前字符的值。
落地思路
- 解码是编码的逆运算:从左到右遍历每一位,先把当前值乘以 62,再加上这个字符在字符表里的下标。
- 编码和解码必须共用同一张字符表,两边顺序差一位就会整体错位,解出来的是完全不同的数。
- 遇到字符表之外的字符要直接报错:一个非法字符会让后面所有位的下标整体偏移,错误结果比报错更难排查。
- 验证方式是往返:先 encode 再 decode 回到原数,先 decode 再 encode 回到原串,两条都过才算编码器正确。
往返验证都通过了。encode(decode("G8")) 还是 "G8",decode(encode(1000)) 还是 1000。编码器没问题。
你可能会问:Base64 比 Base62 还多两个字符,不是更紧凑吗?
没错。Base64 的字符集是:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
62 个字符之外,多了 + 和 /。多两个字符意味着更大的基数,理论上编码更短。但恰恰是这两个字符,在 URL 中会出问题:
+在 URL 查询字符串中会被解析为空格/是 URL 路径分隔符,放进短链接码里会导致路径解析混乱
如果我在短链接里用了 +,浏览器可能把它编码成 %2B——一个字符变成了三个,短链接反而不短了。= 是 Base64 的填充字符,也有同样的问题。
| 特性 | Base62 | Base64 |
|---|---|---|
| URL 安全 | ✅ 直接用 | ❌ 需要额外编码 |
| 编码效率 | 稍低 | 稍高(多 2 个字符) |
| 短链接适用性 | ✅ 完美 | ❌ 不推荐 |
Base64 在传输二进制数据(比如邮件附件、图片)时是标准选择。但对于短链接,Base62 是最佳选择——少两个字符换来完全的 URL 安全,这笔交易很划算。
理论容量 568 亿,但实际能撑多久?让我算算。
假设我的短链接服务每天新增 1 万条链接(这已经是一个相当活跃的服务了——相当于每天有 1 万个新用户在创建短链接):
总容量:62^6 ≈ 568 亿
每天新增:10,000 条
可使用天数:568 亿 ÷ 10,000 = 5680 万天 ≈ 155,616 年
15 万年。人类文明有文字记录的历史也不过 5000 年。
我多算几个场景,心里更有底:
| 每天新增量 | 6 位能用多久 | 对应体量 |
|---|---|---|
| 1 万条 | 15 万年 | 小众服务 |
| 100 万条 | 1,556 年 | 中型服务 |
| 1 亿条 | 1.5 年 | 全球顶级服务 |
就算是每天 1 亿条这种极端情况,6 位用完之后加到 7 位(62^7 ≈ 3.5 万亿)就能再撑 960 年。
嗯,6 位完全够了。我不用在这个问题上纠结了。
现在我把上一章的 MD5 哈希方案和 Base62 编码结合起来:
落地思路
- 整条链路是三步:对长 URL 算哈希、截取固定长度的十六进制、再转成 Base62 得到 6 位短码。
- 字符表里的 62 个字符在 URL 路径里都是安全的,不需要任何百分号转义,短码可以原样放进链接。
- 固定长度靠左侧补零实现:补位用的必须是字符表第 0 位,否则 decode 回去就不是同一个数。
- 兜底要记住这一步只是编码:它不保证唯一,截断丢掉的那些位正是冲突的来源。
从长 URL 到 6 位短链接码,流程就是三步:MD5 哈希 → 截取 → Base62 编码。简洁高效。
等等,我在这里其实悄悄做了一个假设——我假设不同的长 URL 一定会产生不同的短链接码。但真的是这样吗?
我现在的方案是:对长 URL 做哈希,截取后转 Base62。问题在于,MD5 有 128 位,但我只取了前 8 位 16 进制(32 位)再转成 6 位 Base62。信息被丢弃了。
这意味着两个完全不同的 URL,可能在截取后碰巧得到相同的短链接码——这就是哈希冲突。
568 亿的空间虽然大,但根据生日悖论,实际存到几十万条的时候,冲突概率就已经不可忽视了。这不是理论上的担忧,而是一个我必须面对的实际问题。
冲突了怎么办?这是我下一步要解决的问题。
练习 1
为什么 6 位 Base62 能表示 568 亿种组合?请详细计算。
解题思路:
这题要建立的是数量级的直觉,而不是死记 62 的 6 次方。做法是把 62 的各次幂列出来做阶梯比较,再找一个大家有概念的参照物(人口)来换算,于是”568 亿”这个数字立刻有了画面。
详细步骤:
计算过程:
Base62 有 62 个不同的字符,每个位置可以是 62 个字符中的任意一个。
6 位 Base62 的组合数:
62 × 62 × 62 × 62 × 62 × 62
= 62^6
= 56,800,235,584
≈ 568 亿逐位展开:
62^1 = 62
62^2 = 3,844
62^3 = 238,328
62^4 = 14,776,336
62^5 = 916,132,832 (约 9.16 亿)
62^6 = 56,800,235,584 (约 568 亿)直观理解:
- 中国人口约 14 亿,568 亿是它的 40 倍
- 全球人口约 80 亿,可以给每个人分配 7 个不同的短链接
- 如果用 7 位,则是 62^7 ≈ 3.5 万亿,数量级再翻 62 倍
最终答案:
Base62 有 62 个不同字符,每个位置可取其中任意一个,6 位的组合数为 62 的 6 次方,等于 56,800,235,584,约 568 亿。逐位展开看:62 的 1 次方是 62,2 次方是 3,844,3 次方是 238,328,4 次方是 14,776,336,5 次方是 916,132,832(约 9.16 亿),6 次方是 56,800,235,584(约 568 亿)。直观理解:568 亿约是中国 14 亿人口的 40 倍,全球 80 亿人口每人可以分到 7 个不同短链接;如果扩展到 7 位,62 的 7 次方约 3.5 万亿,数量级再翻 62 倍。
练习 2
实现一个 Base62 编码工具类,支持固定长度编码和批量操作。
解题思路: 把「数字 ↔ 字符串」抽象成一对互逆的纯函数:编码按位取商取余,解码按位乘 62 累加。为了支持批量操作,把核心逻辑写成不依赖集合状态的单条转换,再在外面包一层循环即可。
详细步骤:
- 字符表:定义一个长度为 62 的常量表,顺序固定为
0-9、A-Z、a-z。编码与解码必须共用同一张表,否则两者会互相错位。 - 编码:
encode(n)对 不断取n % 62得到低位字符、令n = n / 62取整,直到 为止;因为得到的顺序是从低位到高位,最后需要把字符列表整体反转。 - 固定长度:在反转后的结果左侧补
0到指定长度。补位用的字符必须是字符表的第 0 位,这样decode才能把它还原为数值 0。 - 解码:
decode(s)从左到右遍历,每读入一个字符,先令value = value * 62,再加上该字符在字符表中的下标。 - 批量处理:提供
encodeBatch(List<Long>)与decodeBatch(List<String>),内部逐条调用单条转换;单条转换保持无状态,批量层就不需要考虑中间结果的重置问题。 - 失败兜底:遇到字符表之外的字符时抛出异常,而不是静默跳过——否则一个非法字符会让后面所有字符的下标整体错位,产生的错误结果比直接报错更难排查。
最终答案:
工具类暴露 encode / decode / encodeBatch / decodeBatch 四个方法;字符表为唯一数据源,固定长度靠左侧补零实现,批量接口是单条接口的循环封装,非法输入一律抛异常。
发现内容有问题?
告诉我们哪里需要改进。
