MySQL分库分表如何选择ID

前言

单库场景,我们直接使用 BIGINT UNSIGNED AUTO_INCREMENT 自增主键就能满足需求。一旦做水平分库分表,原来的库内自增ID就会出现跨库ID重复,主键冲突。很多新手容易混淆两个概念:全局主键ID 和 分片键 ,这也是分库分表ID选型最容易踩坑的地方。

本文带你理清分库分表下ID选型标准,对比市面上主流ID方案,区分主键ID和分片键,梳理线上坑点,给出业务选型建议。

一、先分清两个核心概念:全局主键ID vs 分片键

很多人在这里踩坑,把主键和分片键当成同一个字段,带来大量查询性能问题。

  1. 全局主键ID :表的主键,全局唯一,唯一标识一行数据。不管数据落在哪个库、哪个表,ID不能重复,用于单条记录定位、唯一约束。
  2. 分片键(sharding key) :用来路由数据,决定这一行数据落到哪个库、哪个表,通过取模、范围路由等方式。分片键不要求全局唯一。

重要提醒:分片键和主键ID可以是同一个字段,也可以分开。

例如订单表:order_id(雪花ID,全局主键),分片键选 user_id;

也可以直接拿 order_id 取模作为分片键。

如果查询主要是按用户查订单,分片键推荐user_id;如果大多根据订单号查询,就可以把order_id同时作为分片键。

二、分库分表ID选型核心评估指标

在选择分布式ID方案时,需要同时满足下面几个维度,按需取舍:

  1. 全局唯一性:跨库跨表ID不能重复,这是底线。
  2. 存储友好:优先数字类型BIGINT,避免长字符串,减少索引体积。InnoDB二级索引都会带上主键,主键越长,索引膨胀越严重,缓冲池命中率下降。
  3. 趋势递增:尽量有序,减少InnoDB B+树页分裂,保证写入性能。
  4. 高性能&高可用:ID生成器不能成为整个系统单点瓶颈。
  5. 可运维:尽量自带时间信息,方便数据排查、归档。
  6. 安全性: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...

✅ 优点

  1. 无需额外服务,数据库原生支持;
  2. 数字类型,BIGINT存储,索引体积小;
  3. 趋势递增,InnoDB写入友好。

❌ 缺点

  1. 扩展性极差:分片数量一旦扩容,步长要改动,存量数据无法适配。比如从4库扩容到8库,原有ID会冲突。
  2. 运维成本高,每个数据库都要单独配置参数;
  3. 只适合分片数量固定、长期不扩容的老旧业务。

✅ 适用场景:分片固定,未来不会扩容的小型分表业务。

❌ 不推荐互联网业务,业务增长扩容时极易出事故。

四、方案2:雪花算法 Snowflake(业界主流推荐)

雪花ID是64位long数字,可以直接存入BIGINT UNSIGNED,不需要字符串。

结构:1位符号位 + 41位毫秒时间戳 + 10位机器ID + 12位序列号。

✅ 优点

  1. 全局唯一,程序生成,数据库不需要自增;
  2. 趋势递增,InnoDB写入性能好;
  3. 占用8字节,和自增BIGINT一样,索引体积小;
  4. ID自带时间信息,可以粗略按时间排序,方便排查;
  5. 去中心化,不需要依赖数据库/Redis生成ID。

❌ 缺点

  1. 需要保证机器ID不重复;
  2. 时钟回拨问题:服务器时间回拨,会产生重复ID,业务层需要做保护策略;
  3. 不是严格连续。

✅ 适用场景:绝大多数互联网业务(订单、流水、用户),分库分表首选。

注意:ShardingSphere内置雪花ID生成器,可以直接使用。

五、方案3:号段模式(美团Leaf / TinyID)

核心思路:一次性从数据库申请一大段ID,缓存到内存,业务直接从内存分配。耗尽再去数据库申请下一段。

✅ 优点

  1. 性能高,大部分ID从内存发放;
  2. 全局唯一,数字类型,BIGINT存储;
  3. 可以平滑扩容分片;
  4. 时钟回拨风险远低于雪花。

❌ 缺点

  1. 需要独立的发号数据库,存在单点风险,可以做主从;
  2. 服务重启,内存中未使用的号段会浪费,产生空洞。

✅ 适用场景:高并发核心业务,对时钟敏感,不想处理雪花时钟回拨问题的场景。

六、方案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冲突,属于重大线上事故,数据迁移成本极高。

十、最佳实践总结

  1. 优先推荐:雪花ID,字段类型BIGINT UNSIGNED,作为全局主键,适配大部分分库分表业务;同时单独设计分片键,区分主键和分片键。
  2. 如果业务对时钟回拨零容忍,高并发核心流水表,选用号段模式Leaf/TinyID。
  3. 尽量不要使用多库自增步长方案,除非确定分片永久固定,不会扩容。
  4. 尽量避免字符串类型(UUID)作为主键,高并发写入场景性能损耗明显。
  5. 设计表结构时,主键无业务含义;分片键根据业务查询模式选择,优先保证高频查询可以直接路由到单个分片,避免全分片广播查询。
  6. 分库分表架构下,唯一索引很难实现全局唯一,尽量依靠全局ID保证唯一性,慎用业务字段全局唯一索引。

一句话总结:分库分表ID优先选BIGINT类型的雪花ID;主键负责唯一标识一行,分片键负责路由数据,二者不要强行绑定。

相关推荐
imDwAaY1 小时前
SQL 查询突然变慢了?一篇讲清慢 SQL 排查思路
后端·mysql
花间相见1 小时前
【计算基础|MySQL03】【上手】多表查询与聚合
数据库·mysql
冰暮流星2 小时前
mysql索引失效的情形
数据库·笔记·sql·mysql
Pniubi2 小时前
三种count
mysql
ynchyong3 小时前
创建MySQL只读账号
android·数据库·mysql
狗凯之家源码网16 小时前
纯 PHP+MySQL 应用市场系统源码实测!
开发语言·mysql·php
姚远Oracle ACE16 小时前
对比测试了 openGauss、PostgreSQL、Oracle、MySQL 的CPU 指令数
mysql·postgresql·oracle
CJi0NG17 小时前
【自用】MySQL-多表查询
数据库·mysql