用雪花 ID 和 UUID 做 MySQL 主键,被领导怼了
唯一就够了吗?你有没有想过 InnoDB 是如何存储数据的呢?
为什么随机 UUID 不适合直接做主键

CREATE TABLE orders_uuid (
id CHAR(36) NOT NULL,
user_id BIGINTNOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_user_id (user_id)
) ENGINE = InnoDB;
8a46bd38-f0f8-4a7c-92f7-69592be92667
1f376f44-c4c4-43a7-b857-fccbb24af3b8
f702d80e-00a9-47eb-9448-dfa50807b991
idx_user_id 并不是只存储 user_id 的东西,它还带有一个 id。如果主键用的是 CHAR(36),那么每一条二级索引都会重复地存储着这个很长的主键。表中数据量越大、二级索引越多,则额外的空间以及缓存的成本就会越高。
id CHAR(36) PRIMARY KEY
BINARY(16) 改成:
CREATE TABLE orders_uuid_bin (
id BINARY(16) NOT NULL,
user_id BIGINTNOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_user_id (user_id)
) ENGINE = InnoDB;
INSERT INTO orders_uuid_bin (id, user_id, status, created_at)
VALUES (UUID_TO_BIN(UUID()), 10001, 0, NOW());
SELECT BIN_TO_UUID(id) AS id, user_id, status
FROM orders_uuid_bin;
BINARY(16) 可以解决字符串 UUID 占用空间过大问题,但是如果使用的是仍然完全随机的 UUID,那么它并不能从根本上解决随机写入的问题。更合适的方案是使用具有时间顺序特点的 UUID,比如 UUIDv7,在此基础上用 16 字节二进制的形式来存储。
雪花 ID 比 UUID 更适合,但也不是万能答案

符号位 | 时间戳 | 机器标识 | 毫秒内序列号
BIGINT 来存储,只需要 8 字节的空间,比 CHAR(36) UUID 要小很多,也比随机 UUID 更适合 InnoDB 的追加写入模式。
CREATE TABLE orders_snowflake (
id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
order_no VARCHAR(32) NOT NULL,
status TINYINT NOT NULLDEFAULT0,
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id_created_at (user_id, created_at)
) ENGINE = InnoDB;
publicfinalclassSnowflakeIdGenerator {
privatestaticfinallongSEQUENCE_BITS=12L;
privatestaticfinallongWORKER_ID_BITS=10L;
privatestaticfinallongMAX_SEQUENCE= (1L << SEQUENCE_BITS) - 1;
privatefinallong workerId;
privatelonglastTimestamp= -1L;
privatelongsequence=0L;
publicSnowflakeIdGenerator(long workerId) {
if (workerId < 0 || workerId >= (1L << WORKER_ID_BITS)) {
thrownewIllegalArgumentException("workerId 超出范围");
}
this.workerId = workerId;
}
publicsynchronizedlongnextId() {
longtimestamp= System.currentTimeMillis();
if (timestamp < lastTimestamp) {
thrownewIllegalStateException("检测到时钟回拨,拒绝生成 ID");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return (timestamp << (WORKER_ID_BITS + SEQUENCE_BITS))
| (workerId << SEQUENCE_BITS)
| sequence;
}
privatelongwaitNextMillis(long previousTimestamp) {
longtimestamp= System.currentTimeMillis();
while (timestamp <= previousTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
SnowflakeIdGeneratorgenerator=newSnowflakeIdGenerator(12);
longorderId= generator.nextId();
Stringsql="""
INSERT INTO orders_snowflake
(id, user_id, order_no, status, created_at)
VALUES
(?, ?, ?, ?, NOW(3))
""";
-
每一个实例的 workerId都要唯一,否则不同的节点可能会产生相同的 ID; -
当服务器时钟回拨的时候,生成器要等待、出错或者切换备用策略; -
ID 暴露了大概生成时间以及业务的增长趋势,并不适宜直接用在需要防止被猜测的公开接口上。
自增主键为什么仍然是单库首选
BIGINT AUTO_INCREMENT 通常就是最简单的、可靠的解决方案了:
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
status TINYINT NOT NULLDEFAULT0,
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id_created_at (user_id, created_at)
) ENGINE = InnoDB;
INSERT INTO orders (order_no, user_id, status, created_at)
VALUES ('O202608010001', 10001, 0, NOW(3));
SELECT LAST_INSERT_ID();
物理主键和业务编号应该拆开

CREATE TABLE orders_final (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '数据库物理主键',
order_no VARCHAR(32) NOT NULL COMMENT '对外业务编号',
request_id BINARY(16) DEFAULTNULL COMMENT '跨系统请求标识',
user_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
status TINYINT NOT NULLDEFAULT0,
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
UNIQUE KEY uk_request_id (request_id),
KEY idx_user_id_created_at (user_id, created_at)
) ENGINE = InnoDB;
id
用于 InnoDB 存储和表内的关联,选择短小、递增的整数; order_no
为用户提供查询服务、客服交流以及接口展示; request_id
用于跨系统的追踪或者幂等控制,可以用到 UUID。
order_no 而不是暴露物理主键:
{
"orderNo":"O202608010001",
"status":"CREATED",
"amount":"99.00"
}
到底应该怎么选

BIGINT AUTO_INCREMENT。它并不是最先进方案,但是通常是最简单的方案,并且最容易被写入。
BIGINT 来存储。但是上线之前要解决节点编号、时钟回拨以及生成器高可用的问题。
BINARY(16);如果有选择版本的话,优先选择具有时间顺序特征的 UUID,而不能直接用随机 UUIDv4 来做聚簇主键。
微信赞赏
支付宝扫码领红包
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。侵权投诉:375170667@qq.com





