前言
单库场景,我们直接使用
BIGINT UNSIGNED AUTO_INCREMENT自增主键就能满足需求。一旦做水平分库分表,原来的库内自增ID就会出现跨库ID重复,主键冲突。很多新手容易混淆两个概念:全局主键ID 和 分片键 ,这也是分库分表ID选型最容易踩坑的地方。本文带你理清分库分表下ID选型标准,对比市面上主流ID方案,区分主键ID和分片键,梳理线上坑点,给出业务选型建议。
一、先分清两个核心概念:全局主键ID vs 分片键
很多人在这里踩坑,把主键和分片键当成同一个字段,带来大量查询性能问题。
- 全局主键ID :表的主键,全局唯一,唯一标识一行数据。不管数据落在哪个库、哪个表,ID不能重复,用于单条记录定位、唯一约束。
- 分片键(sharding key) :用来路由数据,决定这一行数据落到哪个库、哪个表,通过取模、范围路由等方式。分片键不要求全局唯一。
重要提醒:分片键和主键ID可以是同一个字段,也可以分开。
例如订单表:
order_id(雪花ID,全局主键),分片键选user_id;也可以直接拿
order_id取模作为分片键。如果查询主要是按用户查订单,分片键推荐user_id;如果大多根据订单号查询,就可以把order_id同时作为分片键。
二、分库分表ID选型核心评估指标
在选择分布式ID方案时,需要同时满足下面几个维度,按需取舍:
- 全局唯一性:跨库跨表ID不能重复,这是底线。
- 存储友好:优先数字类型BIGINT,避免长字符串,减少索引体积。InnoDB二级索引都会带上主键,主键越长,索引膨胀越严重,缓冲池命中率下降。
- 趋势递增:尽量有序,减少InnoDB B+树页分裂,保证写入性能。
- 高性能&高可用:ID生成器不能成为整个系统单点瓶颈。
- 可运维:尽量自带时间信息,方便数据排查、归档。
- 安全性:ID不能轻易暴露业务总量。
三、方案1:多库自增ID(auto_increment_offset + auto_increment_increment)
原理:给每个数据库设置不同的起始偏移auto_increment_offset,步长等于分片总数。
示例:4个分库
- db1:offset=1,步长=4 → ID:1,5,9...
- db2:offset=2,步长=4 → ID:2,6,10...
- db3:offset=3,步长=4 → ID:3,7,11...
- db4:offset=4,步长=4 → ID:4,8,12...
✅ 优点
- 无需额外服务,数据库原生支持;
- 数字类型,BIGINT存储,索引体积小;
- 趋势递增,InnoDB写入友好。
❌ 缺点
- 扩展性极差:分片数量一旦扩容,步长要改动,存量数据无法适配。比如从4库扩容到8库,原有ID会冲突。
- 运维成本高,每个数据库都要单独配置参数;
- 只适合分片数量固定、长期不扩容的老旧业务。
✅ 适用场景:分片固定,未来不会扩容的小型分表业务。
❌ 不推荐互联网业务,业务增长扩容时极易出事故。
四、方案2:雪花算法 Snowflake(业界主流推荐)
雪花ID是64位long数字,可以直接存入BIGINT UNSIGNED,不需要字符串。
结构:1位符号位 + 41位毫秒时间戳 + 10位机器ID + 12位序列号。
✅ 优点
- 全局唯一,程序生成,数据库不需要自增;
- 趋势递增,InnoDB写入性能好;
- 占用8字节,和自增BIGINT一样,索引体积小;
- ID自带时间信息,可以粗略按时间排序,方便排查;
- 去中心化,不需要依赖数据库/Redis生成ID。
❌ 缺点
- 需要保证机器ID不重复;
- 时钟回拨问题:服务器时间回拨,会产生重复ID,业务层需要做保护策略;
- 不是严格连续。
✅ 适用场景:绝大多数互联网业务(订单、流水、用户),分库分表首选。
注意:ShardingSphere内置雪花ID生成器,可以直接使用。
五、方案3:号段模式(美团Leaf / TinyID)
核心思路:一次性从数据库申请一大段ID,缓存到内存,业务直接从内存分配。耗尽再去数据库申请下一段。
✅ 优点
- 性能高,大部分ID从内存发放;
- 全局唯一,数字类型,BIGINT存储;
- 可以平滑扩容分片;
- 时钟回拨风险远低于雪花。
❌ 缺点
- 需要独立的发号数据库,存在单点风险,可以做主从;
- 服务重启,内存中未使用的号段会浪费,产生空洞。
✅ 适用场景:高并发核心业务,对时钟敏感,不想处理雪花时钟回拨问题的场景。
六、方案4:UUID / UUIDv4 / UUIDv7
-
UUIDv4:完全随机字符串 CHAR(36)
✅优点:简单,无需额外组件,全局唯一。
❌缺点:无序,InnoDB大量页分裂,写入性能差;36字节字符串,索引膨胀严重,JOIN关联慢。严禁做主键。
-
UUIDv7:带时间有序的新版本UUID
✅优点:有序,缓解页分裂;全局唯一。
❌缺点:依然是字符串,存储占用远大于BIGINT,性能弱于雪花ID。
✅适用场景:跨系统数据同步,低并发场景;高并发业务不推荐。
七、方案5:Redis INCR生成ID
利用Redis的原子incr命令生成全局自增ID。
✅优点:实现简单,有序数字ID。
❌缺点:Redis单点风险,需要集群+持久化保证;高并发下Redis会成为瓶颈。
适合中小流量业务,不适合超高并发核心表。
八、选型对比表
| 方案 | 存储类型 | 全局唯一 | 有序 | 扩容能力 | 主要风险 |
|---|---|---|---|---|---|
| 多库自增步长 | BIGINT | ✅ | ✅ | ❌ 扩容困难 | 分片变更ID冲突 |
| 雪花算法 | BIGINT | ✅ | ✅ | ✅ | 时钟回拨 |
| Leaf号段 | BIGINT | ✅ | ✅ | ✅ | 发号库单点,号段浪费 |
| UUIDv4 | CHAR(36) | ✅ | ❌ | ✅ | 页分裂,索引膨胀 |
| UUIDv7 | CHAR(36) | ✅ | ✅ | ✅ | 字符串性能差 |
| Redis自增 | BIGINT | ✅ | ✅ | ✅ | Redis单点瓶颈 |
九、高频踩坑点
坑1:把分片键和主键混为一谈
订单表分片键选user_id,主键是order_id。如果查询不带user_id,直接根据order_id查询,框架会广播查询所有分片,读扩散,性能雪崩。
优化:如果经常根据order_id查询,可以建立全局索引表,存储order_id -> user_id映射。
坑2:雪花ID直接作为分片键,随机取模,用户查询全分片
如果订单表分片键用order_id取模,同一个用户的订单会散落在所有分片。查询该用户订单,必须扫描所有库,性能极差。
建议:订单表优先user_id作为分片键,order_id仅作为全局主键。
坑3:使用UUIDv4作为分库分表主键
随机无序字符串,大量写入时B+树频繁页分裂,IO飙升,二级索引体积巨大,缓冲池命中率暴跌。
坑4:忽略BIGINT存储,把雪花ID存入VARCHAR
雪花本身是64位数字,存入VARCHAR,白白浪费存储空间,字符串比较慢,索引性能大幅下降。
坑5:时钟回拨未做处理,线上ID重复
雪花算法最大隐患,服务器同步NTP时间发生回拨,同一台机器生成重复ID,引发主键冲突。解决方案:记录上一次生成ID的时间戳,时间回拨时阻塞或抛出异常。
坑6:前期使用多库步长自增,后期分片扩容
业务增长需要增加分片数量,旧ID和新分片ID冲突,属于重大线上事故,数据迁移成本极高。
十、最佳实践总结
- 优先推荐:雪花ID,字段类型BIGINT UNSIGNED,作为全局主键,适配大部分分库分表业务;同时单独设计分片键,区分主键和分片键。
- 如果业务对时钟回拨零容忍,高并发核心流水表,选用号段模式Leaf/TinyID。
- 尽量不要使用多库自增步长方案,除非确定分片永久固定,不会扩容。
- 尽量避免字符串类型(UUID)作为主键,高并发写入场景性能损耗明显。
- 设计表结构时,主键无业务含义;分片键根据业务查询模式选择,优先保证高频查询可以直接路由到单个分片,避免全分片广播查询。
- 分库分表架构下,唯一索引很难实现全局唯一,尽量依靠全局ID保证唯一性,慎用业务字段全局唯一索引。
一句话总结:分库分表ID优先选BIGINT类型的雪花ID;主键负责唯一标识一行,分片键负责路由数据,二者不要强行绑定。