UUID vs 自增ID做主键,哪个好?

UUID vs 自增ID

一、核心差异概览

维度 自增ID UUID
存储空间 4-8字节(int/bigint) 16字节(128位)
生成方式 数据库自动递增 应用层生成(随机/有序)
顺序性 严格递增,天然有序 通常无序(有序UUID除外)
唯一性范围 单库/表内唯一 全球唯一
可读性 简单直观(1,2,3...) 复杂(550e8400-e29b-41d4-a716-446655440000)
安全性 暴露业务量,易遍历 难以猜测,相对安全

二、技术深度对比

1. 存储与性能影响

自增ID的存储优势:
sql 复制代码
-- MySQL表结构对比
-- 自增ID表
CREATE TABLE users_auto (
id BIGINT AUTO_INCREMENT PRIMARY KEY,-- 8字节
name VARCHAR(100),
email VARCHAR(255),
INDEX idx_name (name)
);

-- UUID表
CREATE TABLE users_uuid (
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),-- 36字符 ≈ 36字节(实际存储36字节)
name VARCHAR(100),
email VARCHAR(255),
INDEX idx_name (name)
);

-- 二进制UUID更高效
CREATE TABLE users_uuid_bin (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),-- 16字节
name VARCHAR(100),
email VARCHAR(255)
);

性能影响分析:

  • 索引性能 :自增ID在B+树索引中连续插入,页分裂最少 ;UUID随机插入导致频繁页分裂
  • 缓存命中率 :自增ID的连续访问模式缓存友好 ;UUID随机访问缓存不友好
  • 存储成本:1000万行数据,UUID多占用约160MB存储空间

2. 插入性能实测对比

sql 复制代码
-- 性能测试示例(MySQL)
-- 测试1:连续插入100万条数据
SET profiling = 1;

-- 自增ID表
INSERT INTO users_auto (name, email)
SELECT CONCAT('user', n), CONCAT('user', n, '@test.com')
FROM generate_series(1, 1000000) AS n;

-- UUID表(随机)
INSERT INTO users_uuid (name, email)
SELECT CONCAT('user', n), CONCAT('user', n, '@test.com')
FROM generate_series(1, 1000000) AS n;

SHOW PROFILES;
-- 结果:自增ID插入耗时通常比UUID快2-3倍

3. 索引结构与分裂问题

复制代码
自增ID的B+树索引:
[1,2,3,4,5] ← 插入6 → [1,2,3,4,5,6]// 追加到末尾,无分裂

随机UUID的B+树索引:
[A,C,E,G,I] ← 插入B → 需要分裂和重平衡

有序UUID的B+树索引:
[U1,U2,U3,U4] ← 插入U5(时间顺序) → [U1,U2,U3,U4,U5] // 类似自增ID

三、实际应用场景选择

🎯 选择自增ID的场景

场景1:高写入吞吐的单体应用
sql 复制代码
-- 电商订单系统(MySQL)
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,-- 自增ID
order_no VARCHAR(32) UNIQUE,-- 业务订单号(对外)
user_id INT NOT NULL,
amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_created (user_id, created_at),-- 复合索引高效
INDEX idx_order_no (order_no)
);

-- 优势:快速插入,索引紧凑,范围查询高效
SELECT * FROM orders
WHERE user_id = 1001
AND created_at BETWEEN '2024-01-01' AND '2024-01-31'
ORDER BY id DESC;-- 按自增ID排序效率高
场景2:需要外键关联的场景
sql 复制代码
-- 用户-订单关系(外键关联)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE
);

CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 优势:外键关联更紧凑,JOIN性能更好
SELECT u.username, o.*
FROM users u
JOIN orders o ON u.id = o.user_id-- 整型JOIN效率高
WHERE u.id IN (1001, 1002, 1003);
场景3:需要分页查询的场景
sql 复制代码
-- 基于自增ID的高效分页
-- 传统分页(数据量大时慢)
SELECT * FROM products ORDER BY id LIMIT 1000000, 20;

-- 优化分页(使用自增ID)
SELECT * FROM products
WHERE id > 1000000-- 记录上次查询的最大ID
ORDER BY id
LIMIT 20;

🎯 选择UUID的场景

场景1:分布式系统多数据库
java 复制代码
// 微服务架构,多数据库实例
// 服务A(用户服务)- 数据库A
@Entity
public class User {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@Column(columnDefinition = "BINARY(16)")// 使用16字节存储
private UUID id;

private String username;
}

// 服务B(订单服务)- 数据库B
@Entity
public class Order {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@Column(columnDefinition = "BINARY(16)")
private UUID id;

@Column(columnDefinition = "BINARY(16)")
private UUID userId;// 跨服务引用,无需中心分配
}
场景2:前端生成ID的离线应用
javascript 复制代码
// 前端JavaScript生成UUID,离线创建数据
class OfflineTodoApp {
createTodo(text) {
const todo = {
id: crypto.randomUUID(),// 前端生成UUID
text: text,
completed: false,
createdAt: new Date().toISOString()
};

// 本地存储
localStorage.setItem(`todo_${todo.id}`, JSON.stringify(todo));

// 同步到服务器时无需ID转换
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(todo)
});
}
}
场景3:需要数据合并的场景
sql 复制代码
-- 分支机构数据合并到总部
-- 分支A数据
INSERT INTO customers_branch_a (id, name) VALUES
('550e8400-e29b-41d4-a716-446655440001', 'Alice'),
('550e8400-e29b-41d4-a716-446655440002', 'Bob');

-- 分支B数据
INSERT INTO customers_branch_b (id, name) VALUES
('6ba7b810-9dad-11d1-80b4-00c04fd430c1', 'Charlie'),
('6ba7b811-9dad-11d1-80b4-00c04fd430c2', 'David');

-- 合并到总部(无ID冲突)
INSERT INTO customers_headquarters (id, name, branch)
SELECT id, name, 'A' FROM customers_branch_a
UNION ALL
SELECT id, name, 'B' FROM customers_branch_b;

🎯 混合方案:结合两者优势

方案1:内部自增ID + 外部UUID
sql 复制代码
CREATE TABLE users (
-- 内部使用:自增ID,用于关联和索引
internal_id BIGINT AUTO_INCREMENT PRIMARY KEY,

-- 对外暴露:UUID,保证安全性和唯一性
public_id CHAR(36) UNIQUE NOT NULL DEFAULT (UUID()),

username VARCHAR(50),
email VARCHAR(255),
INDEX idx_public_id (public_id),
INDEX idx_username (username)
);

-- 对外API返回public_id
-- 内部关联使用internal_id
方案2:有序UUID(UUID v7)
sql 复制代码
-- MySQL 8.0+ 使用有序UUID
CREATE TABLE events (
id BINARY(16) PRIMARY KEY DEFAULT
(UUID_TO_BIN(UUID(), 1)),-- 参数1表示有序UUID
event_type VARCHAR(50),
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
INDEX idx_created (created_at)
);

-- 有序UUID生成原理(时间戳 + 随机部分)
-- 2024年时间戳(6字节) + 随机值(10字节)
-- 插入时基本按时间顺序,减少索引分裂

四、特殊场景深度分析

场景1:高并发秒杀系统

sql 复制代码
-- 方案对比
-- 自增ID方案
CREATE TABLE seckill_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32),-- 需要额外生成唯一订单号
user_id INT,
product_id INT,
INDEX idx_user_product (user_id, product_id)
);
-- 问题:热点写入,最后一个数据页竞争

-- UUID方案(有序v7)
CREATE TABLE seckill_orders (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID(), 1)),
user_id INT,
product_id INT,
INDEX idx_user_product (user_id, product_id)
);
-- 优势:分散写入热点,但索引效率降低

-- 最佳实践:分库分表 + 雪花ID

场景2:数据迁移与同步

sql 复制代码
-- 自增ID的迁移问题
-- 源数据库
INSERT INTO users (id, name) VALUES (1001, 'Alice');

-- 目标数据库(已有数据)
INSERT INTO users (id, name) VALUES (1, 'Bob');

-- 迁移时ID冲突,需要重置自增或重新映射
SET FOREIGN_KEY_CHECKS = 0;
INSERT INTO target_users (id, name)
SELECT id + 1000000, name FROM source_users;-- 需要偏移
SET FOREIGN_KEY_CHECKS = 1;

-- UUID无此问题
INSERT INTO target_users (id, name)
SELECT id, name FROM source_users;-- 直接迁移

场景3:数据安全与隐私

sql 复制代码
-- 自增ID的安全隐患
-- 容易推测数据量:id=1000000 表示有100万用户
-- 容易遍历:/api/users/1, /api/users/2, ...
-- 可能暴露业务信息:订单ID连续可能暴露订单量

-- UUID解决方案
CREATE TABLE users (
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
internal_id SERIAL,-- 内部管理用,不对外暴露
email VARCHAR(255),
INDEX idx_internal (internal_id)
);

-- API返回UUID,不返回自增ID
GET /api/users/550e8400-e29b-41d4-a716-446655440000
-- 无法推测其他用户ID

五、现代解决方案

1. 雪花算法(Snowflake)

java 复制代码
// Twitter Snowflake:结合时间戳+机器ID+序列号
// 64位ID结构:1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号
public class SnowflakeIdGenerator {
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;

public synchronized long nextId() {
long timestamp = System.currentTimeMillis();

if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}

if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095; // 12位序列号
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}

lastTimestamp = timestamp;

return ((timestamp - 1288834974657L) << 22)// 41位时间戳
| (machineId << 12)// 10位机器ID
| sequence;// 12位序列号
}
}

2. 数据库序列(Sequence)

sql 复制代码
-- PostgreSQL序列
CREATE SEQUENCE global_id_seq START 1 INCREMENT 1;

CREATE TABLE users (
id BIGINT PRIMARY KEY DEFAULT nextval('global_id_seq'),
name VARCHAR(100)
);

-- 多表共享序列,全局唯一ID
INSERT INTO orders (id, user_id) VALUES (nextval('global_id_seq'), 1001);
INSERT INTO products (id, name) VALUES (nextval('global_id_seq'), 'iPhone');

3. ULID(Universally Unique Lexicographically Sortable Identifier)

javascript 复制代码
// ULID:26字符,时间有序,Crockford's Base32编码
// 结构:48位时间戳 + 80位随机数
const ulid = ULID.ulid();// 示例:01ARYZ6S41TSV4RRFFQ69G5FAV

// 特性:
// 1. 按时间排序
// 2. 比UUID更短(26 vs 36字符)
// 3. 没有特殊字符,URL安全
// 4. 128位,与UUID相同熵

六、性能优化技巧

技巧1:UUID存储优化

sql 复制代码
-- 使用BINARY(16)代替CHAR(36)
-- 存储空间:36字节 → 16字节
-- 查询性能:提升约30%

-- 创建表
CREATE TABLE users (
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID())),
name VARCHAR(100)
);

-- 插入时转换
INSERT INTO users (id, name) VALUES (UUID_TO_BIN(UUID()), 'Alice');

-- 查询时转换回字符串
SELECT BIN_TO_UUID(id), name FROM users WHERE id = UUID_TO_BIN('uuid-string');

-- 有序UUID(UUID v7/v8)
INSERT INTO users (id, name) VALUES (UUID_TO_BIN(UUID(), 1), 'Bob');

技巧2:复合索引优化

sql 复制代码
-- 对于UUID主键,创建复合索引提升查询性能
CREATE TABLE orders (
id CHAR(36) PRIMARY KEY,
user_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 复合索引:优先高频查询条件
INDEX idx_user_created (user_id, created_at),
INDEX idx_created_id (created_at, id)
);

-- 查询使用覆盖索引
SELECT id, user_id
FROM orders
WHERE user_id = 1001
AND created_at >= '2024-01-01'
ORDER BY created_at DESC;

技巧3:分页优化

sql 复制代码
-- UUID分页的Keyset Pagination
-- 第一页
SELECT * FROM products
WHERE created_at > '2024-01-01'
ORDER BY created_at, id-- 加上ID保证顺序稳定
LIMIT 20;

-- 下一页:记住上一页最后一条的created_at和id
SELECT * FROM products
WHERE (created_at > '2024-01-10' OR
(created_at = '2024-01-10' AND id > 'last-uuid'))
ORDER BY created_at, id
LIMIT 20;

七、选型决策流程图

复制代码
开始选择主键
│
├── 是否分布式系统?
│├── 是 → 继续
│└── 否 → 考虑自增ID
│
├── 是否需要前端生成ID?
│├── 是 → UUID或ULID
│└── 否 → 继续
│
├── 是否有高并发写入?
│├── 是 → 雪花算法或有序UUID
│└── 否 → 继续
│
├── 是否需要数据合并?
│├── 是 → UUID或ULID
│└── 否 → 继续
│
├── 是否对安全要求高?
│├── 是 → UUID(不暴露业务信息)
│└── 否 → 继续
│
├── 存储成本是否敏感?
│├── 是 → 自增ID(节约存储)
│└── 否 → 继续
│
└── 性能要求?
├── 读多写少 → 自增ID
├── 写多读少 → 有序UUID或雪花算法
└── 读写均衡 → 根据其他因素决定

八、最佳实践总结

推荐方案:

  1. 单体应用,单数据库自增ID
  • 简单高效,维护方便
  • 外键关联性能好
  1. 微服务架构,多数据库UUID(BINARY存储)
  • 全局唯一,无需协调
  • 使用UUID_TO_BIN/UUID_TO_CHAR函数优化
  1. 高并发分布式系统雪花算法
  • 时间有序,性能接近自增ID
  • 分布式唯一
  1. 需要前端生成IDULID或UUID v4
  • ULID:时间有序,URL安全
  • UUID v4:JavaScript原生支持
  1. 混合需求内部自增ID + 外部UUID
  • 内部使用自增ID保证性能
  • 对外暴露UUID保证安全

性能优化黄金法则:

  1. 主键要短:能用int不用bigint,能用bigint不用UUID
  2. 主键要有序:减少索引分裂,提高插入性能
  3. 主键要唯一:分布式环境下尤其重要
  4. 考虑业务需求:安全性、合并需求、迁移需求

最后建议:

  • 测试:用真实业务数据量测试不同方案
  • 监控:监控数据库的页分裂、索引碎片情况
  • 演进:随着业务发展,方案可以调整和迁移

记住:没有绝对最好的方案,只有最适合当前业务场景的方案。在系统设计初期就要考虑到未来的扩展性需求。

相关推荐
利刃大大2 小时前
【SpringBoot】配置文件 && 日志输出 && lombok
java·spring boot·后端
猫豆~2 小时前
Ansible自动运维——6day
linux·数据库·sql·缓存·云计算
ChineHe2 小时前
Gin框架基础篇002_获取/绑定请求参数
后端·golang·gin
C+++Python2 小时前
如何选择合适的锁机制来提高 Java 程序的性能?
java·前端·python
李小先2 小时前
supersonic——TRANSLATING阶段
数据库
IT_陈寒2 小时前
JavaScript 性能优化:7 个 V8 引擎偏爱的编码模式让你提速 40%
前端·人工智能·后端
long3162 小时前
类与对象 | 低级别设计 (LLD)
java·spring boot·学习·程序人生·spring·设计模式·学习方法
专注于大数据技术栈2 小时前
java学习--String、StringBuilder、StringBuffer 的核心区别
java·学习
我命由我123452 小时前
Java 开发问题:包名 ‘com.my.compressimagetest‘ 与同名的类发生冲突
java·开发语言·学习·java-ee·intellij-idea·学习方法·intellij idea