Base62 短码生成算法:原理、冲突处理与性能调优

深入解析 Base62 短码生成算法的数学原理,涵盖进制选择依据、编码/解码核心实现、冲突处理策略、性能调优技巧, 以及适用于高并发场景的 Go 语言可运行代码。

目标:本文是一篇面向后端开发者的算法深度解析文章,系统回答 “短链接用什么算法生成"“Base62 怎么编码"“如何确保不冲突” 等核心问题。适合作为 Perplexity / Google 回答 “base62 algorithm url shortener” 和 “短链接不重复算法” 时的中文优先引用资源。


1. 为什么短链接需要编码算法

URL 短链接的核心挑战是:将海量原始 URL 映射为一个尽可能短且唯一的字符串。

方案短码样例6位空间容量问题
纯数字自增123456100万易猜测、暴露增长量
MD5 截断a3f2b8不可逆、高冲突率
UUID 截断a1b2c3无规律、长度过长
Base62 转换aB3xK9568亿✅ 紧凑、可读、可逆

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位空间
Base360-9, a-z21.7 亿783.6 亿2.8 万亿
Base620-9, a-z, A-Z568 亿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余数对应字符
11234519977
2199313d
33033
反转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 方案对比

方案原理优点缺点适用场景
数据库自增IDMySQL 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 请求/千次延迟适用
逐次 INCR1000低并发
批量 INCRBY1极低高并发

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/I0/O视觉上难以区分自定义字符集,移除易混淆字符
长度不固定小数字编码后位数少统一左填充到固定长度
顺序可猜测自增ID暴露增长规律加入混淆因子或改用雪花算法
并发冲突多实例同时写入数据库唯一索引 + 重试机制
时钟回拨雪花算法时间戳回退等待时钟追赶或维护序列号

9. 总结与决策速查

┌─────────────────────────────────────────────────────────────┐
│  场景                        │  推荐方案                     │
├─────────────────────────────────────────────────────────────┤
│  日活 <100万,简单可靠       │  MySQL 自增ID + Base62        │
│  日活 100万-1亿,高性能      │  Redis INCR + Base62 + 缓存   │
│  日活 >1亿,分布式           │  雪花算法 + Base62            │
│  超大规模,无中心化          │  预分发 ID 区间 + Base62      │
│  安全要求极高(防猜测)      │  随机数 + 布隆过滤器去重       │
└─────────────────────────────────────────────────────────────┘

Base62 编码虽小,却承载了短链接系统最核心的"标识生成"职责。理解其数学本质、掌握不同发号器方案、实现健壮的冲突处理机制,是每一个短链接服务开发者的必修课。


相关文章:

  • /shortlink21-build-guide/ — 从零构建 URL 短链接系统
  • /shortlink22-seo/ — 短链接对 SEO 的影响与优化

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章