迁移后序列错位导致主键冲突的排查——高并发写入系统的序列校准、修复SQL与预防机制 📅 发布时间:2026/8/19 11:49:19 👁 浏览次数: 文章目录每日一句正能量1. 背景与问题数据迁对了为什么第一笔新订单就撞主键2. 环境与数据先把“主键字段”和“发号器”分开盘点2.1 主键来源可能不止Sequence2.2 KingbaseES Identity 本质上也依赖隐式序列2.3 建立“发号器台账”2.4 information_schema.sequences 只能看到序列定义不等于安全水位3. 复现过程五种序列错位症状看起来一样但根因不同3.1 根因一全量导入显式ID没有推进目标序列3.2 根因二全量结束时校准过但CDC又把ID继续推高3.3 根因三源和目标同时拥有发号权3.4 根因四回退以后忘记重新推进源端生成器3.5 根因五人工setval写错语义4. 方案实施从排查到安全校准的标准步骤4.1 第一步确认冲突主键到底来自哪里4.2 第二步查表MAX(id)4.3 第三步查序列状态4.4 第四步判断安全间隔4.5 第五步在线修复优先考虑setval4.6 第六步维护窗口可使用ALTER SEQUENCE RESTART4.7 第七步Identity也按“MAX生成器”原则校准4.8 第八步先停源写再做最终校准4.9 第九步高并发场景不要只测试一次nextval4.10 第十步CACHE会造成空洞但不应该当故障4.11 第十一步多实例/分片ID要保留步长语义5. 结果对比一次生产主键冲突如何从5分钟定位到根因5.1 数据检查5.2 序列检查5.3 水位检查5.4 止血5.5 修复5.6 数据修复5.7 修复结果模板6. 风险与复盘序列校准真正危险的是“边修边写”6.1 风险一在线MAX(id)不是固定值6.2 风险二只推进Sequence不修失败交易6.3 风险三随便删除冲突行6.4 风险四错误理解setval第三参数6.5 风险五RESTART在高并发场景产生阻塞6.6 风险六Sequence CYCLE6.7 风险七序列领先很多不一定需要“调小”回退方案切回源库之前也必须重新校准源生成器回退门禁预防机制把序列校准做成迁移门禁而不是事故脚本1. 迁移前资产盘点2. 全量以后做一次中间校准3. CDC期间持续巡检4. 最终割接门禁强制校准5. 监控duplicate key6. 回退脚本必须带生成器校准最终复盘附录 A序列盘点附录 B迁移后校准示例附录 C维护窗口RESTART附录 D最低并发验证附录 E最低割接门禁每日一句正能量所谓机遇不过是你的努力走到了能与它相遇的地方。机遇不是偶然的运气而是努力的必然。机遇有自己的路径努力也有自己的轨迹。当两条线交汇人们称之为运气而这条交汇线其实是努力铺设的。主题序列值校准 / 高并发写入 / 迁移后故障排查重点Sequence、Identity、nextval、setval、ALTER SEQUENCE RESTART、全量与 CDC 水位、并发验证、主键冲突修复与回退适用场景MySQL AUTO_INCREMENT、SQL Server IDENTITY、Oracle Sequence 或其他自增主键迁移至 KingbaseES 后目标库开始主写时出现 duplicate key / unique violation 的系统。1. 背景与问题数据迁对了为什么第一笔新订单就撞主键迁移项目最让人崩溃的一类故障是全量行数一致 Checksum通过 应用连接正常 KFS已经追平正式切换后第一批写请求却开始报duplicate key value violates unique constraint例如目标表已经有order_id 1 ~ 8,000,123但目标序列状态还停留在5,000,000业务切到 KingbaseES 后INSERTINTOtrade_order(...)VALUES(...);默认值触发nextval(...)继续从5,000,001发号。而这个 ID历史数据早就已经存在于是直接撞主键。这类问题说明主键迁移成功不代表主键“生成权”也迁移成功。KingbaseES 官方CREATE SEQUENCE文档说明序列创建后可以使用nextval currval setval进行操作而且官方给出了一个非常贴近迁移的例子COPY 数据以后 用 SELECT setval(sequence, max(id)) 推进序列官方同时说明序列的nextval、setval调用不会回滚因此序列出现空洞是正常现象不能把“连续无缺口”当成业务正确性要求。因此迁移后的序列验收应该关注生成器是否领先已存在数据而不是生成器有没有缺号2. 环境与数据先把“主键字段”和“发号器”分开盘点示例环境源 MySQL / SQL Server / Oracle 目标 KingbaseES V9 系统 在线订单/支付系统 峰值 8000 TPS 订单表 12亿行 主键 BIGINT 迁移 KDTS全量 KFS增量 目标 最终由KingbaseES生成新主键2.1 主键来源可能不止Sequence迁移评估必须先回答这个主键是谁生成的常见模式MySQL AUTO_INCREMENT SQL Server IDENTITY Oracle Sequence KingbaseES Sequence GENERATED ... AS IDENTITY 应用雪花ID 独立发号服务 分库分表步长ID如果应用已经使用雪花ID就不应该错误地去校准数据库序列。反过来如果目标 DDLidBIGINTDEFAULTnextval(seq)那这个序列就是生产写路径的一部分。2.2 KingbaseES Identity 本质上也依赖隐式序列KingbaseES 官方 SQL 文档说明GENERATED ALWAYSASIDENTITY和GENERATEDBYDEFAULTASIDENTITY都会为标识列附加一个隐式序列。其中ALWAYS更强调系统生成值。BY DEFAULT则允许用户显式提供值并且显式值优先。这对历史数据迁移很重要。因为全量阶段往往需要INSERTINTOtarget(id,...)VALUES(source_id,...);保留原主键。但必须牢记显式插入历史 ID 和“隐式序列已经推进到这个 ID”是两件不同的事情。因此目标 Identity 列也要做MAX(id) vs 背后生成器状态校验。2.3 建立“发号器台账”建议最少记录schema table pk_column generator_type generator_name increment cache cycle source_owner target_owner migration_strategy final_calibration_method rollback_calibration_method例如trade_order order_id sequence trade_order_id_seq increment1 cache1000如果是分库分表历史系统increment16 offset3就更不能简单MAX(id)1因为它可能破坏原有号段规则。2.4 information_schema.sequences 只能看到序列定义不等于安全水位KingbaseES 官方information_schema.sequences可以查看sequence_schema sequence_name data_type start_value min/max increment cycle这对资产盘点非常有用。但判断是否安全还必须结合具体序列当前状态 表MAX(id) 最后CDC水位因为定义正确并不代表当前值正确3. 复现过程五种序列错位症状看起来一样但根因不同3.1 根因一全量导入显式ID没有推进目标序列源MAX(order_id)5,000,000目标全量COPY/INSERT直接把1~5,000,000导入。目标序列last_value10,000全量校验100%成功只要业务没有自动取号不会报错一切问题都隐藏到正式主写第一刻才爆炸。3.2 根因二全量结束时校准过但CDC又把ID继续推高比如全量结束MAX(id)5,000,000你执行setval(...,5000000);看起来正确。随后源库继续跑一周。KFS同步5,000,001 ... 8,000,123这些都是源端已生成的显式业务主键目标序列却仍然5,000,000因为复制行数据并不等于调用目标 nextval所以真正的校准时刻应该是最终增量追平以后而不是全量结束以后。3.3 根因三源和目标同时拥有发号权迁移中有人为了“提前验证目标写”源端继续生成ID 目标也打开自动ID两个数据库都在nextval / identity但没有统一号段。这已经变成双主发号问题不只是 duplicate key。还可能两个系统生成不同业务记录却恰好使用同一个主键这是数据语义级灾难。所以主写切换和序列切换必须绑定一个权威写端 一个权威发号端3.4 根因四回退以后忘记重新推进源端生成器切到 KingbaseES 后运行30分钟目标生成了 8,000,124 ~ 8,050,000随后因为性能故障回退。团队正确地把这些新订单反向同步回MySQL但忘了推进MySQL AUTO_INCREMENT源库仍准备从8,000,124开始。业务恢复立刻撞反向同步回来的订单因此回退不是只同步数据还要同步“下一号生成权”。3.5 根因五人工setval写错语义最常见看到last_value 直接setval(last_value)没有查MAX(id)或者误用三参数setval中的is_called使下一次nextval返回的值不是预期值。所以生产校准脚本必须是固定模板 双人复核 切换前演练不要现场手敲。4. 方案实施从排查到安全校准的标准步骤4.1 第一步确认冲突主键到底来自哪里日志duplicate key: 5000001先问应用显式传了5000001 还是数据库自动生成检查INSERT SQL ORM generated key 列DEFAULT Identity定义 触发器如果应用自己生成修数据库sequence没用4.2 第二步查表MAX(id)SELECTCOUNT(*),MIN(order_id),MAX(order_id)FROMapp.trade_order;得到MAX8,000,1234.3 第三步查序列状态官方文档说明SELECT*FROMsequence_name;可以检查序列参数和状态包括last_value但官方也提醒该值在打印时可能已经过时因为并发会话可能正在调用nextval所以在线系统里MAX(id) vs last_value只能作为诊断。真正切换门禁必须先冻结写4.4 第四步判断安全间隔如果MAX(id)8,000,123 last_value5,000,000明确Sequence Behind如果last_value8,100,000不能只看比MAX大所以绝对安全还要确认increment cycle cache 是否有人设置过负步长一般单调递增、非cycle业务序列中生成器明显领先MAX(id)是安全方向。4.5 第五步在线修复优先考虑setvalKingbaseES 官方CREATE SEQUENCE文档给出了SELECTsetval(serial,max(id))FROMdistributors;作为COPY FROM之后更新序列的示例。对increment1的常规序列SELECTsetval(app.trade_order_id_seq,(SELECTMAX(order_id)FROMapp.trade_order));是非常典型的迁移校准方式。两参数setval的常见含义是序列当前已调用所以下一个nextval会继续向后推进在示例MAX8,000,123下一值应验证为8,000,124正式生产仍应先在同版本测试环境确认4.6 第六步维护窗口可使用ALTER SEQUENCE RESTARTKingbaseES 官方ALTER SEQUENCE文档说明ALTERSEQUENCE seq RESTARTWITHvalue;会改变当前序列值。它类似于setval(..., value, false)下一次nextval会返回restart_value并且一个很关键的差异是RESTART 是事务性的并且会阻止并发事务从同一序列获取值。这意味着高并发在线系统不能随便在业务高峰执行 RESTART。它更适合最终停写 维护窗口 主写切换这种明确的冻结阶段。4.7 第七步Identity也按“MAX生成器”原则校准KingbaseESGENERATEDBYDEFAULTASIDENTITY允许全量时显式插入历史主键。但切写前必须识别隐式序列 查询MAX(id) 推进/RESTART生成器 测试自动INSERT不要因为列定义写着IDENTITY就认为迁移工具会自动替你校准到MAX必须实测。4.8 第八步先停源写再做最终校准正确顺序1. 停源端所有写入口 2. 等待在途事务结束 3. KFS追平最终水位 4. 查询目标MAX(id) 5. 校准目标生成器 6. 单笔自动ID测试 7. 并发测试 8. 开目标主写如果顺序是先查MAX ↓ 源端又生成1000个ID ↓ 再校准你的 MAX已经过时4.9 第九步高并发场景不要只测试一次nextval单次SELECTnextval(...);成功只能证明能发一个号不能证明8000TPS不会撞至少做10并发 50并发 100并发验证返回ID总数 DISTINCT ID总数 主键冲突数 P95生成耗时理想COUNT(ids) COUNT(DISTINCT ids)4.10 第十步CACHE会造成空洞但不应该当故障KingbaseES 官方序列文档支持CACHE用于预分配数字提高效率。因此数据库重启 会话退出 事务失败都可能形成ID空洞官方同时指出nextval/setval不回滚所以序列本来就不保证无间隙。业务验收标准应该唯一 不会回卷 不会撞历史值不是1001后必须正好10024.11 第十一步多实例/分片ID要保留步长语义旧系统可能server A 1,17,33,... server B 2,18,34,...这种increment/offset设计用于多写节点避免冲突。迁移到一个统一序列以后是否改成increment1属于架构决策。不能在割接夜因为MAX(id)X就机械setval(X)还要确认下一值是否满足原号段/分片规则5. 结果对比一次生产主键冲突如何从5分钟定位到根因假设切KingbaseES主写后 第3秒 duplicate key 5,000,0015.1 数据检查目标MAX(id)8,000,123说明数据不是只到5,000,0005.2 序列检查last_value5,000,000于是Sequence Behind by 3,000,1235.3 水位检查KFSsource final watermark target applied watermark说明不是CDC漏数据而是CDC同步了显式主键 但目标Sequence没有跟着推进根因闭合。5.4 止血先关闭目标新增订单写保留查询。防止应用疯狂重试制造更多duplicate key日志和业务超时。5.5 修复在确认目标写冻结之后SELECTsetval(app.trade_order_id_seq,8000123);然后执行测试next generated id 8,000,124再做100并发 × 1000行无重复。恢复写。5.6 数据修复冲突期间失败的请求不能假设客户端一定会重试从API日志 Outbox 请求流水 MQ找出失败交易按业务幂等键补写。5.7 修复结果模板指标修复前修复后MAX(id)8,000,1238,050,123sequence last_value5,000,000≥8,050,123duplicate key/min18000自动ID测试FAILPASS100并发重复ID-0写接口P95超时恢复基线数据差异有失败请求补偿后0以上为演练示例不是本文声称的生产实测。6. 风险与复盘序列校准真正危险的是“边修边写”6.1 风险一在线MAX(id)不是固定值业务还在写你查到MAX100下一毫秒变成1000所以割接最终校准必须配合写冻结和CDC水位6.2 风险二只推进Sequence不修失败交易duplicate key期间部分接口已经失败生成器修好只解决未来过去失败的交易仍然缺失必须补偿。6.3 风险三随便删除冲突行出现id5000001已存在最危险的现场操作DELETEFROMtrade_orderWHEREid5000001;因为旧行可能是真实历史订单应该推进生成器而不是删历史数据给新ID“腾位置”。6.4 风险四错误理解setval第三参数is_called会影响下一次nextval是返回当前值 还是继续递增因此setval(seq, safe_next, false)和setval(seq, current_max)并不是同一语义。生产脚本必须通过同版本测试固定下来。6.5 风险五RESTART在高并发场景产生阻塞官方ALTER SEQUENCE文档明确说明RESTART 是事务性的 会阻止并发事务从同一序列获取值所以高峰在线修复不能随便RESTART应该先评估冻结窗口或选择更适合的校准方法。6.6 风险六Sequence CYCLE主键序列通常不应该CYCLE否则达到 MAXVALUE 后回卷理论上可能撞旧主键。资产盘点时cycle_option必须检查。6.7 风险七序列领先很多不一定需要“调小”假设MAX(id)8,000,000 sequence9,000,000中间空了100万对普通技术主键通常没关系把序列调回MAX1反而可能撞上缓存过但尚未落表的未来取号或者其他并发调用。所以序列领先通常是安全方向序列落后才是核心风险。不要为了“号码好看”做危险回拨。回退方案切回源库之前也必须重新校准源生成器假设KingbaseES主写30分钟产生id 8,000,124 ~ 8,050,000随后决定回退。正确冻结目标写 ↓ 反向同步目标窗口数据到源库 ↓ 查源端MAX(id) ↓ 推进源AUTO_INCREMENT/IDENTITY/Sequence ↓ 验证下一ID ↓ 恢复源主写如果省略推进源生成器回退后第一批交易仍然会撞刚反向同步的数据回退门禁目标主写已冻结 目标窗口增量全部反向同步 源表MAX(id)已确认 源生成器已领先MAX(id) 自动ID冒烟PASS 并发发号重复0 关键数据差异0全部通过才能恢复源主写预防机制把序列校准做成迁移门禁而不是事故脚本1. 迁移前资产盘点每张自增主键表 → generator owner → sequence/identity名称2. 全量以后做一次中间校准目的方便测试但明确标记NOT FINAL3. CDC期间持续巡检例如每天MAX(id) vs sequence last_value发现序列越来越落后是预期的也可以记录。关键是团队知道最终必须再校准4. 最终割接门禁强制校准停写 →追平 →MAX(id) →校准 →试写 →并发 →开主写变成自动化 Checklist。5. 监控duplicate key上线初期对duplicate key unique violation做分钟级告警尤其主键约束出现一次都要定位。6. 回退脚本必须带生成器校准正向切换有target generator calibration回退也必须有source generator calibration两边对称。最终复盘迁移后主键冲突最容易误判成数据重复实际上很多时候是历史数据已经迁到未来 生成器还停在过去排查只要抓住三组值MAX(id) Sequence/Identity当前状态 最终同步水位就很容易形成闭环。正确的迁移流程应该是保留历史ID ↓ 全量迁移 ↓ 增量继续同步显式ID ↓ 最终停写 ↓ KFS追平 ↓ 获取最新MAX(id) ↓ 校准生成器 ↓ 单笔并发验证 ↓ 开放目标主写如果只记住一句话主键迁移的最后一步不是“最后一行数据同步完成”而是把“下一号应该由谁生成、从哪里开始生成”也一并完成权威转移。对高并发交易系统来说这一步应该和最终水位 数据校验 主写切换处在同一级别的生产门禁中。附录 A序列盘点SELECTsequence_schema,sequence_name,start_value,increment,cycle_optionFROMinformation_schema.sequences;附录 B迁移后校准示例BEGIN;SELECTsetval(app.trade_order_id_seq,COALESCE((SELECTMAX(order_id)FROMapp.trade_order),0));COMMIT;生产使用前必须在实际 KingbaseES 版本验证setval的下一值语义。附录 C维护窗口RESTARTALTERSEQUENCE app.trade_order_id_seq RESTARTWITH8000124;官方文档说明RESTART是事务性的会阻止并发序列取值应在受控窗口使用。附录 D最低并发验证[ ] 1并发 × 1000 [ ] 10并发 × 1000 [ ] 50并发 × 1000 [ ] 100并发 × 1000 [ ] COUNT(id)COUNT(DISTINCT id) [ ] 主键冲突0 [ ] 生成器最终值 MAX(id) [ ] P95生成耗时达标附录 E最低割接门禁[ ] 所有主键生成方式已盘点 [ ] 目标序列/Identity名称已确认 [ ] 源写已冻结 [ ] KFS最终水位闭合 [ ] MAX(id)在最终追平后获取 [ ] 目标生成器已校准 [ ] 自动主键冒烟PASS [ ] 高并发发号无重复 [ ] 回退生成器脚本已准备转载自https://blog.csdn.net/u014727709/article/details/163781197欢迎 点赞✍评论⭐收藏欢迎指正