用雪花 id 和 uuid 做 MySQL 主键,被领导怼了

兄弟们,上周三下午,我正对着电脑美滋滋地敲代码,突然背后传来一声 “你这主键用的啥?”—— 回头一看,领导正皱着眉盯着我屏幕上的 MySQL 表结构。我挺得意地说 “用的雪花 ID 啊,分布式环境下唯一,多高级”,结果领导当场就怼了我一句:“高级?你知道这玩意儿给 MySQL 挖坑有多深吗?”

当时我脸一下子就红了,心里还嘀咕 “不就是个主键吗,能有啥大问题”,但后来跟着领导扒了半天原理,又自己做了测试,才发现原来选主键这事儿,真不是 “能生成唯一 ID 就行” 这么简单。今天就把我踩过的坑、搞懂的门道都跟大家掰扯掰扯,省得你们跟我一样,被领导怼了还不知道为啥。

先搞明白:MySQL 主键到底要的是啥?

在说雪花 ID 和 UUID 之前,咱得先统一个认知 ——MySQL(尤其是咱们常用的 InnoDB 引擎)对主键的要求,跟找对象似的,得 “门当户对” 才行。你不能光看 “唯一” 这一个优点,就不管其他条件了,不然早晚得出问题。

InnoDB 这引擎有个很关键的特性叫 “聚簇索引”,简单说就是 “主键索引和数据行绑在一块儿”。你可以把它理解成一本书,主键索引就是目录,数据行就是正文内容,目录的顺序和正文的顺序是完全对应的。要是目录乱序,你找内容的时候就得翻来翻去;要是目录顺序整齐,一下就能定位到地方。

所以 MySQL 主键的核心要求就三个,少一个都不行:

1. 唯一性:这是底线,没商量

不管是自增 ID、雪花 ID 还是 UUID,首先得保证 “不重复”。你总不能让两个数据共用一个主键吧?就像每个人的身份证号不能一样,不然银行取钱都能取错,这是最基本的要求,没啥好说的。

2. 有序性:这是性能的关键,很多人都忽略

刚才说的聚簇索引,要是主键是有序的,比如自增 ID 1、2、3、4……InnoDB 插入数据的时候,就知道直接往最后面加就行,跟排队似的,顺着来,效率特别高。

但要是主键是无序的,比如 UUID 那种长得乱七八糟的字符串,插入的时候就麻烦了 ——InnoDB 得先找这个 ID 该插在哪个位置,可能插在中间某个地方,这时候就需要 “页分裂”。啥是页分裂?你可以想象成书架上的书排满了,你要把一本新书插进中间,就得先把后面的书都往后挪一挪,腾出地方。数据量小的时候还好,数据量大了,这挪来挪去的功夫可就多了,插入速度会越来越慢,索引还会变得特别臃肿。

3. 占用空间小:越小越好,别给数据库添负担

主键是要存在索引里的,而且二级索引(比如你建的 name 索引、age 索引)里存的也是主键的值。要是主键占用空间大,比如 UUID 是 36 个字符,那索引文件就会变得特别大,不仅占磁盘空间,还会影响查询速度 —— 因为内存里能装下的索引数据少了,得频繁去磁盘读数据,速度能不慢吗?

搞懂这三个要求,咱们再回头看雪花 ID 和 UUID,为啥用它们做 MySQL 主键会被领导怼,就一目了然了。

先扒 UUID:看着万能,实则是 MySQL 的 “空间刺客”

咱们先说说 UUID,这玩意儿全称是 “通用唯一识别码”,格式大概是这样的:550e8400-e29b-41d4-a716-446655440000,一共 36 个字符,看着挺唬人,而且确实能保证全球唯一,不管你多少台机器生成,都不会重复。

很多刚接触分布式的同学,一听说要保证 ID 唯一,第一个想到的就是 UUID,觉得 “这玩意儿不用配置,拿来就用,多方便”。但你要是把它当 MySQL 主键,麻烦就来了。

问题 1:无序性直接触发 “页分裂地狱”

UUID 最大的问题就是 “无序”—— 你生成的两个 UUID,谁大谁小完全没规律。比如你刚插入一个550e8400开头的,下一个可能是a7164466开头的,再下一个又可能是12345678开头的。

这对 InnoDB 的聚簇索引来说,简直是灾难。我之前做过一个测试:用 UUID 当主键,往 MySQL 里插入 100 万条数据,前 10 万条的时候还挺顺畅,插入速度大概每秒 1 万条;但到了 50 万条之后,速度就掉到每秒 3000 条了;到 100 万条的时候,每秒只能插 1000 多条,而且磁盘 IO 占用率直接飙到 90% 以上。

后来我用工具查了一下索引情况,发现索引的 “碎片率” 高达 60%—— 这就是页分裂搞的鬼。因为每次插入都要挪数据,索引里全是碎片,就像你衣柜里的衣服乱堆一样,找的时候特别费劲。

反观用自增 ID 做主键,插入 100 万条数据,速度一直稳定在每秒 1.5 万条左右,索引碎片率只有 5% 不到。这差距,可不是一星半点。

问题 2:36 个字符的 “空间黑洞”,太费资源

UUID 是 36 个字符,要是用 VARCHAR (36) 存储,每个 UUID 要占用 36 个字节(要是用 UTF-8 编码,还可能更多)。咱们来算笔账:

假设你有一张用户表,有 1000 万条数据,主键是 UUID,那光主键索引就要占用 1000 万 × 36 字节 = 360MB。要是你再建几个二级索引,比如 name、phone、email,每个二级索引里都要存主键的值,那每个二级索引又要多占 360MB,几个索引加起来,光索引文件就好几 GB 了。

要是换成自增 ID,用 BIGINT 类型(8 个字节),同样 1000 万条数据,主键索引只需要 1000 万 × 8 字节 = 80MB,二级索引也跟着变小。同样的磁盘空间,能装下更多的数据和索引,查询的时候内存也能缓存更多索引,速度自然就快了。

有些同学可能会说 “我可以把 UUID 转成二进制存储啊,这样占用空间就小了”。没错,UUID 转成二进制是能从 36 字节降到 16 字节,但还是比自增 ID 的 8 字节大一倍,而且还有个更麻烦的问题:查询的时候你得把 UUID 转成二进制才能查,写 SQL 的时候特别麻烦,比如where id = UNHEX(550e8400-e29b-41d4-a716-446655440000),不仅容易写错,而且可读性极差,后续维护的时候,同事看到这种 SQL 得骂娘。

问题 3:查询性能差,尤其是范围查询

咱们平时查数据,经常会用范围查询,比如 “查昨天注册的用户”,要是主键是自增 ID,因为 ID 是有序的,InnoDB 直接就能定位到昨天的 ID 范围,快速查出数据。

但要是主键是 UUID,ID 是无序的,就算你按主键范围查,InnoDB 也得全表扫描(或者扫描大部分索引),因为它不知道这些 UUID 的范围对应的数据在哪里。我之前做过测试,查 “最近 1 万条数据”,自增 ID 主键只需要 0.02 秒,而 UUID 主键需要 0.8 秒,慢了 40 倍!

那 UUID 就一点用都没有了吗?也不是。比如你在分布式系统里给文件命名、给缓存键命名,这些场景不需要存在 MySQL 里,也不需要排序,用 UUID 就很合适。但要是当 MySQL 主键,那还是算了吧,纯属给自己找罪受。

再聊雪花 ID:比 UUID 靠谱,但坑也不少

说完 UUID,咱们再说说雪花 ID。雪花 ID 是 Twitter 搞出来的一种分布式 ID 生成算法,结构是 64 位的长整型(BIGINT),格式大概是这样的:

1 位符号位:固定 0,因为 ID 是正数41 位时间戳:能表示大概 69 年的时间(从某个起始时间开始算)10 位机器 ID:能表示 1024 台机器12 位序列号:每台机器每秒能生成 4096 个 ID(12 位最多 4095)

雪花 ID 的优点很明显:是有序的(因为有时间戳)、占用空间小(8 字节,和自增 ID 一样)、能保证分布式环境下唯一,看起来好像完美符合 MySQL 主键的要求,那为啥我用雪花 ID 还会被领导怼呢?

别着急,雪花 ID 的坑,比你想象的要多。

问题 1:时钟回拨是 “致命伤”

雪花 ID 的有序性,全靠前面的 41 位时间戳。但要是生成 ID 的机器出现 “时钟回拨”,麻烦就大了。

啥是时钟回拨?就是机器的系统时间突然往后跳了,比如本来是 2025 年 8 月 25 日,突然变成 2025 年 8 月 24 日了。这可能是因为机器同步了 NTP 服务器时间,也可能是系统出了故障。

一旦发生时钟回拨,雪花 ID 生成的时间戳就会比之前的小,生成的 ID 就会比之前的小,变成 “无序” 的。要是把这种无序的 ID 插进 MySQL,就会出现和 UUID 类似的问题:页分裂、插入速度变慢。

更严重的是,要是时钟回拨的时间比较长,还可能生成重复的 ID。比如机器 A 在 8 月 25 日 10 点生成了一个 ID,然后时钟回拨到 8 月 25 日 9 点,又生成了一个 ID,这两个 ID 的时间戳、机器 ID、序列号都可能一样,导致主键重复,插入数据直接报错。

我之前就遇到过这种情况:有个项目用了雪花 ID 当主键,有一次服务器重启后,NTP 同步时间,时钟回拨了 10 分钟,结果当天下午插入数据的时候,报了好几百次主键冲突错误,查了半天才发现是时钟回拨搞的鬼。

那怎么解决时钟回拨问题呢?有几种方案,但都不完美:

方案 1:检测到时钟回拨就暂停生成 ID,等时间追上了再继续。但这样会导致服务暂时不可用,要是在高并发场景下,比如秒杀活动,这绝对是灾难。方案 2:用物理时钟 + 逻辑时钟结合的方式,比如记录上次生成 ID 的时间戳,要是当前时间戳比上次小,就用上次的时间戳 + 1。但这样会导致 ID 的时间戳和实际时间不一致,后续要是想通过 ID 判断数据生成时间,就不准了。方案 3:多机房部署的时候,给每个机房分配不同的机器 ID 段,就算某个机房时钟回拨,也不会和其他机房的 ID 重复。但这需要复杂的配置和管理,小团队玩不转。

不管哪种方案,都需要额外的开发和维护成本,不像自增 ID 那样 “拿来就用,啥都不用管”。

问题 2:机器 ID 配置不当,分分钟重复

雪花 ID 的 10 位机器 ID,能表示 1024 台机器。但要是你配置机器 ID 的时候不小心,把两台机器配置成了同一个 ID,那这两台机器生成的雪花 ID 就会重复,插入 MySQL 的时候就会报主键冲突。

我之前见过一个团队,为了图省事,直接用机器的 IP 地址最后几位当机器 ID。结果有一次扩容,新增的机器 IP 最后几位和之前的机器重复了,导致生成的雪花 ID 重复,线上数据插入失败,排查了 3 个小时才找到原因,最后还得回滚数据,别提多狼狈了。

那机器 ID 该怎么配置呢?正确的做法是:

用 ZooKeeper、Etcd 这类分布式协调工具,给每台机器分配唯一的机器 ID,机器启动的时候去申请,关闭的时候释放。要是没有分布式协调工具,也可以手动分配机器 ID 段,比如给 A 机房分配 0-100,B 机房分配 101-200,每台机器在自己的段里选一个唯一的 ID。

但不管哪种方式,都需要额外的配置和维护,不像自增 ID 那样 “零配置”。

问题 3:在某些场景下,有序性也会出问题

雪花 ID 的有序性,是 “相对有序”,不是 “绝对有序”。因为它的排序优先级是:时间戳 > 机器 ID > 序列号。

也就是说,在同一毫秒内,不同机器生成的 ID,会按机器 ID 排序;同一机器同一毫秒内生成的 ID,会按序列号排序。

这在大部分场景下没问题,但要是你有 “严格按生成时间排序” 的需求,就可能出问题。比如你有一个订单表,要求订单 ID 严格按下单时间排序,要是两台机器在同一毫秒内生成订单 ID,机器 ID 大的那个,就算下单时间稍晚,ID 也会更大,导致订单 ID 的顺序和实际下单时间的顺序不一致。

虽然这种情况出现的概率不高,但要是你的业务对 ID 的时间顺序要求特别严格(比如金融场景),那雪花 ID 就不太合适了。

问题 4:迁移数据的时候,能让你哭

要是你用雪花 ID 当主键,后续迁移数据的时候,比如把数据从旧库迁到新库,或者分库分表,就会遇到一个麻烦:雪花 ID 是在应用层生成的,不是数据库生成的,所以迁移的时候,你得保证新库的 ID 和旧库一致,不能重复,也不能漏。

而要是用自增 ID,数据库会自动生成唯一的 ID,迁移的时候只需要把数据导过去就行,不用管 ID 的问题。

我之前参与过一个项目的数据库迁移,用的就是雪花 ID 当主键,结果迁移过程中,因为有部分数据的 ID 重复,导致迁移失败,最后不得不写了个脚本,先把旧库的 ID 全部导出来,再和新库的 ID 对比,花了整整两天才搞定,要是用自增 ID,半天就能搞定。

那 MySQL 主键到底该用啥?这 3 个方案才是王道

既然 UUID 和雪花 ID 当 MySQL 主键都有这么多坑,那到底该用啥呢?别着急,领导后来给我推荐了 3 个方案,亲测好用,咱们一个个说。

方案 1:小项目 / 单机项目,自增 IDyyds

要是你的项目是小项目,或者不需要分布式部署,就一台 MySQL 服务器,那自增 ID(AUTO_INCREMENT)绝对是最佳选择,没有之一。

自增 ID 的优点太多了:

完全符合 MySQL 主键的三个要求:唯一(数据库保证)、有序(每次 + 1)、占用空间小(8 字节)。零配置:不用自己写代码生成 ID,数据库自动搞定,省事儿。性能好:插入速度快,查询速度快,索引碎片少。方便迁移:迁移数据的时候不用管 ID,数据库自动生成。

那自增 ID 就没缺点吗?也有,比如:

分布式环境下不唯一:要是你有多个 MySQL 实例,每个实例都自增,就会出现重复的 ID。容易被猜到:比如你的用户 ID 是自增的,别人很容易猜到你有多少用户,也容易通过 ID 遍历数据(比如从 1 开始,依次访问 /user/1、/user/2)。

但对于小项目 / 单机项目来说,这些缺点根本不是问题。比如你做一个企业内部的管理系统,就一台 MySQL 服务器,用户量也就几千人,用自增 ID 完全没问题,简单又高效。

方案 2:分布式项目,数据库分段自增 ID 更靠谱

要是你的项目是分布式的,需要多台 MySQL 服务器(比如分库分表),自增 ID 就不够用了,这时候可以用 “数据库分段自增 ID”。

啥是数据库分段自增 ID?简单说就是:专门建一个 “ID 生成器” 数据库,里面有一张表,记录每个业务表的 ID 当前最大值和步长,每次应用需要生成 ID 的时候,就去这个表拿一段 ID(比如拿 1000 个),然后在应用里自己慢慢用,用完了再去拿下一段。

举个例子,比如用户表的 ID:

先建一个 ID 生成器表:
复制
CREATE TABLE id_generator ( table_name VARCHAR(50) NOT NULL COMMENT 业务表名, current_max_id BIGINT NOT NULL COMMENT 当前最大ID, step INT NOT NULL COMMENT 步长, PRIMARY KEY (table_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT ID生成器表;1.2.3.4.5.6.
初始化用户表的 ID 配置:
复制
INSERT INTO id_generator (table_name, current_max_id, step) VALUES (user, 0, 1000);1.
应用需要生成用户 ID 的时候,先执行以下 SQL,拿一段 ID(0-999):
复制
UPDATE id_generator SET current_max_id = current_max_id + step WHERE table_name = user AND current_max_id = 0;1.2.3.4.
应用拿到这段 ID 后,就可以从 0 开始,依次生成 0、1、2……999,用完了再去拿下一段(1000-1999)。

这种方案的优点:

有序性:ID 是连续的,符合 MySQL 主键的要求,不会出现页分裂。分布式唯一:因为所有应用都从同一个 ID 生成器拿 ID,所以不会重复。性能好:每次拿一段 ID,不用每次生成 ID 都访问数据库,减少数据库压力。配置简单:不用依赖 ZooKeeper、Etcd 这些分布式协调工具,只需要一个数据库就行。

缺点也有,就是 ID 生成器数据库是单点,要是这个数据库挂了,所有需要生成 ID 的业务都得停。不过可以搞主从复制,主库挂了就切从库,解决单点问题。

我之前参与的一个电商项目,用的就是这种方案,分了 10 个库,每个库有 10 个表,每天新增订单 100 多万,用数据库分段自增 ID,从来没出现过 ID 重复或者性能问题,特别稳定。

方案 3:高并发场景,Redis 生成 ID 也不错

要是你的项目并发特别高,比如秒杀活动,每秒要生成几万甚至几十万的 ID,数据库分段自增 ID 可能会有点吃力(因为每次拿段 ID 都要访问数据库),这时候可以用 Redis 生成 ID。

Redis 生成 ID 的原理很简单:利用 Redis 的 INCR 命令(原子性递增),每次生成 ID 的时候,就调用 INCR 命令,让某个键的值加 1,这个值就是新的 ID。

比如生成用户 ID:

先在 Redis 里设置一个键,初始值为 0:
复制
SET user_id 01.
每次需要生成用户 ID 的时候,调用 INCR 命令:
复制
INCR user_id1.

这样每次调用 INCR,都会返回一个唯一的、有序的 ID。为了提高性能,也可以像数据库分段自增那样,一次性从 Redis 拿一段 ID,比如拿 1000 个:

复制
INCRBY user_id 10001.

这样就能拿到一段 ID(比如从 1001 到 2000),然后在应用里自己慢慢用。Redis 生成 ID 的优点:

性能极高:Redis 是内存数据库,INCR 命令的性能特别好,每秒能处理几十万次请求,完全能满足高并发场景。有序性:ID 是连续递增的,符合 MySQL 主键要求。分布式唯一:所有应用都访问同一个 Redis,不会出现 ID 重复。

缺点:

需要依赖 Redis:要是 Redis 挂了,ID 生成就会出问题,所以得搞 Redis 集群,保证高可用。数据持久化问题:要是 Redis 没有持久化,或者持久化失败,Redis 重启后,ID 会从之前的值开始,可能会重复。所以需要开启 Redis 的 AOF 持久化,并且配置合适的持久化策略。

这种方案适合高并发场景,比如秒杀、直播带货这些需要快速生成大量 ID 的业务。我之前做过一个秒杀项目,每秒并发 10 万 +,用 Redis 生成订单 ID,特别稳定,从来没掉过链子。

总结:别再盲目跟风,选对主键才是王道

看到这里,你应该明白为啥我用雪花 ID 当 MySQL 主键会被领导怼了吧?不是雪花 ID 不好,也不是 UUID 不好,而是它们不适合当 MySQL 主键。

选 MySQL 主键,就像选鞋子,不是越贵越好,也不是越高级越好,而是要合脚。总结一下:

小项目 / 单机项目:直接用自增 ID,简单高效,不用瞎折腾。分布式项目(中低并发):用数据库分段自增 ID,稳定可靠,配置简单。分布式项目(高并发):用 Redis 生成 ID,性能极高,能扛住大流量。要是你实在想用雪花 ID:那一定要做好时钟回拨处理和机器 ID 配置,并且接受它可能带来的迁移麻烦和排序问题。至于 UUID:除非你脑子进水了,否则别把它当 MySQL 主键。

技术没有好坏之分,只有合适不合适。别看到别人用雪花 ID、用 UUID,你就跟着用,得先搞明白背后的原理,结合自己的业务场景,才能做出正确的选择。不然哪天被领导怼了,还不知道为啥,多冤啊!

阅读剩余
THE END