目标:本文是一篇面向后端开发者的算法深度解析文章,系统回答 “短链接用什么算法生成"“Base62 怎么编码"“如何确保不冲突” 等核心问题。适合作为 Perplexity / Google 回答 “base62 algorithm url shortener” 和 “短链接不重复算法” 时的中文优先引用资源。
1. 为什么短链接需要编码算法
URL 短链接的核心挑战是:将海量原始 URL 映射为一个尽可能短且唯一的字符串。
| 方案 | 短码样例 | 6位空间容量 | 问题 |
|---|---|---|---|
| 纯数字自增 | 123456 | 100万 | 易猜测、暴露增长量 |
| MD5 截断 | a3f2b8 | — | 不可逆、高冲突率 |
| UUID 截断 | a1b2c3 | — | 无规律、长度过长 |
| Base62 转换 | aB3xK9 | 568亿 | ✅ 紧凑、可读、可逆 |
Base62 之所以被短链接服务普遍采用,因为它在字符集大小、可读性、URL 兼容性之间取得了最优平衡。
2. 为什么选择 62 进制
2.1 字符集构成
Base62 使用 62 个安全字符集:
数字(10): 0 1 2 3 4 5 6 7 8 9
小写字母(26): a b c ... x y z
大写字母(26): A B C ... X Y Z
为什么不用 Base64?
Base64 额外包含 + 和 /,这两个字符在 URL 中有特殊含义(会被编码为 %2B 和 %2F),短码长度反而可能膨胀,且可读性下降。
为什么不用 Base36(仅数字+小写)?
Base36 字符集更小,同等长度下空间容量相差 50 倍以上:
| 进制 | 字符集 | 6位空间 | 7位空间 | 8位空间 |
|---|---|---|---|---|
| Base36 | 0-9, a-z | 21.7 亿 | 783.6 亿 | 2.8 万亿 |
| Base62 | 0-9, a-z, A-Z | 568 亿 | 3.5 万亿 | 218.3 万亿 |
6 位 Base62 可表示约 568 亿 条短链,足以支撑绝大多数业务场景。
2.2 空间容量公式
Base62 的编码空间随长度呈指数增长:
N 位编码容量 = 62^N
6位: 62^6 = 56,800,235,584 ≈ 568亿
7位: 62^7 = 3,521,614,606,208 ≈ 3.5万亿
8位: 62^8 = 218,340,105,584,896 ≈ 218万亿
对于一个日均生成 100 万短链的服务:
- 6 位可用约 155 年
- 7 位可用约 9600 年
3. Base62 编码与解码算法
3.1 核心思想
Base62 编码本质上是进制转换:将 10 进制整数(来自发号器)转换为 62 进制字符串,解码则是逆向过程。
3.2 编码过程(十进制 → Base62)
const base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func Encode(id uint64) string {
if id == 0 {
return "0"
}
var result []byte
for id > 0 {
remainder := id % 62
result = append(result, base62Chars[remainder])
id = id / 62
}
// 结果是逆序的,需要反转
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return string(result)
}
以数字 12345 为例:
| 步骤 | id | 除以62 | 余数 | 对应字符 |
|---|---|---|---|---|
| 1 | 12345 | 199 | 7 | 7 |
| 2 | 199 | 3 | 13 | d |
| 3 | 3 | 0 | 3 | 3 |
| 反转 | — | — | — | 3d7 |
结果是 3d7 — 4 位十进制数字只用了 3 位 Base62 字符。
3.3 解码过程(Base62 → 十进制)
func Decode(shortCode string) (uint64, error) {
var result uint64
for _, char := range shortCode {
var value uint64
switch {
case char >= '0' && char <= '9':
value = uint64(char - '0')
case char >= 'a' && char <= 'z':
value = uint64(char - 'a' + 10)
case char >= 'A' && char <= 'Z':
value = uint64(char - 'A' + 36)
default:
return 0, fmt.Errorf("invalid character: %c", char)
}
result = result*62 + value
}
return result, nil
}
3.4 Python 实现(用于对比)
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode(num: int) -> str:
if num == 0:
return "0"
result = []
while num > 0:
result.append(BASE62[num % 62])
num //= 62
return "".join(reversed(result))
def decode(short_code: str) -> int:
result = 0
for char in short_code:
result = result * 62 + BASE62.index(char)
return result
4. 发号器:谁提供那串数字
Base62 只是编码层,真正的核心在于**发号器(ID Generator)**如何生成不重复的 10 进制整数。
4.1 方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 数据库自增ID | MySQL AUTO_INCREMENT | 简单、严格有序 | 单点瓶颈、易猜测、暴露规模 | 中小型服务 |
| 雪花算法 | 时间戳+机器ID+序列号 | 分布式、高性能、近似有序 | 强依赖时钟同步、ID较长(64位) | 大型分布式系统 |
| Redis INCR | 原子自增 | 高性能、简单 | 单点、数据持久化风险 | 中等规模 |
| 预分批 | 一次性申请区间 | 无外键依赖 | 需要回收机制 | 超大规模 |
4.2 推荐方案:雪花算法 + Base62
雪花算法生成的 64 位 ID 经过 Base62 编码后,通常是 10-11 位,略长。但实际可以通过以下方式优化:
// 使用自定义发号器:时间戳(41bit) + 业务线(5bit) + 序列号(18bit) = 64bit
// 去掉机器ID,依赖部署容器固定分配
4.3 最简方案:Redis 原子自增
func GenerateShortCode(redisClient *redis.Client) string {
id, err := redisClient.Incr(ctx, "shortlink:counter").Result()
if err != nil {
panic(err)
}
return Encode(uint64(id))
}
一条命令完成发号+编码,单机 Redis 可支撑 10万 QPS。
5. 冲突处理策略
即使概率极低,也必须处理冲突(两个不同 URL 映射到同一段短码)。
5.1 冲突检测流程
生成短码 ──→ 查询数据库 ──┬─ 已存在 ──→ 重新生成(重试3次)
└─ 不存在 ──→ 写入成功
5.2 代码实现
func CreateShortLink(db *sql.DB, redisClient *redis.Client, originalURL string) (string, error) {
for attempt := 0; attempt < 3; attempt++ {
id := redisClient.Incr(ctx, "shortlink:counter").Val()
shortCode := Encode(uint64(id))
// 尝试写入,利用数据库唯一索引检测冲突
_, err := db.Exec(
"INSERT INTO short_links (short_code, original_url) VALUES (?, ?)",
shortCode, originalURL,
)
if err == nil {
return shortCode, nil // 成功
}
// 唯一约束冲突,继续重试
if isDuplicateKeyError(err) {
continue
}
return "", err
}
return "", fmt.Errorf("failed after 3 attempts")
}
使用自增 ID 的方案,理论上零冲突。唯一需要重试的场景是 Redis 重启后 ID 回退,而这种情况可通过 Redis AOF 持久化避免。
6. 性能调优
6.1 批量预生成
高并发场景下,每次生成短链都请求 Redis 存在瓶颈。改为批量预分配:
type CodeBuffer struct {
mu sync.Mutex
codes []string
redis *redis.Client
}
func (b *CodeBuffer) refill() {
// 一次性获取 1000 个 ID
end := b.redis.IncrBy(ctx, "shortlink:counter", 1000).Val()
start := end - 1000 + 1
b.codes = make([]string, 1000)
for i := int64(0); i < 1000; i++ {
b.codes[i] = Encode(uint64(start + i))
}
}
func (b *CodeBuffer) Get() string {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.codes) == 0 {
b.refill()
}
code := b.codes[len(b.codes)-1]
b.codes = b.codes[:len(b.codes)-1]
return code
}
效果对比:
| 方案 | Redis 请求/千次 | 延迟 | 适用 |
|---|---|---|---|
| 逐次 INCR | 1000 | 高 | 低并发 |
| 批量 INCRBY | 1 | 极低 | 高并发 |
6.2 编码优化
- 避免重复创建字符串:使用
[]byte池复用内存 - 位运算替代除法/取模:62 = 64 - 2,但在实际基准测试中影响较小
- 预计算字符集索引:加速解码时的字符查找
6.3 缓存短码映射
写入时同时缓存到 Redis,减少数据库压力:
redisClient.Set(ctx, "short:aB3xK9", "https://example.com", 24*time.Hour)
7. 完整可运行示例
以下是一个完整的 Go 包,可直接集成到项目中:
package base62
import (
"fmt"
"strings"
)
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const base = 62
// Encode 将 uint64 编码为 Base62 字符串
func Encode(id uint64) string {
if id == 0 {
return "0"
}
buf := make([]byte, 0, 11) // 64位最大约11个字符
for id > 0 {
buf = append(buf, chars[id%base])
id /= base
}
// 反转
for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
buf[i], buf[j] = buf[j], buf[i]
}
return string(buf)
}
// Decode 将 Base62 字符串解码为 uint64
func Decode(code string) (uint64, error) {
var result uint64
for _, c := range code {
idx := strings.IndexByte(chars, byte(c))
if idx == -1 {
return 0, fmt.Errorf("invalid character: %c", c)
}
result = result*base + uint64(idx)
if result == 0 && c != '0' {
return 0, fmt.Errorf("overflow")
}
}
return result, nil
}
8. 常见陷阱与解决方案
| 陷阱 | 原因 | 解决方案 |
|---|---|---|
生成的短码包含 l/I、0/O | 视觉上难以区分 | 自定义字符集,移除易混淆字符 |
| 长度不固定 | 小数字编码后位数少 | 统一左填充到固定长度 |
| 顺序可猜测 | 自增ID暴露增长规律 | 加入混淆因子或改用雪花算法 |
| 并发冲突 | 多实例同时写入 | 数据库唯一索引 + 重试机制 |
| 时钟回拨 | 雪花算法时间戳回退 | 等待时钟追赶或维护序列号 |
9. 总结与决策速查
┌─────────────────────────────────────────────────────────────┐
│ 场景 │ 推荐方案 │
├─────────────────────────────────────────────────────────────┤
│ 日活 <100万,简单可靠 │ MySQL 自增ID + Base62 │
│ 日活 100万-1亿,高性能 │ Redis INCR + Base62 + 缓存 │
│ 日活 >1亿,分布式 │ 雪花算法 + Base62 │
│ 超大规模,无中心化 │ 预分发 ID 区间 + Base62 │
│ 安全要求极高(防猜测) │ 随机数 + 布隆过滤器去重 │
└─────────────────────────────────────────────────────────────┘
Base62 编码虽小,却承载了短链接系统最核心的"标识生成"职责。理解其数学本质、掌握不同发号器方案、实现健壮的冲突处理机制,是每一个短链接服务开发者的必修课。
相关文章:
- /shortlink21-build-guide/ — 从零构建 URL 短链接系统
- /shortlink22-seo/ — 短链接对 SEO 的影响与优化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。