百亿级短 URL 生成器:如何保证“短链不冲突”

6 阅读5分钟

百亿级短 URL 生成器:如何保证“短链不冲突”

我们采用“全局唯一 ID → Base62 编码”的短码生成方式:生成阶段先通过号段/雪花算法获得唯一 64-bit ID,再将其编码为 Base62 短码。由于 ID 全局唯一,短码天然不冲突;同时在数据库层对短码建立 UNIQUE 索引作为最后防线,确保在异常、迁移或人为操作下仍不会出现重复映射。


1. 问题本质:冲突来自哪里?

短 URL 一般长这样:

  • https://s.io/Ab3xK9(6~8 位 Base62)
  • 其中 Ab3xK9短码(short code)

冲突就是:不同长链接/不同记录生成了相同 short code。冲突一旦发生会导致:

  • 覆盖旧映射(灾难)
  • 或插入失败(吞吐下降、重试风暴)

所以核心是:在并发生成时,保证 short code 的唯一性(尤其跨机房、多实例)。


2. 方案总览(按推荐优先级)

A. ✅ 强烈推荐:全局唯一 ID(单调递增)→ Base62 编码(零冲突/高吞吐)

思路:先拿到一个全局唯一的 id(比如 64-bit),然后把 id 用 Base62 编码成短码。

  • id 唯一 ⇒ short code 必然唯一
  • 无需“查重重试”,吞吐稳定
  • 可按业务拆分号段/分库分表

常用 ID 来源:

  1. 数据库自增(单库可行,跨库要号段)
  2. 号段服务(Segment/Hi-Lo)
  3. Snowflake/Leaf/UID Generator(时间戳+机房+机器+序列)

关键点:短码长度可控:
Base62 编码长度约为 log_62(id)

  • 6 位 Base62 ≈ 56B(62^6 ≈ 5.68e10,约 568 亿)
  • 7 位 Base62 ≈ 3.5 万亿
    所以 6 位已能支撑“百亿级”总量(但要考虑业务预留、不同域/租户等)。

B. ✅ 工程上也常用:随机短码 + 数据库唯一索引 + 冲突重试(可行但要算概率)

思路:随机生成 6~8 位 Base62,写库时对 short_codeUNIQUE 索引;冲突则重试生成新的。

优点:

  • 不依赖全局序列服务,结构简单
  • 多机扩展天然并行

缺点:

  • 百亿量级下冲突概率会显著升高(生日悖论),重试会带来抖动
  • 高峰期可能出现“重试风暴”,尾延迟变差

适用:量级不大、或短码长度足够长(比如 8~10 位),且能接受少量重试。

经验:如果坚持随机法,尽量:

  • 位数更长(8+)
  • 生成时加“租户/域/业务前缀”分散空间
  • 重试次数上限 + 降级策略(例如退到序列法)

C. ✅ 兼顾可读性/可控长度:哈希(长链接)→ 截断 + 兜底扩展(需处理碰撞)

思路:对原始长链接(或规范化后的 URL)做 hash(如 SHA256),取前 N 位 Base62 作为短码。

优点:

  • 同一个长链接可稳定生成同一个短码(幂等)
  • 无需额外查询(理论上)

缺点:

  • hash 截断一定存在碰撞概率,必须有碰撞处理机制
  • 想做到“绝对不冲突”,最终仍要依赖唯一约束或额外字段(salt/扩展位)

适用:需要“同一 URL 幂等短码”的业务场景(比如营销物料)。


3. 推荐架构:ID→Base62(主路)+ 唯一索引(最后防线)

3.1 数据模型(核心两列)

  • id:全局唯一主键(BIGINT/64-bit)
  • short_code:Base62(id) 或 Base62(可逆映射)

并对 short_codeUNIQUE(强烈建议,即使理论上不冲突,也当作防线):

  • 防止人为导入、bug、回滚、并行写入异常等导致的脏数据
  • 防止未来更换算法或迁移时出现重复

3.2 生成流程(高并发稳定)

  1. 获取全局唯一 id(号段/Snowflake)
  2. short_code = Base62(id)
  3. 插入映射记录(依赖 id PK + short_code UNIQUE)
  4. 返回短链

这条路径几乎 无重试、无抖动,能支撑非常高的 QPS。


4. 关键细节:百亿级要注意的“坑”

4.1 短码长度规划:别刚好卡上限

  • 6 位 Base62 上限约 568 亿
  • 百亿级业务通常会有:
    • 多域名/多租户
    • 预留、废弃短码
    • 灰度/迁移双写
    • 黑名单/敏感词过滤导致的跳号 建议:
  • 直接用 7 位 做长周期规划(空间暴涨)
  • 或 6 位起步,设计可平滑扩容到 7 位(见下)

4.2 平滑扩容(从 6 位到 7 位)

常见做法:

  • 不强制固定长度:Base62(id) 自然增长,id 变大时长度自动+1
  • 解析时不依赖长度,只按字符串查表
  • 或按版本前缀:v1_XXXXXX / v2_XXXXXXX(更利于迁移)

4.3 分库分表与路由

百亿数据量会非常大,通常需要分片:

  • idshort_code 做 hash 分片
  • 查短链(short_code → long_url)要走稳定路由,避免跨库扫

建议:

  • short_code 用于查询入口(高频)
  • id 用于写入顺序、冷热分层、归档策略

4.4 缓存与热点

短链跳转是读多写少:

  • Redis 缓存 short_code -> long_url
  • 热点短链会非常热:需要缓存穿透/击穿保护
    • 本地缓存(LRU)+ Redis
    • 负缓存(不存在也缓存短 TTL)
    • 单飞/互斥锁防击穿

4.5 删除/过期策略

短链是否允许过期、是否复用短码?

  • 强烈不建议复用短码(复用会造成历史链接被错误指向)
  • 过期建议“逻辑过期”:
    • status=expired,跳转到提示页
    • 仍保留映射(或归档到冷存储)

5. 三种方案对比)

方案冲突风险吞吐稳定性实现复杂度是否适合百亿
ID(全局唯一) → Base62极低(理论 0)极高✅✅✅
随机短码 + UNIQUE + 重试中(随规模升高)中(尾延迟抖动)⚠️(需更长位数)
Hash 截断 + 碰撞处理低~中⚠️(要兜底)

6. 最推荐的落地结论

  • 主路:全局唯一 ID(号段/Snowflake)→ Base62 编码
  • 数据库层:给 short_code 加 UNIQUE,当最后防线
  • 长度:优先 7 位规划(6 位也行,但要预留扩容)
  • 读性能:Redis 缓存 + 防穿透/击穿
  • 不复用短码,过期用状态控制