600 字
2 分钟
为什么总是需要无意义的 ID
在数据库中,我们经常看到这样的设计:
CREATE TABLE users ( id BIGINT PRIMARY KEY, -- 无意义的自增 ID name VARCHAR(255), email VARCHAR(255));为什么不用有意义的 ID(如邮箱、手机号)?为什么总是需要一个”无意义”的 ID?
一、什么是无意义的 ID?
1.1 无意义 vs 有意义
| 类型 | 示例 | 特点 |
|---|---|---|
| 无意义 ID | id = 1234567890 | 顺序、随机 |
| 有意义 ID | email = "user@example.com" | 包含业务信息 |
1.2 无意义 ID 的常见形式
# 自增 ID1, 2, 3, 4, ...
# UUID550e8400-e29b-41d4-a716-446655440000
# 雪花算法 ID69329847238479872
# ULID01ARZ3NDEKTSV4RRFFQ69G5FAV二、为什么需要无意义的 ID?
2.1 解耦:ID 与业务分离
flowchart LR
subgraph 有意义 ID 的问题
E[email 作为 ID] --> C[更换邮箱]
C --> U[ID 变更]
U --> D[所有外键更新]
end
subgraph 无意义 ID 的优势
I[自增 ID] --> M[业务变更]
M --> F[ID 不变]
F --> R[外键无需更新]
end
核心原则:ID 应该代表记录本身,而不是记录的属性。
2.2 性能:索引友好
自增 ID 的性能优势:
-- 自增主键:B+ 树插入在末尾,效率高INSERT INTO users VALUES (1000000, 'name'); -- 插入末尾
-- 随机 UUID:B+ 树随机插入,页分裂INSERT INTO users VALUES ('550e8400-...', 'name'); -- 随机位置| ID 类型 | 插入位置 | B+ 树页分裂 | 索引效率 |
|---|---|---|---|
| 自增 ID | 末尾 | 极少 | 高 |
| UUID | 随机 | 频繁 | 低 |
| 雪花算法 | 时间顺序 | 较少 | 高 |
2.3 安全性:隐藏业务信息
# 有意义 ID 泄露业务信息https://api.example.com/users/13824678# 攻击者知道用户数量 ≈ 1382 万
# 无意义 ID 不泄露信息https://api.example.com/users/69329847238479872# 攻击者无法推断任何业务信息2.4 扩展性:分布式友好
flowchart TB
subgraph 自增 ID 问题
S1[服务器 1] --> D[(DB)]
S2[服务器 2] --> D
S3[服务器 3] --> D
Note over S1,S2,S3: 单点瓶颈:DB 自增
end
subgraph 雪花算法
G1[生成器 1] --> ID1[ID: 1xxx]
G2[生成器 2] --> ID2[ID: 2xxx]
G3[生成器 3] --> ID3[ID: 3xxx]
Note over G1,G2,G3: 无中心,自主生成
end
三、各种 ID 生成方案对比
3.1 数据库自增
-- MySQL 自增CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY);
-- PostgreSQL 自增CREATE TABLE users ( id BIGSerial PRIMARY KEY);| 优点 | 缺点 |
|---|---|
| 简单 | 单点瓶颈 |
| 有序,插入效率高 | 无法分布式生成 |
| 紧凑(8 字节) | 暴露业务量 |
3.2 UUID
import uuid
# UUID v1:基于时间戳 + MAC 地址uuid.uuid1() # 550e8400-e29b-41d4-a716-446655440000
# UUID v4:完全随机uuid.uuid4() # 7c9e6679-7425-40de-944b-e07fc1f90ae7| 优点 | 缺点 |
|---|---|
| 无中心 | 随机,插入效率低 |
| 全球唯一 | 36 字符,太长 |
| 简单 | 无序 |
3.3 雪花算法(Snowflake)
# Twitter 雪花算法class Snowflake: def __init__(self, worker_id, datacenter_id): self.worker_id = worker_id self.datacenter_id = datacenter_id self.sequence = 0 self.last_timestamp = -1
def generate(self): timestamp = int(time.time() * 1000)
if timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 4095 else: self.sequence = 0
self.last_timestamp = timestamp
# 组装 ID return ((timestamp - 1288834974657) << 22) | \ (self.datacenter_id << 17) | \ (self.worker_id << 12) | \ self.sequenceflowchart LR
subgraph 雪花 ID 结构(64 位)
subgraph 时间戳(41 位)
T[毫秒时间戳]
end
subgraph 机器 ID(10 位)
M[工作机器]
end
subgraph 序列号(12 位)
S[序列号]
end
end
3.4 ULID
import ulid
# ULID:时间戳 + 随机数ulid.ulid() # 01ARZ3NDEKTSV4RRFFQ69G5FAV| 特性 | UUID v4 | 雪花算法 | ULID |
|---|---|---|---|
| 唯一性 | 全球唯一 | 机器内唯一 | 全球唯一 |
| 有序性 | 无 | 时间有序 | 时间有序 |
| 可读性 | 36 字符 | 18-20 位整数 | 26 字符 |
| 速度 | 快 | 快 | 快 |
四、ID 设计的最佳实践
4.1 选择合适的 ID 类型
# 单体应用:自增 ID 足够class UserRepository: def create(self, name): return self.db.insert("users", {"name": name}) # 返回自增 ID
# 分布式应用:雪花算法class DistributedUserRepository: def create(self, name): snowflake = Snowflake(worker_id=1, datacenter_id=1) user_id = snowflake.generate() return self.db.insert("users", {"id": user_id, "name": name})
# 多语言系统:UUID v4class MultiLangUserRepository: def create(self, name): user_id = str(uuid.uuid4()) return self.db.insert("users", {"id": user_id, "name": name})4.2 ID 作为主键 vs 业务键
-- 推荐做法:使用无意义 ID 作为主键,但保留业务唯一键CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, -- 无意义主键 user_uuid UUID UNIQUE NOT NULL, -- 外部暴露的 ID email VARCHAR(255) UNIQUE NOT NULL, -- 业务唯一键 phone VARCHAR(20) UNIQUE, -- 业务唯一键 created_at TIMESTAMP DEFAULT NOW());
-- 索引策略CREATE INDEX idx_users_uuid ON users(user_uuid); -- 快速查找CREATE INDEX idx_users_email ON users(email); -- 业务查询4.3 外部暴露的 ID
# 内部使用自增 IDinternal_id = 12345
# 外部暴露使用 UUID 或雪花 IDpublic_id = uuid.uuid4() # 6ba7b810-9dad-11d1-80b4-00c04fd430c8
# 或使用 base62 编码的雪花 IDpublic_id = base62.encode(snowflake_id) # 2VxNMwNMNMNMNMNMNMNMNM五、ID 的安全性考量
5.1 ID 遍历攻击
flowchart LR
A[攻击者] -->|枚举 ID| B[API]
B --> D1[用户 1 数据]
B --> D2[用户 2 数据]
B --> D3[用户 3 数据]
Note over A: 自增 ID 容易被遍历
防御措施:
- 使用 UUID 或雪花 ID
- 实施速率限制
- 使用不透明的 token
5.2 ID 与业务分离
# 内部表使用自增 IDclass User: __table__ = "users" __primary_key__ = "id" # 自增
# 外部暴露 UUIDclass UserPublic: __expose__ = ["uuid", "name"] # 不暴露内部 ID六、总结
为什么需要无意义的 ID:
| 原因 | 说明 |
|---|---|
| 解耦 | ID 不应随业务属性变化 |
| 性能 | 有序 ID 插入效率高 |
| 安全 | 不暴露业务信息 |
| 扩展 | 分布式环境下可生成 |
| 简洁 | 避免复合主键 |
选择建议:
| 场景 | 推荐 ID 类型 |
|---|---|
| 单体应用 | 自增 ID |
| 分布式应用 | 雪花算法 |
| 多语言/微服务 | UUID v4 |
| 需要时间排序 | ULID |
参考资料
- Snowflake ID Generator — Twitter 雪花算法
- ULID Specification — ULID 规范
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
为什么早期的 Windows 需要整理碎片
技术科普 深入解析为什么早期 Windows 系统需要定期磁盘碎片整理,FAT 和 NTFS 的设计问题。
2
为什么 Linux 需要 Swapping
技术科普 深入解析 Linux Swapping 机制,为什么需要将内存交换到磁盘,以及 swappiness 的作用。
3
为什么十进制计算也需要精确
技术科普 深入解析为什么金融计算需要精确的十进制运算,以及 Decimal 类型的实现原理。
4
为什么 WebSocket 需要握手
技术科普 深入解析 WebSocket 握手协议的设计原理,理解从 HTTP 到 WebSocket 的协议升级机制。
5
为什么 OLAP 需要列式存储
技术科普 深入解析为什么 OLAP 数据库使用列式存储,以及它相比行式存储的优势。






