CMS 迁移后出现自增 ID 冲突,根因只有一个:目标表的 AUTO_INCREMENT 计数器小于表里实际存在的最大 ID,新写入拿到的 ID 撞上了老数据。处理顺序是:先停写并把表锁成只读,再执行 ALTER TABLE 表名 AUTO_INCREMENT = 最大值+1 把计数器推上去,最后逐条核对关联表和唯一索引。多来源库合并的情况不要靠改计数器,直接用 ID 段位偏移。
为什么迁移完就报 Duplicate entry?
结论:冲突来自「显式插入了 id 值,但自增计数器没跟着涨」。典型报错是 ERROR 1062 (23000): Duplicate entry '1024' for key 'PRIMARY'。
迁移时如果用 INSERT INTO ... (id, ...) VALUES (...) 显式带上原库 ID,InnoDB 不会把计数器提升到「已插入最大值 + 1」以上——尤其 MySQL 5.7 及更早版本,自增计数器只存在内存里,实例重启后按 MAX(id)+1 重算,中间删过数据就会把计数器算小。MySQL 8.0 起自增计数器写入 redo log 持久化,重启不再回退,但显式插入的边界情况依然存在。
先确认现状,两条语句对比着看:
SELECT AUTO_INCREMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA='your_db' AND TABLE_NAME='your_table';
SELECT MAX(id) FROM your_table;
前者小于后者,就是本次冲突的直接证据。
单库迁移的计数器怎么修?
结论:一条 ALTER TABLE 就能解决,但必须先停写。
-- 1. 应用侧切只读或停写
-- 2. 校正计数器(MySQL 会自动取 MAX(id)+1,填小于该值的数字不生效)
ALTER TABLE your_table AUTO_INCREMENT = 1;
-- 3. 校验
SELECT AUTO_INCREMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA='your_db' AND TABLE_NAME='your_table';
注意点三条:其一,AUTO_INCREMENT 参数设成比当前最大值小的值会被静默忽略并取 MAX(id)+1,所以填 1 是安全的兜底写法;其二,MyISAM 表执行该语句会重建整张表,大表务必挑低峰期;其三,校正期间只要有新行走 INSERT,计数器又会基于旧值继续,所以停写是前置条件,不是可选项。
多来源库合并,ID 段位偏移怎么做?
结论:给每个来源库分配互不重叠的 ID 段位,一次性批量偏移,绝不允许两库 ID 区间交叉。
按百万级留余量,A 库保持原样,B 库整体 +1000000,C 库 +2000000:
SET FOREIGN_KEY_CHECKS = 0;
START TRANSACTION;
UPDATE b_article SET id = id + 1000000;
UPDATE b_comment SET id = id + 1000000,
article_id = article_id + 1000000;
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
关键在所有引用该 ID 的外键列与冗余列都要同步偏移,漏一列就产生孤儿数据。偏移前后各跑一次孤儿检查:
SELECT c.id FROM b_comment c
LEFT JOIN b_article a ON c.article_id = a.id
WHERE a.id IS NULL;
更稳妥的做法是建映射表:把 old_id → new_id 落成一张临时表,再按映射回填关联列。映射表的额外好处是迁移出错时能反向追溯,改错也能重放。
迁移前后必须核对哪几项?
结论:核对四项——计数器、外键约束、唯一索引、孤儿记录,缺一项就会在上线后炸。
- 计数器:
information_schema.TABLES 的 AUTO_INCREMENT 必须大于 MAX(id)。
- 外键:
SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_SCHEMA='your_db'; 列出全部关联列,逐列确认偏移已覆盖。
- 唯一索引:除主键外,
slug、username 这类唯一索引在合并时同样会撞,用 SELECT col, COUNT(*) FROM t GROUP BY col HAVING COUNT(*)>1; 全表扫一遍。
- 孤儿记录:上面那类
LEFT JOIN ... IS NULL 查询,每个关联表跑一次,结果必须为 0 行。
导入阶段常规操作是 mysqldump --single-transaction --no-create-info 导数据、SET FOREIGN_KEY_CHECKS=0 关闭校验以提速,但导入结束后必须把 FOREIGN_KEY_CHECKS 恢复成 1,否则后续写入会绕过约束而不报错。
怎么从根上不再撞 ID?
结论:把自增 ID 降级为纯内部主键,对外标识换成不依赖数据库分配的 ID。
具体三条:对外 URL、API 返回、导出文件里不再暴露自增 ID,改用 UUID v4 或雪花 ID(Snowflake,64 位、含时间戳与机器位,单机每毫秒可生成 4096 个不重复 ID);多实例写入场景配置 auto_increment_increment(步长)与 auto_increment_offset(起始偏移),例如双主写设 increment=2、offset 分别为 1 和 2;迁移脚本里显式插入 ID 之后,紧跟一条 ALTER TABLE ... AUTO_INCREMENT 校正,把这一步固化进迁移 SOP。
这套流程在 GJ 站长论坛的站长交流版块里也有不少实操讨论,遇到具体报错可以带表结构去发帖,比对着通用文档猜要快得多。收个尾:冲突只是计数器没跟上,先停写、再校正、多源就偏移,核对四项再上线,最后把自增 ID 从对外接口里摘出去,这事儿才算真正闭环。