先说结论
UUID 去中心化且简单,但较长、随机写入索引成本高;数据库或 Redis 自增容易理解但依赖中心服务;数据库号段把一段 ID 分配到应用内生成,减少远程访问;雪花算法把时间、节点和序列编码进整数,吞吐高且趋势递增,但依赖时钟与节点标识治理。
没有方案同时天然满足绝对递增、无中心、无限可用和零协调,应按业务约束取舍。
雪花算法要点
时间位决定可用年限,节点位决定并发节点数,序列位决定单毫秒吞吐。时钟回拨可选择短暂等待、使用逻辑时间、切换备用节点位或拒绝发号并告警,不能默默生成重复 ID。
先区分 ID 的业务目标
| 目标 | 关注点 | 可能方案 |
|---|---|---|
| 全局唯一 | 不重复、可验证 | UUID、雪花、号段 |
| 趋势递增 | 索引局部性、分页 | 号段、雪花、数据库序列 |
| 严格有序 | 全局协调和可用性 | 单点序列、分区内序列 |
| 不暴露业务量 | 难猜、不可枚举 | 随机 ID、加密映射 |
| 高吞吐低延迟 | 本地生成、少依赖 | 雪花、号段缓存 |
一个订单主键可以趋势递增,但对外展示的订单号可能需要不可枚举;这两个需求可以用内部 ID 与外部展示号分离解决,而不是让一个格式满足所有场景。
号段模式的故障处理
中心服务为节点分配 [max_id, max_id + step),应用在本地内存中递增。号段耗尽前异步预取下一段,中心短暂故障时仍可继续使用当前段;进程重启后会浪费未使用号段,但通常不影响唯一性。
要处理预取并发、号段重复、数据库事务、步长过大和所有号段耗尽。若本地缓存两段并切换失败,必须停止发号或切换备用中心,不能回退到一个未经协调的初始值。
雪花位分配示例
1 bit 符号位(保留)
41 bit 时间戳(自定义 epoch)
10 bit 节点标识
12 bit 毫秒内序列
该布局每毫秒每节点最多 4096 个 ID,节点最多 1024 个;时间位寿命取决于 epoch。真实系统还需考虑序列溢出时等待下一毫秒、同一节点多进程、容器 IP 变化和时钟回拨。
时钟回拨策略
小幅回拨可等待物理时间追上上次时间;较大回拨可切换备用节点位或进入逻辑时间模式;无法证明唯一时应拒绝发号并告警。用“把回拨时间加回去”可能导致未来窗口与真实时间混乱,必须记录逻辑时间和恢复状态。
工程治理
节点号由可靠注册中心或部署系统分配并设租约,防止容器重启后冲突。监控发号延迟、序列耗尽和时间偏移;ID 若对外暴露,还要评估是否泄漏业务量与生成时间。
容易踩坑的地方
趋势递增不等于严格全局递增,多节点雪花 ID 在极近时间仍可能交错。唯一 ID 也不等于业务幂等键,同一业务请求重试可能生成两个不同 ID。
ID 与索引、分页
趋势递增主键改善 B+Tree 插入局部性,但多个节点的雪花 ID 只保证大致按时间增长,不保证严格全局排序。按 ID 游标分页应接受节点间交错;需要严格业务时间,使用 (occurred_at, id) 组合游标。ID 长度、符号范围和 JSON/JavaScript 精度也要纳入接口设计。
安全与隐私
连续可猜 ID 会暴露注册量、订单增长和资源枚举风险。对外 ID 可使用随机化、Hashids 变体或独立展示号,但要注意可逆性、碰撞和查询成本;不要把“不可猜”误认为授权,服务端仍需校验资源归属。
线上治理
节点号由可靠注册中心或部署系统分配并设租约,防止容器重启后冲突。监控发号延迟、序列耗尽、时间偏移、拒绝率和碰撞告警;对外 API 明确 ID 是否可排序、是否可能跳号、是否可能转成字符串。
常见问题
追问:为什么随机 UUID 不适合 InnoDB 主键?
随机键会让聚簇索引插入位置分散,增加页分裂、缓存局部性和二级索引存储成本;是否可接受仍取决于规模与实现。
追问:雪花算法能保证严格递增吗?
单节点按时间和序列递增,但多节点生成结果会交错,不能保证全局严格递增。若业务依赖严格顺序,应使用协调序列或业务版本。
追问:节点号冲突会发生什么?
两个节点在同一时间窗口产生相同序列可能生成重复 ID。节点号必须由租约/注册中心分配并监控,容器重启和网络分区都要测试。
追问:ID 服务不可用时应该继续发号吗?
号段模式可在已分配号段内继续,雪花可本地生成;一旦无法证明唯一性,应拒绝或切换可靠备用,而不是静默降级为时间戳。