分布式 ID 生成
订单号、用户 ID、消息 ID,这些标识要全局唯一。单库时代自增主键就够了,拆库分表之后,多台机器各自生成 ID,怎么保证不撞车,还要尽量有序。本章只回答这一个问题。
1. 自增主键为什么失效
单库时 ID 靠数据库自增,AUTO_INCREMENT 或 GENERATED ALWAYS AS IDENTITY,由数据库保证唯一且递增。拆成多库或多实例后,每个库各自维护自己的自增序列,两个库都会从 1 开始往上加,撞车是必然的。
于是有了三种主流方案,区别在于唯一性和有序性分别怎么保证。
2. UUID
UUID 用随机数加时间戳拼出 128 位,本地生成,不依赖任何中心节点,绝对唯一。
它的致命伤是无序。UUID 是随机分布的,用它做主键,数据库 B+ 树每插一条都要在叶子节点随机找位置,页分裂频繁,索引碎片化严重,写入性能随数据量增长明显下降。
结论是 UUID 适合消息 ID、日志 ID 这类只要求唯一、不要求有序的场景。凡是要落数据库主键、要按时间排序的 ID,别用 UUID。
3. 雪花算法
雪花算法把 64 位拆成三段,时间戳、机器标识、序列号。时间戳占高位,保证整体递增;机器标识区分不同节点;序列号处理同一毫秒内的并发。
| 1 位符号 | 41 位时间戳(毫秒) | 10 位机器标识 | 12 位序列号 |时间戳放高位是关键。生成的 ID 随时间递增,做主键时插入基本都在索引末尾,页分裂少。10 位机器标识支持 1024 台机器,12 位序列号支持同一毫秒 4096 个 ID。
它有两个绕不开的坑。一是时钟回拨,机器的系统时间被 NTP 往前调,时间戳变小,可能生成重复 ID。处理方式要么抛异常拒绝服务,要么短暂等待时钟追回来。二是机器标识要分配,1024 台机器各自用哪个编号,得有一个配置或注册机制,不能靠机器自己猜。
4. 号段模式
号段模式把发号权交回数据库,但不每次取一条,而是一次取一段。数据库里一张表存当前已经发到哪,应用启动时取一段,比如 1 到 1000,本机在这段内自增,用完了再去取下一段。
它解决了两个问题。一是唯一性,靠数据库的行锁保证各实例拿到的号段不重叠。二是数据库压力,取号段是低频操作,一段能用很久,不像每插一条都依赖数据库自增。
代价是号段耗尽时机器的本地序列可能乱序,且拿号段这个动作本身依赖数据库,数据库挂了就发不出新 ID。它适合对 ID 只要求趋势递增、不要求严格连续的场景,比如订单号。
5. 怎么选
| 方案 | 唯一性 | 有序性 | 依赖 | 适用 |
|---|---|---|---|---|
| UUID | 本地保证 | 无序 | 无 | 消息、日志 ID |
| 雪花算法 | 时钟依赖 | 递增 | 时钟 | 订单、用户主键 |
| 号段模式 | 数据库保证 | 趋势递增 | 数据库 | 订单号等业务号 |
判断只看两个问题,这个 ID 要不要做主键,能不能容忍时钟问题。要主键又不想处理时钟回拨,用雪花算法;要业务号、能接受依赖数据库,用号段模式;只是要个唯一标识,UUID 最简单。
三种方案都不是银弹,共同的底线是,ID 生成逻辑要封装在一个独立的发号服务或工具类里,别散落在业务代码各处。散落之后想换方案,就是一场全量改造。