mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
600 字
2 分钟
为什么总是需要无意义的 ID
2023-03-08

在数据库中,我们经常看到这样的设计:

CREATE TABLE users (
id BIGINT PRIMARY KEY, -- 无意义的自增 ID
name VARCHAR(255),
email VARCHAR(255)
);

为什么不用有意义的 ID(如邮箱、手机号)?为什么总是需要一个”无意义”的 ID?

一、什么是无意义的 ID?#

1.1 无意义 vs 有意义#

类型示例特点
无意义 IDid = 1234567890顺序、随机
有意义 IDemail = "user@example.com"包含业务信息

1.2 无意义 ID 的常见形式#

# 自增 ID
1, 2, 3, 4, ...
# UUID
550e8400-e29b-41d4-a716-446655440000
# 雪花算法 ID
69329847238479872
# ULID
01ARZ3NDEKTSV4RRFFQ69G5FAV

二、为什么需要无意义的 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.sequence
flowchart 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 v4
class 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#

# 内部使用自增 ID
internal_id = 12345
# 外部暴露使用 UUID 或雪花 ID
public_id = uuid.uuid4() # 6ba7b810-9dad-11d1-80b4-00c04fd430c8
# 或使用 base62 编码的雪花 ID
public_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 与业务分离#

# 内部表使用自增 ID
class User:
__table__ = "users"
__primary_key__ = "id" # 自增
# 外部暴露 UUID
class UserPublic:
__expose__ = ["uuid", "name"] # 不暴露内部 ID

六、总结#

为什么需要无意义的 ID:

原因说明
解耦ID 不应随业务属性变化
性能有序 ID 插入效率高
安全不暴露业务信息
扩展分布式环境下可生成
简洁避免复合主键

选择建议

场景推荐 ID 类型
单体应用自增 ID
分布式应用雪花算法
多语言/微服务UUID v4
需要时间排序ULID

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么总是需要无意义的 ID
https://blog.souloss.com/posts/why-the-design/why-meaningless-ids-are-always-needed/
作者
Souloss
发布于
2023-03-08
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时