关系型数据库主键生成策略
主键是关系型数据库表的行唯一标识。选择合适的主键生成策略直接影响写入性能、索引效率、以及分布式扩展能力。
1. 主键选型的核心考量
1.1 评估维度
| 维度 | 说明 | 为什么重要 |
|---|---|---|
| 有序性 | ID 是否趋势递增 | 有序 ID 减少 B+ 树页分裂,写入性能更高 |
| 全局唯一 | 分布式环境下是否保证唯一 | 分库分表后单库自增不再唯一 |
| 存储效率 | ID 占用字节大小 | 主键作为二级索引的叶子节点值,越短越省空间 |
| 插入性能 | 批量插入是否支持 | IDENTITY 模式下 JDBC 批量插入失效 |
| 可读性 | ID 是否携带业务信息 | 订单号、流水号需要语义化 |
| 依赖程度 | 是否依赖外部组件 | 雪花算法纯本地生成,号段模式依赖数据库 |
1.2 核心结论
InnoDB 聚簇索引决定了主键必须是有序、短小的整型。UUID 和无序雪花 ID 直接做主键会导致频繁页分裂,写入性能急剧下降。
2. 数据库原生机制
2.1 MySQL — AUTO_INCREMENT
MySQL 通过 AUTO_INCREMENT 属性实现自增主键,是 InnoDB 下最常用的方案。
sql
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL
);
-- 查看当前自增值
SHOW TABLE STATUS LIKE 't_user';
-- 手动设置起始值
ALTER TABLE t_user AUTO_INCREMENT = 10000;
工作原理:
- InnoDB 为每张含有
AUTO_INCREMENT列的表维护一个计数器,存储在内存中 - MySQL 8.0 前,自增值持久化在 redo log 中(重启后可能回退);8.0+ 改为写入数据字典,重启不丢失
- 每次插入时自增值 +1,即使事务回滚,已分配的值也不会复用(存在空洞)
InnoDB 锁机制:
| 插入类型 | 锁策略 | 并发影响 |
|---|---|---|
| 简单 INSERT(已知行数) | 轻量级互斥锁,分配后立即释放 | 高并发下无瓶颈 |
| 批量 INSERT(未知行数) | AUTO-INC 表级锁,语句结束释放 |
并发性能差 |
innodb_autoinc_lock_mode = 2 |
全部使用轻量级锁(交错模式) | 性能最好,但 binlog 为 STATEMENT 时主从不安全 |
局限性:
- 分库分表后无法保证全局唯一
- 批量插入需要逐条获取 ID,JDBC
rewriteBatchedStatements无法优化 INSERT - 自增值在主从切换后可能冲突
2.2 PostgreSQL — SEQUENCE / SERIAL
PostgreSQL 使用 SEQUENCE(序列对象)生成自增值,SERIAL / BIGSERIAL 是语法糖。
sql
-- 方式一:SERIAL 语法糖(自动创建序列 + 设置默认值)
CREATE TABLE t_user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL
);
-- 等价于:
-- CREATE SEQUENCE t_user_id_seq;
-- id BIGINT PRIMARY KEY DEFAULT nextval('t_user_id_seq');
-- 方式二:手动创建序列(更灵活)
CREATE SEQUENCE t_user_seq START WITH 10000 INCREMENT BY 1;
CREATE TABLE t_user (
id BIGINT PRIMARY KEY DEFAULT nextval('t_user_seq'),
username VARCHAR(50) NOT NULL
);
-- 批量分配:预取一批 ID
SELECT nextval('t_user_seq') FROM generate_series(1, 100);
相比 MySQL AUTO_INCREMENT 的优势:
| 特性 | MySQL AUTO_INCREMENT | PostgreSQL SEQUENCE |
|---|---|---|
| 预分配 | 不支持 | ALTER SEQUENCE ... CACHE 1000 |
| 步长调整 | 不支持(固定+1) | INCREMENT BY N 自定义步长 |
| 起始值 | ALTER TABLE 修改 | START WITH N |
| 循环使用 | 不支持 | CYCLE / NO CYCLE |
| 批量获取 | 不支持 | nextval() + generate_series() |
| 与表耦合 | 是 | 否(独立对象,可多表共享) |
PostgreSQL 10+ 的 IDENTITY 列:
sql
-- 推荐写法(取代 SERIAL)
CREATE TABLE t_user (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(50) NOT NULL
);
-- 优势:防止手动插入覆盖序列值
-- GENERATED ALWAYS:禁止手动插入
-- GENERATED BY DEFAULT:允许手动插入时覆盖
2.3 Oracle — SEQUENCE
Oracle 的 SEQUENCE 是功能最丰富的实现,也是 Oracle 数据库主键生成的标准方案。
sql
-- 创建序列
CREATE SEQUENCE t_user_seq
START WITH 1
INCREMENT BY 1
CACHE 500 -- 预缓存500个值,减少递归调用
NOORDER -- 不保证顺序(RAC 环境下提升性能)
NOCYCLE;
-- 使用序列
INSERT INTO t_user (id, username) VALUES (t_user_seq.NEXTVAL, '张三');
-- 获取当前会话最后一次 NEXTVAL 的值
SELECT t_user_seq.CURRVAL FROM dual;
RAC 集群下的特殊处理:
| 参数 | 说明 | 性能影响 |
|---|---|---|
ORDER |
保证全局有序(跨节点) | 性能较低,需要跨节点协调 |
NOORDER |
各节点独立分配,不保证全局有序 | 性能高,推荐用于非业务排序场景 |
CACHE |
每个节点缓存一段值 | 值越大,性能越好,但重启浪费越多 |
2.4 三大数据库对比
| 特性 | MySQL | PostgreSQL | Oracle |
|---|---|---|---|
| 自增方式 | AUTO_INCREMENT |
SEQUENCE / SERIAL / IDENTITY |
SEQUENCE |
| 预分配缓存 | 不支持 | CACHE N |
CACHE N |
| 步长自定义 | 不支持 | INCREMENT BY N |
INCREMENT BY N |
| 批量获取 ID | 不支持 | generate_series + nextval |
不原生支持 |
| 集群支持 | 主从复制 | 流复制 / 逻辑复制 | RAC(ORDER/NOORDER) |
| 有序性保证 | 单实例严格递增 | 单实例严格递增 | ORDER 模式下全局有序 |
3. UUID
3.1 生成方式
java
// Java 原生 UUID(版本4,随机)
UUID uuid = UUID.randomUUID(); // 36字符:550e8400-e29b-41d4-a716-446655440000
String id = uuid.toString().replace("-", ""); // 32字符:550e8400e29b41d4a716446655440000
// MySQL 内置 UUID
SELECT UUID(); -- 550e8400-e29b-41d4-a716-446655440000
-- MySQL 8.0+:有序 UUID(替换时间戳字节顺序)
SELECT UUID_TO_BIN(UUID(), TRUE); -- 16字节二进制,有序
3.2 UUID v7(2023年 RFC 9562)
UUID v7 解决了 UUID v4 无序的问题,以毫秒级时间戳开头,趋势递增:
UUID v7 结构(128位):
| 48位毫秒时间戳 | 4位版本(0111) | 12位随机 | 2位变体 | 62位随机 |
sql
-- PostgreSQL 17+ 原生支持
SELECT gen_random_uuid(); -- UUID v4
-- UUID v7 需扩展或自定义函数
-- MySQL 8.0 中生成有序 UUID
SELECT UUID_TO_BIN(UUID(), TRUE) AS ordered_uuid;
3.3 为什么 UUID 不适合做 InnoDB 主键
问题链:
UUID 无序 → 每次插入落到 B+ 树随机位置
→ 频繁页分裂(Page Split)
→ 大量随机 I/O
→ 写入性能严重下降
此外:
UUID 36字符 → 二级索引叶子节点存储主键值
→ 索引膨胀(36字节 vs BIGINT 8字节)
→ 内存利用率低,磁盘占用大
性能对比数据(参考 Percona 基准测试):
| 主键类型 | 插入 QPS | 表大小 | 二级索引大小 |
|---|---|---|---|
| BIGINT AUTO_INCREMENT | ~25,000 | 100MB | 50MB |
| BINARY(16) UUID | ~12,000 | 120MB | 80MB |
| CHAR(36) UUID | ~8,000 | 160MB | 120MB |
4. 雪花算法(Snowflake)
4.1 结构设计
Snowflake ID 结构(64位 = 8字节 = Java long):
0 | 00000000 00000000 00000000 00000000 00000000 0 | 00000 00000 | 000000000000
│ │ 41位时间戳 │ 10位机器ID │ 12位序列号 │
符号位 │(5位数据中心+5位机器)│
- 每毫秒可生成 4096 个 ID(单节点)
- 可用 69 年(从纪元开始计算)
- 支持 1024 个节点(32 个数据中心 × 32 台机器)
4.2 Java 实现
java
public class SnowflakeIdGenerator {
private final long epoch = 1609459200000L; // 2021-01-01 起始纪元
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits); // 31
private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 31
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampShift = sequenceBits + workerIdBits
+ datacenterIdBits; // 22
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id error");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenter Id error");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨检测
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}
// 同一毫秒内,序列号自增
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
4.3 时钟回拨问题
核心风险:NTP 时间同步可能导致系统时钟回退,产生重复 ID。
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 抛异常 | 回拨超过阈值直接报错 | 对唯一性要求极高 |
| 等待追上 | 小幅回拨(<50ms)时自旋等待 | 生产环境推荐 |
| 备用 workerId | 回拨时切换到预分配的备用机器 ID | 高可用场景 |
| 百度 UidGenerator | 改用未来时间填补,环形缓冲区 | 时钟回拨频繁的环境 |
4.4 雪花 ID 与数据库主键
关键问题:雪花 ID 做主键好吗?
雪花 ID 是趋势递增的(同一毫秒内严格递增,跨毫秒也递增),相比 UUID 好很多,但不如纯自增 ID 严格有序。对 InnoDB 聚簇索引来说:
- 可以接受:趋势递增,页分裂远少于 UUID
- 非最优:不如纯自增 ID 的顺序写入效率
- 折中方案:主键用
BIGINT AUTO_INCREMENT,雪花 ID 存业务字段(如order_no)
5. 号段模式
5.1 核心思想
从数据库批量预分配一段 ID,应用在内存中消费,用完再取下一段。
号段模式工作流:
应用启动
↓
从数据库获取号段 [1, 1000](step=1000)
↓
内存中分配:1, 2, 3, ..., 999
↓
消耗到 10% 时,异步预加载下一段 [1001, 2000]
↓
当前号段用完 → 无缝切换到备用号段
5.2 号段表设计
sql
CREATE TABLE id_segment (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
biz_tag VARCHAR(64) NOT NULL COMMENT '业务标识(如 order、user)',
max_id BIGINT NOT NULL DEFAULT 1 COMMENT '当前最大已分配 ID',
step INT NOT NULL COMMENT '步长(每次预分配的 ID 数量)',
version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
description VARCHAR(256) COMMENT '描述',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_biz_tag (biz_tag)
) ENGINE=InnoDB;
sql
-- 获取号段(乐观锁 + CAS 更新)
UPDATE id_segment
SET max_id = max_id + step,
version = version + 1
WHERE biz_tag = 'order' AND version = #{currentVersion};
-- 获取更新后的 max_id
SELECT max_id, step FROM id_segment WHERE biz_tag = 'order';
-- 号段范围:[max_id - step + 1, max_id]
5.3 双 Buffer 优化
┌──────────────┐ ┌──────────────┐
│ 当前号段 │ │ 备用号段 │
│ [1, 1000] │ │ [1001, 2000] │
│ 消耗中... │ │ 已预加载 ✓ │
└──────────────┘ └──────────────┘
当前号段消耗到 10% → 触发异步加载备用号段
当前号段用完 → 原子切换到备用号段 → 旧备用变为当前,开始加载新备用
优势:数据库短暂不可用时,应用仍可从缓存号段中分配 ID,提供缓冲期。
6. TSID(Time-Sorted ID)
6.1 什么是 TSID
TSID(Time-Sorted ID)是一种 64 位时间排序的唯一标识符,由 Fabio Lima 设计,Vlad Mihalcea 提供 Hibernate 集成。它结合了雪花算法的紧凑性和 ULID 的简洁性,是 Snowflake 的现代演进方案。
与雪花算法的核心区别:
| 维度 | 雪花算法 | TSID |
|---|---|---|
| 机器 ID | 必须预分配 workerId / datacenterId | 可随机生成,无需协调 |
| 字符串表示 | 纯数字(如 1541815603606036480) |
Crockford Base32 编码,13 字符(如 0HJBZJM5V3YC8) |
| 单调性 | 同毫秒内序列号递增 | 同毫秒内计数器递增,可跨越毫秒保持单调 |
| 时钟回拨 | 直接抛异常 | 允许时间组件领先系统时钟 1ms+ 来维持单调性 |
| 数据库存储 | BIGINT |
BIGINT(一致) |
6.2 结构设计
TSID 结构(64位 = Java long):
adjustable
<---------->
----------┼----------┼----------┼----------
time (42 bits) node counter
(10 bits) (12 bits)
- time:毫秒时间戳,自 2020-01-01 起算
42 位 → 有符号 long 可用 ~69 年,无符号 ~139 年
- node:节点标识,0~20 位可调(默认 10 位,支持 1024 节点)
- counter:同一毫秒内的计数器,初始值随机
node bits + counter bits = 22 位(固定)
→ node=10 时 counter=12,每毫秒 4096 个 ID
→ node=0 时 counter=22,每毫秒 419 万个 ID(单机模式)
6.3 Java 使用
依赖(两个主流实现任选其一):
xml
<!-- 方式一:Hypersistence TSID(Vlad Mihalcea,提供 Hibernate 集成) -->
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-tsid</artifactId>
<version>2.1.3</version>
</dependency>
<!-- 方式二:TSID Creator(Fabio Lima,原版实现) -->
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>tsid-creator</artifactId>
<version>5.2.6</version>
</dependency>
基础用法:
java
import io.hypersistence.tsid.Tsid;
// 快速生成(使用静态工厂)
Tsid tsid = Tsid.fast();
// 获取数值(存入数据库 BIGINT)
long id = tsid.toLong(); // 如 809100737063473402
// 获取字符串(13字符,Crockford Base32)
String str = tsid.toString(); // 如 "0PEM0TNJ2SM7T"
// 从 ID 反推创建时间
Instant createdAt = tsid.getInstant();
自定义工厂配置:
java
// 场景一:指定节点 ID(分布式环境多节点)
int node = 256; // 最大值:2^nodeBits - 1
Tsid.Factory factory = Tsid.Factory.builder()
.withNodeBits(10) // 节点位:0~20 可调
.withNode(node)
.build();
// 场景二:单机模式(无需节点 ID,最大化每毫秒 ID 数)
Tsid.Factory factory = Tsid.Factory.builder()
.withNodeBits(0) // 无节点位,counter 占 22 位
.build(); // 每毫秒可生成 419 万个 ID
// 场景三:自定义纪元
Instant customEpoch = Instant.parse("2000-01-01T00:00:00Z");
Tsid.Factory factory = Tsid.Factory.builder()
.withCustomEpoch(customEpoch)
.build();
// 场景四:模拟 Twitter Snowflake 结构
int datacenter = 1, worker = 1;
int snowflakeNode = (datacenter << 5) | worker;
Tsid.Factory factory = Tsid.Factory.builder()
.withCustomEpoch(Instant.ofEpochMilli(1288834974657L))
.withNode(snowflakeNode)
.withRandomFunction(x -> new byte[x]) // 计数器从 0 开始
.build();
6.4 JPA / Hibernate 集成
方式一:@PrePersist 回调(最简单)
java
@Entity
@Table(name = "t_order")
public class Order {
@Id
private Long id;
private String orderNo;
@PrePersist
public void prePersist() {
if (id == null) {
id = Tsid.fast().toLong();
}
}
// 从 ID 反推创建时间(无需额外的 create_time 字段)
public Instant getCreatedAt() {
return Tsid.from(id).getInstant();
}
}
方式二:自定义 Hibernate IdentifierGenerator(更优雅)
java
import io.hypersistence.tsid.Tsid;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
public class TsidGenerator implements IdentifierGenerator {
@Override
public Long generate(SharedSessionContractImplementor session, Object object) {
return Tsid.fast().toLong();
}
}
// 实体中使用
@Entity
@Table(name = "t_order")
public class Order {
@Id
@GeneratedValue(generator = "tsid-generator")
@GenericGenerator(name = "tsid-generator", strategy = "com.example.TsidGenerator")
private Long id;
}
方式三:Spring Data JPA + BeforeConvertCallback(Spring Boot 3+)
java
@Component
public class TsidIdListener {
@BeforeConvert
public void assignId(Object entity) {
if (entity instanceof BaseEntity base && base.getId() == null) {
base.setId(Tsid.fast().toLong());
}
}
}
6.5 TSID 字符串格式的优势
连续生成的 TSID 字符串序列:
01226N0640J7K
01226N0640J7M
01226N0640J7N
01226N0640J7P
01226N0640J7Q
01226N0640J7R
01226N0640J7S
01226N0640J7T
01226N0693HDA ← 毫秒切换
01226N0693HDB
01226N0693HDC
...
^^ ^ ^
│ │ │
│ │ └── 随机部分(node + counter)
│ └── 时间戳部分(毫秒)
└── 时间戳高位
特点:
- 字符串天然按时间排序(字典序 = 时间序)
- 仅 13 字符(UUID v4 需要 36 字符)
- 可安全用于 URL、日志、前端展示
- 兼容 JavaScript(Number 精度丢失问题 → 使用字符串)
7. 其他方案
7.1 Redis INCR
bash
# 原子自增
INCR order:id # 返回 1, 2, 3, ...
INCRBY order:id 1000 # 一次分配 1000 个号段
# 带过期时间(按天重置)
SET order:20260525:id 0 EX 86400
INCR order:20260525:id
| 优点 | 缺点 |
|---|---|
| 原子操作,高性能 | 依赖 Redis,Redis 故障则不可用 |
| 灵活控制步长 | 持久化配置不当可能丢失(RDB/AOF) |
| 天然支持号段预分配 | 需要额外基础设施 |
7.2 数据库集群步长偏移
sql
-- 两个数据库实例,步长为 2
-- 数据库 A:起始值 1,步长 2 → 生成 1, 3, 5, 7, ...
SET @@auto_increment_increment = 2;
SET @@auto_increment_offset = 1;
-- 数据库 B:起始值 2,步长 2 → 生成 2, 4, 6, 8, ...
SET @@auto_increment_increment = 2;
SET @@auto_increment_offset = 2;
局限:扩容困难(需要修改步长和起始值),ID 不连续。
8. ORM 框架集成
8.1 JPA / Hibernate 主键策略
JPA 通过 @GeneratedValue 注解定义主键生成策略,支持四种标准策略加 Hibernate 扩展:
java
// 策略一:IDENTITY — 使用数据库自增(MySQL AUTO_INCREMENT, PostgreSQL SERIAL)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 策略二:SEQUENCE — 使用数据库序列(Oracle, PostgreSQL)
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq",
sequenceName = "t_user_seq",
allocationSize = 1, // 每次 nextval 调用分配的 ID 数
initialValue = 1)
private Long id;
// 策略三:TABLE — 用独立表模拟序列(跨数据库兼容,性能差)
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "user_table_gen")
@TableGenerator(name = "user_table_gen",
table = "id_generator",
pkColumnName = "gen_name",
pkColumnValue = "user_id",
valueColumnName = "gen_value",
allocationSize = 50)
private Long id;
// 策略四:UUID(Hibernate 扩展,JPA 标准不内置)
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(columnDefinition = "VARCHAR(36)")
private String id;
allocationSize 与性能:
| allocationSize | 行为 | 性能影响 |
|---|---|---|
| 1(默认) | 每次插入都调用 nextval |
高频数据库访问 |
| 50 | 预分配 50 个 ID 到内存 | 减少数据库访问,推荐设置 |
重要:
allocationSize必须与数据库序列的INCREMENT BY值一致,否则会导致主键冲突。
java
// 正确的配合使用
// 数据库端:CREATE SEQUENCE t_user_seq INCREMENT BY 50;
// JPA 端:
@SequenceGenerator(name = "user_seq",
sequenceName = "t_user_seq",
allocationSize = 50) // 与 INCREMENT BY 一致
JPA 主键策略对比:
| 策略 | 数据库支持 | 批量插入 | 性能 | 分布式 |
|---|---|---|---|---|
| IDENTITY | MySQL, PostgreSQL | 不支持(需逐条获取) | 一般 | 不支持 |
| SEQUENCE | Oracle, PostgreSQL | 支持(预分配) | 好 | 不支持 |
| TABLE | 所有 | 支持 | 较差(锁竞争) | 不支持 |
| UUID | 所有 | 支持 | 较差(字符串索引) | 支持 |
IDENTITY 与批量插入的矛盾:Hibernate 使用
IDENTITY策略时,无法使用 JDBC 批量插入。因为 INSERT 后必须立即获取生成的 ID 才能管理实体状态,导致每条 INSERT 单独执行。这是选择 SEQUENCE 的重要理由。
8.2 MyBatis-Plus 主键策略
MyBatis-Plus 通过 @TableId 注解和全局配置控制主键生成:
java
@Data
@TableName("t_user")
public class User {
// 雪花算法(默认策略)
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
}
IdType 枚举:
| IdType | 生成方式 | 说明 |
|---|---|---|
AUTO |
数据库自增 | 对应 MySQL AUTO_INCREMENT |
ASSIGN_ID |
雪花算法(默认) | 分布式唯一 ID,64位 long |
ASSIGN_UUID |
UUID | 去掉横线的 32 位 UUID |
INPUT |
手动输入 | 需要在插入前手动设置 ID |
NONE |
跟随全局配置 | 由 idType 全局设置决定 |
yaml
# application.yml 全局配置
mybatis-plus:
global-config:
db-config:
id-type: assign_id # 全局主键策略(默认雪花算法)
8.3 框架选型建议
| 场景 | 推荐方案 | ORM 配置 |
|---|---|---|
| 单库 MySQL + JPA | GenerationType.IDENTITY |
@GeneratedValue(strategy = IDENTITY) |
| 单库 Oracle/PG + JPA | GenerationType.SEQUENCE + allocationSize |
@SequenceGenerator(allocationSize = 50) |
| 分布式 + MyBatis-Plus | 雪花算法(默认) | IdType.ASSIGN_ID |
| 分布式 + JPA | TSID 自定义生成器 | @GenericGenerator + TsidGenerator(见第 6 章) |
| 订单号等业务 ID | 号段模式(Leaf) | 应用层独立服务 |
9. 方案总览与选型决策
9.1 全方案对比
| 方案 | 有序性 | 全局唯一 | 性能 | 存储效率 | 批量插入 | 依赖 | 适用场景 |
|---|---|---|---|---|---|---|---|
| AUTO_INCREMENT | 严格递增 | 单库唯一 | 高 | 8 字节 | 差 | 无 | 单库单表 |
| SEQUENCE | 严格递增 | 单库唯一 | 高(可预分配) | 8 字节 | 好 | 无 | Oracle/PG 单库 |
| UUID v4 | 无序 | 全局唯一 | 高 | 36 字节 | 好 | 无 | 临时 ID、非索引字段 |
| UUID v7 | 趋势递增 | 全局唯一 | 高 | 36 字节 | 好 | 无 | 兼容已有 UUID 方案 |
| 雪花算法 | 趋势递增 | 全局唯一 | 极高 | 8 字节 | 好 | 时钟 | 分布式系统经典方案 |
| TSID | 趋势递增 | 全局唯一 | 极高 | 8 字节 / 13字符 | 好 | 时钟 | 雪花算法的现代替代 |
| 号段模式 | 严格递增 | 全局唯一 | 高 | 8 字节 | 好 | 数据库 | 电商订单、高可用要求 |
| Redis INCR | 严格递增 | 全局唯一 | 高 | 8 字节 | 好 | Redis | 已有 Redis 基础设施 |
9.2 选型决策树
开始
↓
是否分库分表 / 微服务?
├── 否 → 单库自增(MySQL AUTO_INCREMENT / PG SEQUENCE)
└── 是
↓
是否需要严格递增?
├── 否 → TSID(推荐)/ 雪花算法(无需协调、高性能)
└── 是
↓
是否对高可用有极高要求?
├── 否 → Redis INCR
└── 是 → 号段模式(Leaf-segment)
9.3 主键 vs 业务 ID 分离
生产环境中常见的最佳实践:主键与业务 ID 分离。
t_order 表:
id BIGINT AUTO_INCREMENT PRIMARY KEY -- 聚簇索引主键,有序写入
order_no BIGINT NOT NULL UNIQUE -- 雪花算法生成的业务订单号
...
好处:
1. 主键有序 → InnoDB 写入性能最优
2. 业务 ID 全局唯一 → 分库分表、跨服务引用时使用
3. 主键短小 → 二级索引紧凑,内存利用率高

