核心结论先行 :自增 ID 只适合简单单库单表的玩具项目,绝对不能用于生产环境的核心表(尤其是用户表、订单表);纯 UUID 是比自增 ID 更糟糕的选择;企业级首选是 雪花算法及其变种 **,用BIGINT存储,兼顾性能、唯一性和扩展性。**
一、先讲透:为什么用户表主键绝对不能用数据库自增 ID?
很多人以为自增 ID"简单、性能好",但它在生产环境有 5 个致命缺陷,每一个都可能导致系统崩溃或数据泄露:
1. 分库分表时会产生 ID 冲突(最致命)
这是自增 ID 最大的问题,也是你之前问分库分表时必然会遇到的。
- 如果你把用户表拆成
user_db_0和user_db_1两个库,每个库的自增 ID 都会从 1 开始 - 结果就是两个库都会有
id=1、id=2的用户,全局唯一主键彻底失效 - 没有任何简单的办法可以解决这个问题,只能放弃自增 ID
2. 严重的安全隐患
自增 ID 是连续的,会泄露极其敏感的业务信息:
- 爬虫可以按
id=1,2,3...遍历你的所有用户数据,轻松爬取整个用户库 - 订单号用自增 ID 的话,竞争对手可以通过订单号算出你的日订单量、月销售额
- 用户 ID 暴露在 URL 中时,别人可以轻易猜出你有多少用户,以及用户的注册顺序
3. 数据迁移和合并时会产生灾难
- 当你需要把两个系统的用户数据合并时,自增 ID 会大量冲突,需要手动处理,极其容易出错
- 当你需要从旧库迁移数据到新库时,必须重新生成 ID,所有关联表的外键都要跟着改,工作量巨大
4. 分布式系统中无法使用
在微服务架构下,多个服务实例同时生成 ID,自增 ID 根本无法保证全局唯一。
5. 无法提前获取 ID
自增 ID 必须插入数据库后才能生成,导致很多业务逻辑无法实现:
- 比如你需要在插入用户之前,先生成用户 ID,然后用这个 ID 去调用其他服务
- 比如批量插入数据时,无法提前知道每条数据的 ID
二、为什么纯 UUID 是更糟糕的选择?
很多人第一反应是 "用 UUID 啊,全局唯一",但纯 UUID 作为主键,是比自增 ID 更糟糕的选择,在 InnoDB 中性能会差 10 倍以上。
1. 无序性导致 InnoDB 插入性能灾难
这是 UUID 最致命的问题。InnoDB 的主键是聚簇索引,数据是按照主键的顺序物理存储的。
- 纯 UUID 是完全无序的,每次插入新数据都可能插入到索引的中间位置
- 这会导致频繁的页分裂,大量的数据移动和碎片,插入性能断崖式下跌
- 当表的数据量超过千万时,插入性能会变得无法接受
2. 存储空间浪费巨大
- 标准 UUID 是 36 位的字符串(比如
550e8400-e29b-41d4-a716-446655440000) - 如果用
VARCHAR(36)存储,每个 ID 需要 36 字节,是BIGINT(8 字节)的 4.5 倍 - 主键会被所有二级索引引用,导致所有索引都变得巨大,查询性能也会下降
3. 没有任何业务含义,排查问题困难
UUID 是一串随机字符串,你无法从 ID 中看出任何信息,排查问题时极其不方便。
4. 误区纠正:UUID 可以用BINARY(16)存储,但依然不推荐
很多人说 " 把 UUID 转换成二进制用BINARY(16)存储就好了 ",确实可以把存储空间从 36 字节降到 16 字节,但依然解决不了无序性导致的插入性能问题。
三、企业级首选:雪花算法(Snowflake)及其变种
这是目前互联网公司使用最广泛的 ID 生成方案,完美解决了自增 ID 和 UUID 的所有问题。
1. 雪花算法核心结构(64 位长整型)
雪花算法生成的是一个 64 位的整数,正好可以用 MySQL 的BIGINT类型存储(BIGINT就是 64 位)。
| 符号位(1 位) | 时间戳(41 位) | 机器 ID(10 位) | 序列号(12 位) |
|---|---|---|---|
| 0(正数) | 毫秒级时间戳 | 分布式机器唯一标识 | 同一毫秒内的自增序号 |
- 时间戳:可以使用约 69 年(从 1970 年到 2039 年),足够用了
- 机器 ID:最多支持 1024 台机器,足够绝大多数公司使用
- 序列号:每台机器每毫秒最多可以生成 4096 个 ID,理论上每秒可以生成 409.6 万个 ID,性能极高
2. 雪花算法的核心优势
- 全局唯一:通过时间戳 + 机器 ID + 序列号的组合,保证分布式环境下绝对唯一
- 趋势有序:ID 整体是随着时间递增的,完美适配 InnoDB 的聚簇索引,插入性能极高
- 性能极高:纯内存计算,不需要访问数据库或 Redis,每秒可以生成几百万个 ID
- 存储空间小 :用
BIGINT存储,只需要 8 字节,是 UUID 的 1/4 - 有一定业务含义:可以从 ID 中提取出生成时间,排查问题时非常方便
3. 雪花算法的唯一问题:时钟回拨
雪花算法依赖系统时钟,如果服务器的时钟回拨了(比如 NTP 同步错误),就可能生成重复的 ID。
解决方案:
- 大厂会使用自己的 ID 生成服务,比如美团的 Leaf、百度的 UidGenerator、阿里的 TinyID,这些都解决了时钟回拨问题
- 中小公司可以使用 MyBatis-Plus 自带的雪花算法实现,它已经做了简单的时钟回拨处理
- 或者使用号段模式,完全不依赖时钟,从根本上解决时钟回拨问题
四、其他企业级 ID 生成方案
1. 号段模式(数据库自增的升级版)
核心思想:每次从数据库中获取一段 ID(比如 1-1000),缓存在本地,用完再去取下一段。
优点:
- 完全不依赖时钟,没有时钟回拨问题
- 实现简单,可靠性高
- 性能也很高,一次获取一段可以用很久
缺点:
- 依赖数据库,数据库挂了会导致 ID 生成服务不可用
- ID 是趋势有序的,但不是严格有序的
适用场景:对时钟回拨特别敏感的场景,或者不想引入复杂的雪花算法实现的场景。
2. Redis 自增模式
核心思想 :利用 Redis 的INCR命令生成自增 ID。
优点:
- 实现简单
- 性能不错
缺点:
- 依赖 Redis,Redis 挂了会导致 ID 生成服务不可用
- 需要做 Redis 集群来保证高可用
- 不适合超大规模的场景
适用场景:并发量不高的内部系统。
五、企业级最佳实践(直接抄作业)
1. 主键类型和存储
- 绝对不要用 :
INT自增、VARCHAR(36)UUID - 推荐用 :
BIGINT UNSIGNED存储雪花算法生成的 ID - 不推荐但可以接受 :
BINARY(16)存储有序 UUID(比如 UUID v1)
2. ID 生成方案选型
| 业务场景 | 首选方案 | 备选方案 |
|---|---|---|
| 中小项目,单库单表,无分库分表计划 | MyBatis-Plus 自带雪花算法 | 自增 ID(但不要暴露给前端) |
| 中大型项目,有分库分表需求 | 美团 Leaf(号段模式) | 百度 UidGenerator |
| 超大规模项目,高并发 | 美团 Leaf(雪花模式) | 阿里 TinyID |
| 不想自己部署 ID 生成服务 | MyBatis-Plus 自带雪花算法 | 无 |
3. 关键注意事项
-
永远不要把主键暴露给前端 即使你用了雪花算法,也不要直接把用户 ID、订单 ID 暴露在 URL 或接口返回中。应该用一个单独的
biz_id字段,生成一个随机字符串作为对外的业务 ID。 -
不要自己实现雪花算法自己实现很容易出问题,比如机器 ID 冲突、时钟回拨处理不当。直接用成熟的开源实现就好。
-
主键不要用业务字段永远不要用手机号、身份证号、邮箱等业务字段作为主键。业务字段可能会变,而且长度通常比较大,性能差。
-
分库分表时的分片键选择如果你用雪花算法生成了用户 ID,那么分库分表时直接用用户 ID 作为分片键就可以了,非常方便。
六、面试时的标准回答
当面试官问你这个问题时,你可以这样回答:
我不推荐用数据库自增 ID 作为用户表的主键,主要有几个原因:
第一,分库分表时会产生 ID 冲突,无法保证全局唯一;
第二,有严重的安全隐患,连续的 ID 会泄露业务数据;
第三,数据迁移和合并时会非常麻烦;第四,分布式系统中无法使用。
纯 UUID 也不推荐,因为它是完全无序的,会导致 InnoDB 的聚簇索引频繁页分裂,插入性能极差,而且存储空间也很大。
在企业级项目中,我们一般会用雪花算法及其变种来生成主键。雪花算法生成的是 64 位的长整型,用 BIGINT 存储,全局唯一,趋势有序,性能极高,完美适配 InnoDB 的聚簇索引。
对于中小项目,我们可以直接用 MyBatis-Plus 自带的雪花算法实现;对于中大型项目,我们会使用成熟的 ID 生成服务,比如美团的 Leaf 或者百度的 UidGenerator,这些都解决了雪花算法的时钟回拨问题。
另外,我们永远不会把主键直接暴露给前端,而是会用一个单独的业务 ID 来对外使用,保证数据安全。
七、最后总结
- 自增 ID:只适合玩具项目,生产环境核心表绝对不能用
- 纯 UUID:比自增 ID 更糟糕,绝对不要作为主键
- 雪花算法:企业级首选,兼顾性能、唯一性和扩展性
- 存储类型 :永远用
BIGINT存储 ID,不要用VARCHAR