原创

MySQL 重复键与自增空号:编号分配不等于事务提交

导入任务报了一次重复键,下一条成功记录的自增主键却跳过了一个号码。日志里找不到对应数据,也没有谁执行 DELETE。这个空洞很可能在编号分配阶段就已经产生,不代表数据库丢了一条已提交记录。

MySQL 8.4 的 InnoDB 不承诺 AUTO_INCREMENT 连续。分配编号与最终提交是两个阶段,唯一键冲突、事务回滚以及某些批量插入的预留,都可能让号码不再出现在表里。把自增主键当作成功业务流水,会从这个地方开始算错。

从业务唯一键冲突开始复现

在独立库执行,不要对现有表重设自增值来制造实验条件:

CREATE DATABASE auto_gap_lab;
USE auto_gap_lab;
CREATE TABLE import_record (
    id BIGINT NOT NULL AUTO_INCREMENT,
    request_no VARCHAR(40) NOT NULL,
    payload VARCHAR(100) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_request_no (request_no)
) ENGINE=InnoDB;

SELECT @@innodb_autoinc_lock_mode,
       @@auto_increment_increment,
       @@auto_increment_offset;
INSERT INTO import_record(request_no, payload) VALUES ('req-a', 'first');

-- 预期失败:业务唯一键重复。
INSERT INTO import_record(request_no, payload) VALUES ('req-a', 'duplicate');

INSERT INTO import_record(request_no, payload) VALUES ('req-b', 'second');
SELECT id, request_no FROM import_record ORDER BY id;

默认步长与偏移的隔离实例中,可以观察成功记录之间是否出现空号。具体编号要看实际配置和执行路径,不应把某个示例里的固定数字写成所有失败语句都满足的公式。

原因是一个 INSERT 可能先获得自增编号,再在检查其他唯一键时失败。数据行没有成功落下,已经分配的编号却不必归还。反过来,如果语句显式提供主键,失败路径不一定发生同样的自动分配,因此“每次重复键都必定跳一号”也不准确。

回滚数据不会把取过的编号倒回去

START TRANSACTION;
INSERT INTO import_record(request_no, payload)
VALUES ('req-rollback', 'temporary');
SELECT LAST_INSERT_ID() AS allocated_id;
ROLLBACK;

INSERT INTO import_record(request_no, payload)
VALUES ('req-after', 'committed');
SELECT id, request_no FROM import_record ORDER BY id;

req-rollback 不应留在表中,分配给它的自增值却可能形成永久空洞。这样设计允许并发会话取号,不必为了追回一个号码去等待其他事务全部结束。

把锁模式换成传统模式,也不能由此获得事务级连续编号保证。MySQL 对各个自增锁模式都允许回滚后留下空号。锁模式主要影响分配时的并发与语句层面的编号行为,不会把取号改造成能随业务事务回收的凭证系统。

多行插入还存在另一个层次:某些路径会预留一批号码,最终未全部使用;并发插入和自增步长也可能让一批成功数据看起来不连续。业务若需要逐项对应导入结果,应保存请求标识与实际生成键,不能用第一条编号加数组下标推算。

自增值也不等于提交顺序

会话 A 先分配到较小的编号,事务却很久以后才提交;B 后取号并先提交。按主键增量扫描的消费程序如果把最大已见编号当成“更小编号都已完成”,就可能错过 A 稍后提交的记录。

这不是主键异常,而是读取协议把分配顺序误当提交顺序。同步任务需要明确处理未提交窗口、重复读取和断点推进,或者使用适合捕获提交变更的日志机制。单纯在轮询里加一个很大的延迟,只是把漏读风险藏进一个经验数字。

同样,MAX(id) 不是记录数量,MAX(id)-MIN(id)+1 也不是导入成功数。用 COUNT 核对当前记录,用明确状态或导入明细核对每个请求;两类统计有各自的含义,不要拿主键区间充当账本。

不要为了好看去修补空洞

把 AUTO_INCREMENT 调回较小值,不能安全追回并发环境已经分配过的号码。手工重排主键则可能破坏外键、缓存键、外部链接与日志关联;修正一列显示效果,却改变了对象身份。

删除记录造成的空号也应保留其业务历史。若凭证需要连续编号,通常应另设可追溯的业务号和作废规则,让每一个已发号码都有状态,而不是要求物理表里每个整数都必须对应一条有效记录。这种设计需要明确的串行范围和事务代价,不能用“让自增不跳号”一句配置替代。

排查异常跳号时仍要核对实际环境:近期是否发生批量导入、重复重试、回滚、步长调整或显式插入大主键。跳号本身不证明数据丢失,也不应成为停止调查的理由。真正需要追踪的是某个业务请求是否成功、它的唯一标识是什么、提交后能否被核验。

实验结束:

DROP DATABASE auto_gap_lab;
正文到此结束
评论插件初始化中...
Loading...
本文目录