OceanBase 自增主键模式说明
适用范围:OceanBase MySQL 兼容模式。
AUTO_INCREMENT_MODE的支持情况、默认值及具体行为应以目标版本为准。本文示例未连接实际实例执行,不应直接套用于原生 MySQL。
一、先区分四个概念
| 概念 | 作用 | 不保证什么 |
|---|---|---|
AUTO_INCREMENT |
在插入时自动生成 ID | 不保证编号连续 |
PRIMARY KEY (id) |
强制 ID 非空且全表唯一 | 不表示 ID 就是创建时间或提交时间 |
AUTO_INCREMENT_MODE |
控制自增值生成的有序性模式 | 不改变查询的排序规则 |
PARTITION BY HASH (id) |
根据 ID 的 HASH 规则决定数据属于哪个分区 | 不按连续 ID 范围分区 |
text
应用插入订单,不传 id
|
v
自增机制分配 id
|
v
HASH 分区规则确定目标分区
|
v
数据库写入数据并检查主键唯一性
这是概念上的职责划分,不代表数据库内部严格的执行步骤。
二、自增 ID 与并发插入
定义自增列后,应用可以不提供 ID:
sql
INSERT INTO orders (user_id, amount)
VALUES (1001, 99.90);
并发插入时,数据库负责协调生成自增值,应用不需要使用 SELECT MAX(id) + 1 计算 ID。后者存在并发竞争,不应当作 ID 生成方案。
PRIMARY KEY (id) 是唯一性约束:即使应用手动提供 ID,数据库也不会允许相同 ID 的两条记录都成功提交。手动插入已有 ID 的普通 INSERT 会产生重复键错误。
为什么自增值不保证连续?
表中的 ID 可能是:
text
101、102、105、108......
缺号不一定表示数据丢失。常见原因包括:
- 事务分配 ID 后回滚,已分配的值通常不会回收。
- 某些插入执行路径先分配 ID,随后因其他原因失败。
- 已有记录被删除,空缺不会自动补回。
- 预分配或缓存的自增值未用完,可能因重启等情况留下空缺,具体取决于版本和模式。
text
事务 A:获得 101 -> 提交成功
事务 B:获得 102 -> 回滚
事务 C:获得 103 -> 提交成功
表中最终保留:101、103
因此不能使用 MAX(id) 计算订单总数,也不能仅凭缺号判断是否丢单。
三、AUTO_INCREMENT_MODE 的两种模式
3.1 ORDER:有序自增
ORDER 关注自动生成值的全局有序性。需要区分自增值的分配顺序、请求到达顺序和事务提交顺序,它们并不是同一件事。
text
事务 A:先获得 ID 101,执行较慢
事务 B:后获得 ID 102,先提交
事务 A:随后提交
分配顺序:101 -> 102
提交顺序:B -> A
即使选择 ORDER:
- ID 也不保证连续。
- ID 大小也不代表事务提交先后。
- ID 顺序也不能严格代替业务创建时间顺序。
- 全局有序生成需要协调,应结合实际负载评估性能成本。
3.2 NOORDER:无序自增
NOORDER 不要求跨节点生成的自增值按分配时间全局递增,适用于更关注分布式生成效率的场景。
可以通过号段缓存理解可能出现的现象:
text
分配端 A 缓存较小的一段 ID
分配端 B 缓存较大的一段 ID
B 先生成一个 ID:10001
A 后生成一个 ID:101
这里的数字和缓存分配端只是示意,不代表固定号段大小,也不代表每个分区都固定拥有一个号段。具体缓存粒度和分配实现以版本为准。
无序表示生成顺序不要求全局递增,不表示允许主键重复,也不表示查询结果无法排序。
3.3 模式对比
| 比较项 | ORDER | NOORDER |
|---|---|---|
| 自动生成值的全局有序性 | 关注全局有序生成 | 不要求全局有序生成 |
| 是否保证无跳号 | 否 | 否 |
| 是否保证按事务提交顺序编号 | 否 | 否 |
PRIMARY KEY (id) 是否约束 ID 唯一 |
是 | 是 |
是否影响 ORDER BY id 的正确性 |
否 | 否 |
| 选择侧重点 | 有序生成需求 | 分布式生成效率 |
不要把某种模式当作所有版本的默认值。未显式指定时,应根据实际版本和表定义确认生效模式。
四、建表 DDL
以下两种方案二选一,不要在同一数据库中重复创建同名表。
4.1 ORDER 模式
sql
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(18, 2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_created_at (created_at)
)
AUTO_INCREMENT_MODE = 'ORDER'
PARTITION BY HASH (id)
PARTITIONS 8;
4.2 NOORDER 模式
sql
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(18, 2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_created_at (created_at)
)
AUTO_INCREMENT_MODE = 'NOORDER'
PARTITION BY HASH (id)
PARTITIONS 8;
两个示例都是 id 单列自增主键,按 ID 做 HASH 分区。8 只是示例分区数,应结合数据量和租户资源确定。不需要分区时,可以移除末尾的 PARTITION BY HASH (id) PARTITIONS 8,保留语句结尾分号。
4.3 为什么不继续按 created_at 分区?
前面的按月分区设计采用:
sql
PRIMARY KEY (id, created_at)
该主键只保证组合唯一,不单独保证 id 唯一。在这里讨论的 OceanBase MySQL 分区表规则下,主键需要包含分区键。因此,要求 PRIMARY KEY (id) 时,应将分区键改为 id,或使用非分区表。
如果业务必须按创建时间分区,又必须由数据库约束 ID 全表唯一,应结合目标版本评估"包含分区键的联合主键 + ID 全局唯一索引"。全局唯一索引不等于 ID 单列主键。
五、号段分配不等于按号段分区
这两个问题需要分开理解:
| 问题 | 对应机制 |
|---|---|
| 下一个 ID 如何产生? | 自增模式及其分配、缓存机制 |
| 这个 ID 对应的记录存在哪个分区? | 表的分区规则 |
当前 HASH 分区不表示:
text
分区 1:ID 1~10000
分区 2:ID 10001~20000
分区 3:ID 20001~30000
按连续 ID 范围划分属于 RANGE 分区。内部即使存在自增号段缓存,也不会把 HASH 分区自动变成 RANGE 分区。
六、跨分区排序
6.1 按 ID 排序没有正确性问题
sql
SELECT *
FROM orders
ORDER BY id DESC
LIMIT 20;
该查询返回整个查询范围中 ID 最大的 20 条记录,不是每个分区分别返回一组未合并的结果。
text
多个分区的数据
|
v
数据库全局排序或有序归并
|
v
返回 ID 最大的 20 条
实际算法由执行计划决定。ID 跳号及 NOORDER 生成模式都不影响排序正确性。不写 ORDER BY,则不能依赖默认返回顺序。
6.2 按创建时间展示订单
"ID 最大的订单"不一定等于"最新创建的订单"。需要按创建时间展示时使用:
sql
SELECT *
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;
id 用于在创建时间相同时提供确定的排序顺序。当前 DATETIME 没有小数秒精度,同一秒创建多条订单很正常;即使改成 DATETIME(6),时间戳也不能保证唯一。
6.3 跨分区查询的成本
HASH 分区不按 ID 连续范围排列。全表按 ID 取前 20 条通常需要访问多个分区,再合并结果,但不一定需要读取所有记录。
id = 12345 可以据分区键定位分区;id < 9801 或仅按时间过滤,通常不能据此裁剪 ID 的 HASH 分区。实际性能应通过执行计划和数据规模验证。
七、按 ID 游标分页
sql
-- 第一页
SELECT *
FROM orders
ORDER BY id DESC
LIMIT 20;
-- 下一页,9801 为上一页最后一条记录的 ID
SELECT *
FROM orders
WHERE id < 9801
ORDER BY id DESC
LIMIT 20;
该方式无需 ID 连续,也能避免大偏移量 OFFSET 的开销。但它不自动提供跨多次查询的一致快照:
- 并发写入、删除或晚提交可能改变后续页看到的数据。
NOORDER下,后生成的 ID 可能小于已有 ID,不能把 ID 游标直接当作完整的增量变更水位。- 即使使用
ORDER,较小 ID 的事务晚提交,也可能让按id > 上次最大值轮询的程序漏读。
需要可靠地捕获所有变更时,应评估 CDC 或具有明确一致性保证的同步方案,而不是仅依赖自增 ID。
八、选型与验证
| 业务需求 | 建议 |
|---|---|
| ID 只作为唯一标识,侧重分布式生成效率 | 评估 NOORDER |
| 需要自动生成值全局有序 | 评估 ORDER,并测试性能 |
| 展示最新创建的订单 | 使用创建时间排序,ID 作为次级排序键 |
| 编号严格连续、不允许跳号 | 另行设计业务编号机制,两种模式都不能直接满足 |
| 防止同一订单请求重试后重复创建 | 使用稳定的业务唯一键或幂等机制,自增主键本身不能识别重复请求 |
先检查版本,再检查实际表定义:
sql
SELECT VERSION();
SHOW CREATE TABLE orders;
如果表定义未显示模式,不要仅凭这一点推断默认值,应查阅对应版本文档。若建表提示不支持 AUTO_INCREMENT_MODE,需要核对版本和兼容模式,而不是假定设置已经生效。
九、总结
自增负责生成 ID,主键负责约束唯一,分区负责组织数据,ORDER BY 负责查询排序。
ORDER与NOORDER区分的是生成有序性,不是查询能否排序。- 两种模式都不保证 ID 连续,也不保证 ID 顺序等于事务提交顺序。
- HASH 分区和自增号段是独立的概念。
- 跨分区按 ID 排序可以得到正确的全局结果,但需要评估执行成本。
- 默认模式及可用语法必须结合目标 OceanBase 版本确认。