首页
学习
活动
专区
圈层
工具
发布

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

这事我见过不止一次:表还没长大,拿 UUID 当主键,开发环境一点感觉没有;一上量,MySQL 的页分裂、二级索引膨胀、插入抖动,全来了。你跟领导说“全局唯一、分布式方便”,领导回你一句也很直接:你先别讲道理,线上慢的是谁扛?

被怼不冤。

因为在 MySQL,尤其 InnoDB 里,主键不是一个普通字段,它就是聚簇索引。数据行本身是跟着主键顺序存的。你主键如果是 UUID 这种随机串,新的数据不会老老实实往后追加,而是东一榔头西一棒子插进各个数据页。看着只是一个 id 方案,落到存储层,就是频繁页分裂、缓存命中下降、二级索引也跟着变胖。

我一般不先争“雪花 id 好还是 uuid 好”,先看表结构和 SQL。

像这种表,我第一眼就不太信:

CREATE TABLE order_info (

  idVARCHAR(36) NOTNULL,

  user_id BIGINTNOTNULL,

  order_no VARCHAR(32) NOTNULL,

  statusTINYINTNOTNULL,

  create_time DATETIME NOTNULL,

  PRIMARY KEY (id),

  KEY idx_user_id (user_id),

  KEY idx_order_no (order_no)

) ENGINE=InnoDB;

id是VARCHAR(36),主键一长,二级索引叶子节点里存的主键值也跟着长。你以为只是主键占了点空间,实际上idx_user_id、idx_order_no后面都挂着这坨东西。数据一多,索引页能装下的记录数变少,B+ 树更高,查起来也更重。

有些同学这时候会说,那我不用 UUID,换雪花 ID 不就完了?

先别急。雪花 ID 比 UUID 强很多,至少趋势递增,对 InnoDB 友好得多。但它也不是“拿来就赢”。

我见过最典型的坑是这样的:

public class IdWorker {

  privatefinallong workerId;

  privatefinallong datacenterId;

  privatelong sequence = 0L;

  privatelong lastTimestamp = -1L;

  public synchronized long nextId() {

      long timestamp = System.currentTimeMillis();

      if (timestamp == lastTimestamp) {

          sequence = (sequence + 1) & 4095;

          if (sequence == 0) {

              while ((timestamp = System.currentTimeMillis()) <= lastTimestamp) {

                  // 自旋等下一毫秒

              }

          }

      } else {

          sequence = 0L;

      }

      if (timestamp < lastTimestamp) {

          thrownew IllegalStateException("clock moved backwards");

      }

      lastTimestamp = timestamp;

      return ((timestamp - 1700000000000L) << 22)

              | (datacenterId << 17)

              | (workerId << 12)

              | sequence;

  }

}

代码不复杂,问题也不在代码“能不能跑”,而在它背后的约束:机器号不能撞、时钟不能乱跳、并发打满时同毫秒序列不能溢。你本地一台机器测得挺欢,线上容器一扩缩容,workerId 配重了,或者 NTP 回拨一下,生成重复 id,这锅还是你背。

所以领导怼“别再上这些花里胡哨的主键”,有时候不是反对雪花 ID,本质上是在反对没把存储和运维代价算进去的技术选择

如果让我给方案,我一般这么落:

先看是不是单库单表、写入集中、没有跨系统提前生成 id 的硬需求。没有的话,BIGINT AUTO_INCREMENT最省事,插入顺序友好,维护成本最低。

如果业务明确要分布式生成,再上雪花 ID,但字段必须是BIGINT,不要再存成字符串,更别把Long转成前端喜欢看的各种花样格式再倒回库里。

像这样才算顺手:

CREATE TABLE order_info (

  idBIGINTNOTNULL,

  user_id BIGINTNOTNULL,

  order_no VARCHAR(32) NOTNULL,

  statusTINYINTNOTNULL,

  create_time DATETIME NOTNULL,

  PRIMARY KEY (id),

  KEY idx_user_id (user_id),

  KEY idx_order_no (order_no)

) ENGINE=InnoDB;

插入代码也别整虚的:

OrderInfoDO order = new OrderInfoDO();

order.setId(idWorker.nextId());

order.setUserId(userId);

order.setOrderNo(bizOrderNo);

order.setStatus((byte) 1);

order.setCreateTime(LocalDateTime.now());

orderMapper.insert(order);

还有个事很多人容易漏:不要拿主键有序,替代业务上的分页和时间排序。雪花 ID 只是大体递增,不是严格连续,也不是业务时间的精确替身。你拿它做“最近订单”排序,短期看没问题,碰上多机房、时钟漂移、补数据,就开始别扭。

所以这类问题,结论没那么玄乎:

UUID 做 MySQL 主键,最大的问题不是“占 36 个字符难看”,而是它和 InnoDB 的聚簇索引机制天然拧巴。 雪花 ID 可以用,但前提是你真有分布式生成的必要,并且能兜住机器号、时钟回拨、序列耗尽这些坑。 没有这些前提,老老实实BIGINT AUTO_INCREMENT,反而是最像线上答案的答案。

很多技术选型,坏就坏在只盯着“功能成立”,没盯着“代价落在哪”。主键这事尤其明显。它不是一个字段,它后面拖着整棵索引树。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Og8N8OpF4PySQm6GgbTWxa8g0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券