短链系统发号器设计
一个撞车事故
2023年Q3,我们短链服务上线了一个"批量创建短链"的功能。
运营同学一次性导入了 10 万条短链映射。大促预热页面全部配置好,推广链接分发到各个渠道。
预热开始后,大量用户反馈跳转到了错误的页面。我们的客服在 5 分钟内接到了 200 多个投诉电话。
排查发现:批量导入过程中,有一批请求并发处理,两个请求同时从数据库拿了同一个自增 ID,各自创建了不同的短链。第二个请求覆盖了第一个——10 万条里,有 3 万条映射被错误覆盖。
这次事故之后,我们彻底重写了 ID 生成模块。
问题定义
短链系统需要为每个长 URL 分配一个唯一的短码。发号器(ID Generator)的职责就是生成这个唯一 ID。
核心需求:
- 全局唯一:不能出现两个不同的长 URL 共用同一个短码
- 趋势递增:ID 最好随时间递增,便于数据库索引和范围查询
- 高可用:ID 生成服务不能成为系统的单点瓶颈
- 高性能:单节点 QPS 要能达到数万,支持水平扩展
- 短码友好:ID 最好是 62 进制的原始值,能直接映射到短码字符
【架构权衡】
短码的长度直接由 ID 的最大值决定:
- 6 位短码 =
62^6 = 568 亿,够用 - 7 位短码 =
62^7 = 3.5 万亿,非常充裕
但问题是:ID 越大,短码越长,用户体验越差。需要在 ID 空间和用户体验之间找平衡。
方案演进
方案A:数据库自增 ID
最简单粗暴的方案。
优点:简单,绝对唯一,数据库帮你保证 缺点:
- 性能差:每次生成需要一次数据库往返
- 水平扩展困难:多数据库实例时无法保证全局唯一
- ID 连续递增:可被猜测,不适合对外暴露
- 单点瓶颈:数据库是中心节点
方案B:Redis INCR
用 Redis 的原子递增操作生成 ID。
优点:性能比数据库高一个数量级,天然支持集群 缺点:
- 依赖 Redis,Redis 挂了 ID 生成就挂了(但 Redis 通常比 MySQL 高可用得多)
- ID 连续递增问题依然存在
- 多 Redis 实例时需要额外的分配策略(如 Redis Cluster 不同节点分配不同起始值)
方案C:雪花算法(Snowflake)
Twitter 开源的分布式 ID 算法,是工业界最流行的方案。
雪花 ID 结构(64 位):
优点:
- 不依赖第三方组件,纯本地算法,性能极高(单节点可达每秒数十万 ID)
- 趋势递增,对数据库索引友好
- 支持分布式(通过 workerId 区分不同机器)
缺点:
- 依赖机器时钟,时钟回拨会导致 ID 冲突
- ID 趋势递增但不是连续的(中间有空洞)
- 10 位 workerId 在大规模集群下可能不够
方案D:号段模式(数据库号段)
解决雪花算法 ID 太大问题的折中方案。
优点:
- 数据库操作次数大幅降低(每 1000 次请求才一次 DB)
- ID 连续递增,数据库索引友好
- 可按业务标签(biz_tag)分表存储不同业务的 ID
缺点:
- ID 有空洞:号段分配后,如果服务重启,未用完的号段就浪费了
- 数据库是中心节点:数据库挂了,所有服务都拿不到 ID
- 多数据库实例需要额外的号段分配协调
方案E:多基地时钟同步
解决雪花算法时钟回拨问题。
生产避坑
坑1:雪花算法时钟回拨
这是生产环境中雪花算法最常见的故障原因。
场景:服务器从休眠中恢复、NTP 服务异常调整时间、虚拟机迁移。
解决方案:
- 容忍少量回拨(5ms 以内),等待补齐
- 回拨超出容忍范围时抛出异常,人工介入
- 更可靠的方案:使用数据库或 ZooKeeper 持久化上次的时间戳,重启后从持久化时间继续
坑2:号段耗尽瞬间的尖刺
号段模式中,当 currentId 接近 maxId 时,所有请求都会阻塞在刷新号段的数据库操作上。这会导致短暂的 QPS 下降。
解决方案:
- 异步预取:当 currentId 达到 maxId 的 80% 时,后台线程就开始预取下一个号段
- 双号段缓冲:一个号段快用完时,切换到另一个已经预取好的号段
坑3:ID 转换短码时的前导零问题
工程代价评估
落地 Checklist
- ID 生成方案选型(根据业务规模选择雪花或号段)
- 时钟回拨处理方案(雪花算法必做)
- 号段模式的异步预取和双缓冲(号段模式必做)
- ID 到短码的转换逻辑(前导零处理)
- 短码长度规划(6位够用吗?7位?)
- 监控告警:ID 生成 QPS、延迟、号段剩余量
- 降级方案:ID 生成服务挂了怎么降级(缓存一批预生成的 ID)
- 单元测试:时钟回拨、并发安全、ID 唯一性
短链发号器与存储的联合设计
发号器生成的 ID 最终要持久化到存储中,短链系统和发号器的联合设计有几种模式:
模式1:发号器 + 异步写库
模式2:发号器 + 缓存双写
模式3:号段 + 批量预写
【架构权衡】
短链发号器的选型关键看业务规模:
- 日均千万级以下:号段模式 + MySQL 就够了,简单可靠
- 日均亿级以上:雪花算法,多节点部署,每秒可生成数十万 ID
- 需要严格不可猜测:雪花算法的 ID 不连续,有一定安全性;如果需要完全不可猜测,可以在雪花 ID 基础上加加密(如 Base62 随机打乱)
- 多机房部署:必须使用多基地时钟同步方案,避免时钟漂移导致 ID 冲突