三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MySQL到金仓:自增主键与序列改造全流程——AUTO_INCREMENT兼容、并发写入与回退实战

MySQL到金仓:自增主键与序列改造全流程——AUTO_INCREMENT兼容、并发写入与回退实战

文章目录

    • 每日一句正能量
    • 1. 背景与问题:AUTO_INCREMENT迁移的核心,是把“下一个ID由谁生成”迁过去
    • 2. 环境与数据:先判断是“简单自增”还是“分布式编号”
      • 2.1 为什么 `auto_increment_increment/offset` 必须检查
      • 2.2 KingbaseES有两条主流落地路线
        • 方案A:Identity
        • 方案B:Sequence + Default
    • 3. 复现过程:最容易踩的五个坑
      • 3.1 坑一:导入3亿历史ID后,目标生成器仍从1开始
      • 3.2 坑二:误以为显式插入历史ID会自动同步目标生成器
      • 3.3 坑三:把主键是否连续当成验收指标
      • 3.4 坑四:只测试单条INSERT,不测试批量主键回传
      • 3.5 坑五:源库用多写AUTO_INCREMENT,目标却按单写设计
    • 4. 方案实施:一套可执行的AUTO_INCREMENT迁移步骤
      • 4.1 第一步:建立主键清单
      • 4.2 第二步:处理 UNSIGNED
      • 4.3 第三步:优先Identity映射
      • 4.4 第四步:Sequence兜底方案
      • 4.5 第五步:全量导数保留原ID
      • 4.6 第六步:增量同步仍然传原ID
      • 4.7 第七步:切流前二次校准生成器
      • 4.8 第八步:并发回归
      • 单条 INSERT
      • 10/50/100并发
      • 回滚
      • 显式历史ID
      • 4.9 第九步:批量INSERT单独验证
      • 4.10 第十步:ON DUPLICATE KEY UPDATE一起扫描
    • 5. 结果对比:验收必须同时看数据、生成器和应用
      • 5.1 历史数据
      • 5.2 外键和业务引用
      • 5.3 生成器安全距离
      • 5.4 应用主键回传
      • 5.5 示例回归结果模板
      • 5.6 性能指标
    • 6. 风险与复盘:自增主键最危险的是“双主同时生成”
      • 6.1 风险一:切换窗口两边都在自动生成ID
      • 6.2 风险二:多库分片ID被误当单库AUTO_INCREMENT
      • 6.3 风险三:Sequence CACHE造成空洞被误判数据丢失
      • 6.4 风险四:历史高值异常
      • 6.5 风险五:BIGINT UNSIGNED容量问题
      • 6.6 风险六:LAST_INSERT_ID依赖被遗漏
      • 6.7 风险七:Sequence权限遗漏
  • 回退方案:回源之前也要重新校准AUTO_INCREMENT
  • 最终复盘
  • 附录 A:MySQL源DDL
  • 附录 B:KingbaseES Identity方案
  • 附录 C:KingbaseES Sequence方案
  • 附录 D:最低回归清单

每日一句正能量

“人生没有标准答案,敢于重新开始的人永远自带光芒。”
每个人都可以书写自己的答案。而“重新开始”的能力,是一个人生命力最璀璨的证明。跌倒后能爬起,归零后能重启,这种勇气本身就是一种无法被忽视的光芒。

主题:AUTO_INCREMENT 兼容 / MySQL → KingbaseES / 互联网业务迁移
重点:DDL 转换、Identity/Sequence 选择、历史 ID 保留、并发写入、主键回传、回归测试、数据校验与回退
适用场景:订单、用户、支付流水、内容主表、互联网业务单库或分片库迁移。


1. 背景与问题:AUTO_INCREMENT迁移的核心,是把“下一个ID由谁生成”迁过去

MySQL 互联网业务里最常见的主键定义之一:

CREATETABLEbiz_order(order_idBIGINTNOTNULLAUTO_INCREMENT,...PRIMARYKEY(order_id));

很多迁移方案会直接写:

AUTO_INCREMENT → Identity

然后认为工作完成。

但自增主键真正影响的不是一行 DDL,而是一整条写入链路:

INSERT不传ID → 数据库生成ID → 驱动/ORM拿回ID → 子表/消息使用这个ID → CDC传播 → 下一条记录继续生成

对互联网业务还可能存在:

auto_increment_increment auto_increment_offset 多主写入 分库分表 号段 雪花ID 外部ID服务

因此迁移前必须先判断:

源系统到底是在使用 MySQL 的“单实例 AUTO_INCREMENT”,还是借用了 AUTO_INCREMENT 做多写节点的号段隔离。

MySQL 8.4 官方说明,InnoDB 为 AUTO_INCREMENT 维护专门计数器;不显式提供值时,由计数器产生新值。如果显式插入的 ID 大于当前计数器,后续计数器会被推进。MySQL 同时提供auto_increment_incrementauto_increment_offset,可以用于多服务器生成互不冲突的自增值。

所以迁移目标必须覆盖:

历史ID不变 目标新ID不冲突 应用主键回传正常 并发写入安全 多写架构语义不丢失 异常时能回退


2. 环境与数据:先判断是“简单自增”还是“分布式编号”

示例环境:

源库:MySQL 8.0/8.4 InnoDB 目标:KingbaseES V9 业务:互联网订单服务 单表数据:约3亿 日新增:300万 主键:BIGINT AUTO_INCREMENT 写入方式:JDBC/MyBatis

源表:

CREATETABLEbiz_order(order_idBIGINTNOTNULLAUTO_INCREMENT,customer_idBIGINTNOTNULL,order_noVARCHAR(64)NOTNULL,amountDECIMAL(18,2)NOTNULL,created_atDATETIME(6)NOTNULL,PRIMARYKEY(order_id),UNIQUEKEYuk_order_no(order_no));

迁移前至少记录:

当前MAX(order_id) AUTO_INCREMENT当前起点 数据类型是否UNSIGNED auto_increment_increment auto_increment_offset 主库数量 是否多主写 应用如何取得新ID 是否有显式插入ID逻辑

2.1 为什么auto_increment_increment/offset必须检查

MySQL 官方文档说明:

auto_increment_increment

控制每次自增的步长;

auto_increment_offset

控制起始偏移。

比如两个写节点:

节点A:offset=1, increment=2 → 1,3,5,7... 节点B:offset=2, increment=2 → 2,4,6,8...

如果这种架构迁移到 KingbaseES 后简单变成:

所有节点共用一个 START 1 INCREMENT 1

虽然不会一定产生重复,但源系统的分布式编号策略已经改变。

如果应用或分片逻辑依赖:

id % 2

判断来源节点,就会出现业务问题。

因此要先做“主键架构识别”。


2.2 KingbaseES有两条主流落地路线

当前 KingbaseES SQL 参考明确支持:

GENERATED ALWAYSASIDENTITYGENERATEDBYDEFAULTASIDENTITY

Identity 列会绑定一个隐式序列,新插入行可以自动获得值。

另外 KingbaseES 还提供独立:

CREATESEQUENCE

支持:

START WITH INCREMENT BY CACHE NO CYCLE NEXTVAL CURRVAL SETVAL

因此最常用两条路线:

方案A:Identity
order_idBIGINTGENERATEDBYDEFAULTASIDENTITY

适合:

单写服务 传统AUTO_INCREMENT表 希望DDL简单
方案B:Sequence + Default
CREATESEQUENCE biz_order_id_seq;order_idBIGINTDEFAULTNEXTVAL('biz_order_id_seq')

适合:

需要显式管理生成器 需要更清楚控制START/INCREMENT/CACHE 迁移工具对Identity支持一般


3. 复现过程:最容易踩的五个坑

3.1 坑一:导入3亿历史ID后,目标生成器仍从1开始

源端:

MAX(order_id)=386,120,008

全量迁移:

order_id原值全部保留

目标数据检查:

COUNT一致 MAX一致

如果 Identity/Sequence 仍然从:

1

开始,切流后就有主键冲突风险。

因此切换前必须满足:

目标NEXT VALUE > 全局已使用MAX(id)

这应该是自动化阻断条件,而不是检查清单里“人工看一眼”。


3.2 坑二:误以为显式插入历史ID会自动同步目标生成器

MySQL 的行为很容易形成惯性。

MySQL 官方示例说明,如果向 AUTO_INCREMENT 列显式写一个较大的值,例如:

100

随后自动生成的值可以从:

101

继续。

因此 DBA 容易形成:

“我已经导入了历史ID,目标自增肯定也知道最大值。”

但 KingbaseES 的 Identity 依赖隐式序列;BY DEFAULT允许显式历史值优先,不代表迁移程序就可以省略生成器校准。

正确做法仍然是:

导完数据 → SELECT MAX(id) → 检查Identity/Sequence当前状态 → 显式RESTART/SETVAL/ALTER

3.3 坑三:把主键是否连续当成验收指标

MySQL AUTO_INCREMENT 在:

事务回滚 并发插入 失败插入 批量语句

场景下并不应该被当作业务连续号码。

KingbaseES Sequence 也一样。

官方开发规范明确建议:

不要将商业逻辑建立在序列完全连续性上

并说明增大 CACHE 可以减少争用,但会增加不连续的可能。

所以验收重点是:

唯一 不会回退到已使用范围 并发安全

不是:

1001、1002、1003一个都不能少

订单号、发票号如果要求特定连续规则,应使用独立业务编号机制。


3.4 坑四:只测试单条INSERT,不测试批量主键回传

MySQL 生态中很多框架依赖:

LAST_INSERT_ID() getGeneratedKeys() useGeneratedKeys

MySQL 官方文档也明确提供LAST_INSERT_ID()/mysql_insert_id()获取最近自动生成的 AUTO_INCREMENT 值。

迁到 KingbaseES 后,数据库生成 ID 没问题,不代表:

JDBC MyBatis JPA 批量INSERT

都能用完全相同方式拿到主键。

尤其批量插入:

INSERTINTO...VALUES(...),(...),(...);

不能只假设:

拿到第一个ID → 后面的ID必然连续推导

因为这种假设把“生成器连续性”当成了应用协议。

应该让真实驱动/ORM返回并验证每个生成主键。


3.5 坑五:源库用多写AUTO_INCREMENT,目标却按单写设计

MySQL FAQ 明确说明,MySQL 本身没有通用 Sequence,但可以通过:

auto_increment_increment auto_increment_offset

在多服务器场景减少 AUTO_INCREMENT 冲突。

如果源系统:

双主 多源复制 多机房

迁移时必须回答:

目标还是多写吗?

如果目标变成:

单主写

可以把主键生成收敛到单一 Sequence/Identity。

如果目标仍然需要:

多节点独立生成ID

则要重新设计:

不同START/OFFSET的Sequence 号段 全局ID服务 雪花ID

不能只把源表 DDL 翻译一下。


4. 方案实施:一套可执行的AUTO_INCREMENT迁移步骤

4.1 第一步:建立主键清单

建议 SQL 清单至少记录:

schema table column type unsigned current_max_id auto_increment increment offset foreign_key_count write_qps id_generation_mode

分类:

S1:单库单写AUTO_INCREMENT S2:多写increment/offset S3:分库分表号段 S4:外部ID/雪花 S5:业务显式赋ID

优先迁:

S1

复杂度最低。


4.2 第二步:处理 UNSIGNED

MySQL 常见:

BIGINTUNSIGNEDAUTO_INCREMENT

这不仅是自增问题,也是数据类型范围问题。

目标 KingbaseES 如果采用有符号BIGINT,必须检查:

MAX(id)

是否已经超过目标类型上限。

大部分业务实际值远低于上限,但迁移评估不能靠猜。

必须:

MAX(id) + 未来增长年限

做容量评估。


4.3 第三步:优先Identity映射

典型目标:

CREATETABLEbiz_order(order_idBIGINTGENERATEDBYDEFAULTASIDENTITY(STARTWITH1INCREMENTBY1)PRIMARYKEY,customer_idBIGINTNOTNULL,order_noVARCHAR(64)NOTNULLUNIQUE,amountNUMERIC(18,2)NOTNULL,created_atTIMESTAMP(6)NOTNULL);

为什么使用:

BY DEFAULT

而不是迁移阶段直接ALWAYS

KingbaseES 官方语义是:

BY DEFAULT: 用户显式提供值时,用户值优先 ALWAYS: 默认强制使用生成值,除非显式覆盖系统值

迁移全量和增量都需要保留 MySQL 历史 ID,因此BY DEFAULT更方便。

切换完成后是否调整更严格的写入策略,可以通过:

应用层禁止传ID 权限 SQL审计

实现。


4.4 第四步:Sequence兜底方案

如果想把生成器独立出来:

CREATESEQUENCE biz_order_id_seqASBIGINTSTARTWITH386120009INCREMENTBY1CACHE100NOCYCLE;

列:

order_idBIGINTDEFAULTNEXTVAL('biz_order_id_seq')

KingbaseES 官方序列文档明确支持START WITHINCREMENT BYCACHENO CYCLE,并可通过nextval/currval/setval管理生成器。

Sequence 方案的工程优势:

生成器独立可见 参数更容易审计 多表/特殊生成策略可重用 迁移脚本容易校准

缺点:

DDL比Identity多一个对象 权限要单独确认 对象命名和生命周期要治理

4.5 第五步:全量导数保留原ID

历史订单:

1 2 ... 386120008

目标必须仍然是:

1 2 ... 386120008

不要重新生成。

原因:

order_detail.order_id payment.order_id message业务键 数据仓库引用 日志关联

都可能已经依赖原ID。

如果重新编号,就会把“主表迁移”变成“全链路主键重映射项目”。


4.6 第六步:增量同步仍然传原ID

全量迁移运行几个小时甚至几天时,MySQL 仍有新订单写入。

CDC 增量:

INSERT order_id=386120009

目标也要:

INSERT order_id=386120009

而不是由 KingbaseES 自己生成一个 ID。

迁移期:

MySQL是主键权威

切流后:

KingbaseES才成为新的主键权威

这个切换点必须明确。


4.7 第七步:切流前二次校准生成器

第一次全量导完:

MAX(id)=386120008

几小时后 CDC 已经追到:

386520100

所以不能只在全量后校准一次。

切流正确顺序:

停止源端新写 ↓ 追平最后CDC ↓ 源目标MAX(id)比对 ↓ 目标生成器再次校准 ↓ 验证NEXT VALUE安全 ↓ 开启KingbaseES写

4.8 第八步:并发回归

至少测试:

单条 INSERT

INSERTINTObiz_order(customer_id,order_no,amount)VALUES(...);

断言:

生成ID非NULL 数据库行ID = 应用拿到ID

10/50/100并发

检查:

duplicate=0 PK conflict=0 generated ids unique

回滚

BEGIN INSERT ROLLBACK

允许:

ID出现空洞

但后续不得产生重复。

显式历史ID

分别插入:

比当前MAX低 比当前MAX高

确认迁移流程和后续校准脚本都能正确处理。


4.9 第九步:批量INSERT单独验证

MySQL 应用很喜欢:

INSERTINTOt(...)VALUES(...),(...),(...);

迁移后至少验证:

生成多少个ID 驱动返回多少个 顺序是否对应输入行 批次部分失败如何处理

不要用:

first_id + i

推算后续主键作为正式设计。


4.10 第十步:ON DUPLICATE KEY UPDATE一起扫描

互联网 MySQL 常见:

INSERT...ONDUPLICATEKEYUPDATE...

MySQL 官方文档说明,它遇到 UNIQUE/PRIMARY KEY 冲突时可以转为 UPDATE;在含 AUTO_INCREMENT 的表上,它还会影响自增值和LAST_INSERT_ID()相关行为。

因此迁移自增主键时应该顺便扫描:

ON DUPLICATE KEY UPDATE REPLACE INTO INSERT IGNORE LAST_INSERT_ID

这些都属于“写入语义”,不能只改表结构。


5. 结果对比:验收必须同时看数据、生成器和应用

5.1 历史数据

至少比较:

COUNT(*)MIN(id)MAX(id)COUNT(DISTINCTid)

断言:

COUNT = COUNT(DISTINCT id)

主键无重复。


5.2 外键和业务引用

例如:

SELECTCOUNT(*)FROMorder_detail dLEFTJOINbiz_order oONd.order_id=o.order_idWHEREo.order_idISNULL;

结果:

0

5.3 生成器安全距离

迁移完成:

MAX(id)=386520100

目标下一值必须:

>386520100

如果采用多节点号段,还要验证:

所有节点未来生成空间互不冲突

5.4 应用主键回传

测试:

JDBC getGeneratedKeys MyBatis useGeneratedKeys JPA @GeneratedValue 批量Insert 事务Insert

必须断言:

应用对象ID = 数据库实际ID

5.5 示例回归结果模板

用例MySQLKingbaseES结果
单条自动ID成功成功通过
显式历史ID保留保留通过
50并发无重复无重复通过
ROLLBACK后继续插入允许空洞允许空洞通过
JDBC取主键正常正常通过
批量Insert返回ID已验证返回通过

这些是验收模板,不是本文声称的真实生产结果。


5.6 性能指标

自增迁移还应该记录:

Insert TPS P50/P95/P99 生成器等待 WAL 索引写入 Sequence CACHE大小

KingbaseES 官方开发规范建议适当增大 Sequence CACHE 可以降低争用,但同时会增加序列不连续性。

互联网高并发业务可以测试:

CACHE 1 CACHE 20 CACHE 100 CACHE 300

选择性能和可接受空洞之间的平衡。

再次强调:

主键唯一

远比:

主键连续

重要。


6. 风险与复盘:自增主键最危险的是“双主同时生成”

6.1 风险一:切换窗口两边都在自动生成ID

这是最大的风险。

如果:

MySQL继续AUTO_INCREMENT + KingbaseES Identity也开放

两个库可能在相同数值空间生成新ID。

所以切换要保证:

同一个业务主键空间在任意时刻只能有一个权威生成器,除非已经设计了明确不冲突的号段。


6.2 风险二:多库分片ID被误当单库AUTO_INCREMENT

比如:

db0 → 奇数 db1 → 偶数

或者:

每个分片从不同亿级号段开始

这些信息可能根本不在表 DDL 中,而在:

MySQL系统变量 部署配置 中间件 应用代码

迁移清单必须覆盖数据库外部。


6.3 风险三:Sequence CACHE造成空洞被误判数据丢失

KingbaseES 官方文档说明,缓存序列号能提升性能,但实例异常关闭时,缓存中尚未使用的值可能被跳过。

这不是订单丢失。

真正的订单完整性应该通过:

业务唯一键 记录数 状态 消息链路

判断。

不要用:

ID必须连续

做数据完整性校验。


6.4 风险四:历史高值异常

可能绝大多数:

id < 4亿

但曾经人工修复:

id=9,000,000,000

如果只按“正常增长趋势”设置目标序列:

4亿+1

最终仍会撞到历史记录。

所以必须读取真实:

MAX(id)

6.5 风险五:BIGINT UNSIGNED容量问题

如果 MySQL 使用:

BIGINT UNSIGNED

目标类型范围可能不同。

即使当前数据没超过范围,也要评估:

未来3年/5年增长

不能等迁移后几年才发现主键逼近上限。


6.6 风险六:LAST_INSERT_ID依赖被遗漏

应用可能直接执行:

SELECTLAST_INSERT_ID();

MySQL 官方说明LAST_INSERT_ID()对当前连接生成的 AUTO_INCREMENT 值有明确语义。

迁移后不能假设这个函数和连接态语义仍然完全一样。

更推荐应用通过目标驱动支持的:

generated keys RETURNING

获取新主键,并做真实连接池测试。


6.7 风险七:Sequence权限遗漏

如果用:

Sequence + Default

应用账号除了表 INSERT 权限,还需要确认能够正常访问相关序列。

这种权限问题往往在:

DBA账号测试成功 生产应用账号失败

时才暴露。

切换清单要使用真实应用账号执行。


回退方案:回源之前也要重新校准AUTO_INCREMENT

假设:

MySQL最后ID = 386520100

切到 KingbaseES 后又写了:

50000条

最大 ID 已经:

386570100

现在因为应用兼容问题需要回退 MySQL。

不能简单:

把连接串切回MySQL

否则 MySQL 原计数器可能继续从:

386520101

生成,而 KingbaseES 窗口期已经使用了这些 ID。

正确回退:

1. 停止KingbaseES新写 2. 固化目标最后ID和业务水位 3. 将目标新增记录反向同步MySQL并保留原ID 4. 校验两端MAX(id) 5. 将MySQL AUTO_INCREMENT推进到全局MAX(id)+安全步长 6. 恢复MySQL写入口 7. KingbaseES转为只读排障

回退原则和切流原则其实完全一致:

下一任主库的主键生成器,必须位于所有已使用ID之后。


最终复盘

MySQL 到 KingbaseES 的 AUTO_INCREMENT 迁移建议按五层处理:

第一层:识别源编号架构 AUTO_INCREMENT / increment+offset / 分片 / 外部ID 第二层:选择目标生成器 Identity / Sequence / 外部ID服务 第三层:保留历史主键 全量 + CDC 都显式传原ID 第四层:校准生成器 NEXT VALUE > 全局MAX(id) 第五层:回归与回退 并发写 + 主键回传 + 双端水位 + 回源校准

如果只记住一句话:

AUTO_INCREMENT 迁移不是把关键字换掉,而是在迁移“谁拥有下一个唯一ID的生成权”。

这个生成权只要在切换窗口中模糊一秒,就可能留下后续非常难修复的主键冲突。


附录 A:MySQL源DDL

CREATETABLEbiz_order(order_idBIGINTNOTNULLAUTO_INCREMENT,customer_idBIGINTNOTNULL,order_noVARCHAR(64)NOTNULL,amountDECIMAL(18,2)NOTNULL,PRIMARYKEY(order_id));

附录 B:KingbaseES Identity方案

CREATETABLEbiz_order(order_idBIGINTGENERATEDBYDEFAULTASIDENTITY(STARTWITH1INCREMENTBY1)PRIMARYKEY,customer_idBIGINTNOTNULL,order_noVARCHAR(64)NOTNULL,amountNUMERIC(18,2)NOTNULL);

附录 C:KingbaseES Sequence方案

CREATESEQUENCE biz_order_id_seqASBIGINTSTARTWITH386520101INCREMENTBY1CACHE100NOCYCLE;CREATETABLEbiz_order(order_idBIGINTPRIMARYKEYDEFAULTNEXTVAL('biz_order_id_seq'),...);

附录 D:最低回归清单

[ ] AUTO_INCREMENT列已全部扫描 [ ] 当前MAX(id)已记录 [ ] increment/offset已记录 [ ] UNSIGNED范围已评估 [ ] 分库分表/外部ID逻辑已识别 [ ] 全量导入保留原ID [ ] CDC保留原ID [ ] 目标生成器已二次校准 [ ] 单条INSERT主键回传通过 [ ] 批量INSERT主键回传通过 [ ] 10/50/100并发无重复 [ ] 回滚后写入通过 [ ] 应用真实账号权限通过 [ ] 回退AUTO_INCREMENT校准脚本已演练

转载自:https://blog.csdn.net/u014727709/article/details/163728579
欢迎 👍点赞✍评论⭐收藏,欢迎指正

← 返回列表