一、主键概述
主键(Primary Key)是数据库表中唯一标识每条记录的字段或字段组合。主键在数据库设计中至关重要,它不仅用于唯一标识记录,还用于建立表之间的关联关系和优化查询性能。
二、主键设计原则
2.1 唯一性
主键必须唯一,确保每条记录都有唯一标识。
2.2 稳定性
主键的值不应频繁变更,一旦设定就应保持不变。
2.3 非空性
主键字段不允许为空值(NOT NULL)。
2.4 简洁性
主键应尽可能简洁,避免使用过长的字符串或复合主键。
2.5 业务无关性
建议使用与业务无关的主键,避免使用业务含义的字段。
graph TD
A[主键设计原则] --> B[唯一性]
A --> C[稳定性]
A --> D[非空性]
A --> E[简洁性]
A --> F[业务无关性]
B --> B1[唯一标识记录]
C --> C1[避免关联失效]
D --> D1[确保完整性]
E --> E1[提高性能]
F --> F1[灵活应对业务变化]
三、常见主键类型对比
| 主键类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 自增ID | 简单高效,索引友好 | 分布式环境冲突 | 单体应用 |
| UUID | 全局唯一,分布式友好 | 索引性能差,空间占用大 | 分布式系统 |
| 雪花算法 | 有序,高性能,分布式友好 | 依赖时钟,复杂度较高 | 大规模分布式 |
| 业务主键 | 业务含义明确 | 易变更,扩展性差 | 特定业务场景 |
四、自增ID设计
4.1 原理
自增ID是数据库自动生成的递增数字,每次插入记录时自动加1。
4.2 MySQL实现
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL
);
4.3 PostgreSQL实现
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL
);
4.4 优缺点分析
graph TD
A[自增ID] --> B[优点]
A --> C[缺点]
B --> B1[生成简单]
B --> B2[索引效率高]
B --> B3[存储空间小]
C --> C1[分布式冲突]
C --> C2[ID可预测]
C --> C3[单点故障]
五、UUID设计
5.1 原理
UUID(Universally Unique Identifier)是一种标准化的唯一标识符,通常由32位十六进制数组成。
5.2 UUID版本
- UUID v1:基于时间戳和MAC地址
- UUID v4:完全随机
- UUID v5:基于命名空间和名称
5.3 生成方式
// C#生成UUID
Guid.NewGuid().ToString()
// Java生成UUID
UUID.randomUUID().toString()
// MySQL生成UUID
UUID()
5.4 存储优化
UUID存储占用较大,可以使用BINARY(16)代替VARCHAR(36):
CREATE TABLE users (
id BINARY(16) PRIMARY KEY,
username VARCHAR(50) NOT NULL
);
六、雪花算法设计
6.1 原理
雪花算法(Snowflake)是Twitter开源的分布式ID生成算法,生成64位的long型ID:
graph LR
A[雪花算法ID结构] --> B[1位: 符号位]
A --> C[41位: 时间戳]
A --> D[10位: 机器ID]
A --> E[12位: 序列号]
B --> B1[固定为0]
C --> C1[约69年]
D --> D1[最多1024台]
E --> E1[每毫秒4096个]
6.2 实现代码
public class SnowflakeIdGenerator
{
private readonly long _workerId;
private readonly long _dataCenterId;
private long _sequence = 0L;
private long _lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long dataCenterId)
{
_workerId = workerId;
_dataCenterId = dataCenterId;
}
public synchronized long NextId()
{
long timestamp = GetCurrentTimestamp();
if (timestamp < _lastTimestamp)
{
throw new Exception("时钟回拨");
}
if (timestamp == _lastTimestamp)
{
_sequence = (_sequence + 1) & 4095;
if (_sequence == 0)
{
timestamp = WaitNextMillis(_lastTimestamp);
}
}
else
{
_sequence = 0L;
}
_lastTimestamp = timestamp;
return ((timestamp - 1288834974657L) << 22)
| (_dataCenterId << 17)
| (_workerId << 12)
| _sequence;
}
}
6.3 时钟回拨问题
雪花算法依赖系统时钟,如果时钟回拨会导致ID重复。解决方案:
- 等待时钟恢复:暂停生成ID直到时钟恢复
- 使用NTP同步:确保时钟同步
- 备用方案:时钟异常时使用其他算法
七、分布式ID生成方案对比
| 方案 | 优点 | 缺点 | 性能 |
|---|---|---|---|
| 雪花算法 | 高性能,有序,无依赖 | 依赖时钟 | 极高 |
| 数据库自增 | 简单,可靠 | 性能瓶颈,单点故障 | 低 |
| Redis自增 | 高性能,分布式友好 | 依赖Redis | 高 |
| UUID | 完全独立,无依赖 | 索引性能差 | 中 |
八、主键选择策略
8.1 单体应用
单体应用建议使用自增ID,简单高效。
8.2 分布式应用
分布式应用建议使用雪花算法或UUID。
8.3 数据迁移场景
数据迁移场景建议使用UUID,避免ID冲突。
8.4 高性能场景
高性能场景建议使用雪花算法,兼顾性能和有序性。
flowchart TD
A[选择主键类型] --> B{部署方式}
B -->|单体应用| C[自增ID]
B -->|分布式应用| D{性能要求}
D -->|极高| E[雪花算法]
D -->|一般| F[UUID]
B -->|数据迁移| F
九、主键设计最佳实践
9.1 避免使用业务主键
业务主键容易变更,建议使用与业务无关的主键。
9.2 使用单一主键
尽量使用单一字段作为主键,避免复合主键。
9.3 预分配ID
在分布式系统中,可以预分配ID段,减少网络交互。
9.4 考虑索引性能
主键是聚集索引,应考虑插入顺序对索引性能的影响。
9.5 定期清理历史数据
对于主键范围查询频繁的表,定期清理历史数据可以提高查询性能。
十、总结
主键设计是数据库设计的重要环节,选择合适的主键类型对于系统的性能、可扩展性和数据完整性至关重要。在实际应用中,应根据系统架构和业务需求选择最合适的主键方案。