这事我见过不止一次:表还没长大,拿 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,反而是最像线上答案的答案。
很多技术选型,坏就坏在只盯着“功能成立”,没盯着“代价落在哪”。主键这事尤其明显。它不是一个字段,它后面拖着整棵索引树。