【金仓数据库征文】Oracle到金仓:序列、触发器与自增键改造实践

【金仓数据库征文】Oracle到金仓:序列、触发器与自增键改造实践
文章目录每日一句正能量导读1. 背景与问题2. 环境与数据2.1 验证环境2.2 源端对象扫描3. 复现过程3.1 复现“历史主键被触发器覆盖”3.2 复现“序列起点低于历史最大值”3.3 复现“序列空洞被误判为丢单”3.4 复现并发冲突4. 方案实施4.1 方案选择4.2 目标端保留序列4.3 保留触发器时的改造4.4 历史装载与序列校准4.5 灰度期间使用独立高位号段4.6 权限改造5. 结果对比5.1 对象级校验5.2 数据级校验5.3 并发验证结果模板5.4 切换门禁6. 风险与复盘6.1 灰度切换步骤6.2 回退方案6.3 关键风险清单6.4 项目复盘附录 A对象改造脚本模板附录 B上线检查清单每日一句正能量世界上最可怕的事是你把别人当成了朋友别人并没拿你当朋友。人际关系中最深的孤独不是没有朋友而是你付出的真心被对方视为寻常甚至利用。这种不对等的情感投射会让人产生自我怀疑。友谊需要双向确认单方面的热情只是一厢情愿的冒险。导读本文以订单系统迁移为背景讨论 Oracle 序列、BEFORE INSERT触发器、应用显式取号和目标端自增机制之间的兼容改造。示例强调可复现验证不把“SQL 能执行”当作迁移完成而是把对象依赖、并发唯一性、历史数据装载、生成器校准和可回退性一并纳入验收。1. 背景与问题订单系统的主键生成看起来简单插入一行得到一个新的订单号。但在真实系统里主键通常贯穿订单主表、明细、支付流水、物流任务、消息队列和外部接口。迁移时只要生成机制有一处改错就可能出现重复键、子表找不到主表、历史订单被覆盖甚至回退时新订单无法写回 Oracle。本次演练中的 Oracle 源库存在三种取号路径-- 路径一应用显式取号SELECTseq_order_id.NEXTVALFROMdual;-- 路径二插入时不传主键由触发器赋值INSERTINTOt_order(order_no,customer_id,amount)VALUES(:order_no,:customer_id,:amount);-- 路径三批量导入时显式传入历史主键INSERTINTOt_order(order_id,order_no,customer_id,amount)VALUES(:order_id,:order_no,:customer_id,:amount);对应的 Oracle 对象如下CREATESEQUENCE seq_order_idSTARTWITH100000000INCREMENTBY1MAXVALUE999999999999999999NOCYCLE CACHE100NOORDER;CREATEORREPLACETRIGGERtrg_bi_order_id BEFOREINSERTONt_orderFOR EACH ROWWHEN(NEW.order_idISNULL)BEGINSELECTseq_order_id.NEXTVALINTO:NEW.order_idFROMdual;END;/真正的迁移难点不在语法替换而在于以下问题目标端是否继续使用“序列触发器”还是改为列默认值或身份列迁移历史数据时触发器是否会覆盖原主键全量迁移和在线增量同时进行时目标序列从哪里起步应用是否同时存在NEXTVAL、触发器和 ORM 自动生成三套逻辑序列缓存导致的空洞是否会被业务错误地判断为“丢单”切换后产生的新主键能否在回退时写回 Oracle业务账号在目标端是否具有正确的序列和触发器权限。Oracle 官方文档说明序列通过NEXTVAL生成值可配置起始值、步长、最大值、循环和缓存缓存可以提升取号性能但实例故障时未使用的缓存值可能丢失因此序列天然不保证连续。KingbaseES 官方文档同样提供CREATE SEQUENCE并可通过nextval、currval和setval操作序列。两端功能相似但对象语法、默认值表达式、权限模型和兼容模式仍应按实际版本验证。主键生成机制兼容评估图2. 环境与数据2.1 验证环境项目源端目标端数据库Oracle 19cKingbaseES V8 兼容环境应用Java 订单服务同一套迁移分支数据规模订单主表约 5000 万行全量副本峰值写入约 3000 单/秒按相同压测模型验证主键类型NUMBER(19,0)NUMERIC(19,0)或等价整数型迁移方式全量 增量同步灰度切换版本、数据量和性能指标必须替换为真实项目数据。本文数值仅用于展示验证方法。2.2 源端对象扫描先扫描序列SELECTsequence_owner,sequence_name,min_value,max_value,increment_by,cycle_flag,order_flag,cache_size,last_numberFROMdba_sequencesWHEREsequence_ownerOMSORDERBYsequence_name;LAST_NUMBER不能直接等同于“最后一个已使用值”尤其在启用缓存时。它只能作为辅助信息真正安全的起点还要结合业务表最大主键和在线增量。扫描触发器及状态SELECTowner,trigger_name,table_name,triggering_event,trigger_type,statusFROMdba_triggersWHEREownerOMSORDERBYtable_name,trigger_name;提取触发器定义SELECTowner,name,type,line,textFROMdba_sourceWHEREownerOMSANDtypeTRIGGERORDERBYname,line;扫描列默认值和身份列SELECTowner,table_name,column_name,data_default,identity_columnFROMdba_tab_columnsWHEREownerOMSAND(data_defaultISNOTNULLORidentity_columnYES)ORDERBYtable_name,column_id;除了数据库对象还要搜索应用代码和配置NEXTVAL CURRVAL selectKey useGeneratedKeys GenerationType.SEQUENCE GenerationType.IDENTITY SequenceGenerator before insert order_id如果同一张表既有触发器又有 MyBatisselectKey或 JPASequenceGenerator就必须明确谁负责生成主键。双重取号通常不会直接重复但会造成大量空洞、逻辑分叉和难以回退。3. 复现过程3.1 复现“历史主键被触发器覆盖”错误触发器常写成无条件赋值CREATEORREPLACETRIGGERtrg_bi_order_id BEFOREINSERTONt_orderFOR EACH ROWBEGIN:NEW.order_id :seq_order_id.NEXTVAL;END;/当迁移工具显式插入历史主键时该触发器仍然生成新值导致目标端主键与 Oracle 不一致明细表仍引用旧主键外键装载失败外部系统按历史订单 ID 查询不到数据回退时无法将目标端数据和源端订单关联。正确原则是“仅在主键为空时取号”IF:NEW.order_idISNULLTHEN:NEW.order_id :seq_order_id.NEXTVAL;ENDIF;迁移历史数据时还可以临时禁用触发器但必须记录禁用窗口并在启用前完成序列校准。3.2 复现“序列起点低于历史最大值”假设源表最大订单主键为SELECTMAX(order_id)FROMt_order;-- 结果1087654321目标端却按旧 DDL 创建CREATESEQUENCE seq_order_idSTARTWITH100000000;历史数据导入后一旦在线写入序列迟早会撞上已经存在的主键。这个问题在小规模测试中可能不出现因为序列尚未增长到冲突区间正式运行一段时间后才爆发风险很高。安全起点至少满足目标序列下一值 max( Oracle 当前最大主键, 目标端已迁移最大主键, 增量同步队列中的最大主键, 灰度期间目标端已生成最大主键 )建议额外预留安全步长例如取上述最大值加 10000但预留量必须结合峰值写入和同步延迟确定不能随意拍脑袋。3.3 复现“序列空洞被误判为丢单”执行SELECTseq_order_id.NEXTVALFROMdual;ROLLBACK;SELECTseq_order_id.NEXTVALFROMdual;序列值通常不会因事务回滚而回退。启用缓存后数据库重启或节点故障也可能造成未使用号段丢失。因此序列负责唯一取号不负责连续编号对账不能用“最大 ID - 最小 ID 1”推导订单数量财务凭证号、发票号等需要连续性监管的编号应采用独立业务规则而不是直接依赖数据库序列监控应关注重复、越界和生成失败不应把空洞本身当成数据库错误。3.4 复现并发冲突创建最小测试表CREATETABLEt_order_pk_test(order_idNUMERIC(19,0)PRIMARYKEY,order_noVARCHAR(64)NOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);并发测试需要覆盖50、100、200 并发线程持续取号并插入每事务单行插入每事务批量插入 1001000 行部分事务取号后主动回滚在线插入与历史主键导入并行重启连接池、切换节点或模拟故障后的继续写入。测试结束后执行SELECTCOUNT(*)AStotal_rows,COUNT(DISTINCTorder_id)ASdistinct_ids,MIN(order_id)ASmin_id,MAX(order_id)ASmax_idFROMt_order_pk_test;重复键验证SELECTorder_id,COUNT(*)FROMt_order_pk_testGROUPBYorder_idHAVINGCOUNT(*)1;不能只看 SQL 成功率还应记录 TPS、平均延迟、P95/P99、序列锁等待、失败事务数和重试次数。序列触发器改造流程图4. 方案实施4.1 方案选择订单系统常见三种目标方案方案优点风险适用建议保留序列 触发器与旧逻辑接近改动较小触发器隐式、排查链路长大量旧应用依赖空主键插入时序列 列默认值逻辑更直观显式主键仍可导入应用若显式传NULL需验证推荐作为多数表的改造方向身份列/自增列DDL 简洁ORM 支持较好历史值装载、重置和回退更复杂新表或调用链简单的表不建议全库一刀切改成身份列。订单主表、明细表和接口表应分别评估。4.2 目标端保留序列根据实际 KingbaseES 版本和兼容模式确认语法。通用思路如下CREATESEQUENCE oms.seq_order_idSTARTWITH2000000000INCREMENTBY1MINVALUE1MAXVALUE999999999999999999CACHE100NOCYCLE;金仓文档说明创建序列后可使用nextval、currval和setval。目标表可将序列调用设置为默认值CREATETABLEoms.t_order(order_idNUMERIC(19,0)DEFAULTnextval(oms.seq_order_id::regclass)PRIMARYKEY,order_noVARCHAR(64)NOTNULL,customer_idNUMERIC(19,0)NOTNULL,amountNUMERIC(18,2)NOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);该方案的关键优点是普通插入不传order_id时自动取号历史迁移显式传入order_id时保留原值主键生成依赖关系可从列默认值中看到比触发器更容易排查。但应用显式传入NULL的行为必须实测因为“省略列”和“传入 NULL”并不总是等价。4.3 保留触发器时的改造若旧应用大量依赖触发器可先保持调用方式降低首轮迁移风险。示意代码CREATEORREPLACEFUNCTIONoms.fn_bi_order_id()RETURNStriggerAS$$BEGINIFNEW.order_idISNULLTHENNEW.order_id :nextval(oms.seq_order_id::regclass);ENDIF;RETURNNEW;END;$$LANGUAGEplpgsql;CREATETRIGGERtrg_bi_order_id BEFOREINSERTONoms.t_orderFOR EACH ROWEXECUTEFUNCTIONoms.fn_bi_order_id();不同 KingbaseES 版本、数据库模式和兼容配置对过程语言、触发器语法可能存在差异必须以项目环境实际执行结果为准不能仅复制示例。4.4 历史装载与序列校准历史数据迁移完成后先得到所有相关端的最大值SELECTMAX(order_id)FROMoms.t_order;目标端校准示意SELECTsetval(oms.seq_order_id::regclass,(SELECTMAX(order_id)FROMoms.t_order)10000,false);第三个参数的具体语义需要按实际版本文档验证。校准后必须立即检查下一值SELECTnextval(oms.seq_order_id::regclass);并确认它大于所有已用值。不要在没有停写或没有纳入增量最大值的情况下直接执行MAX(id)1否则查询结束后新增的 Oracle 订单仍可能占用更高主键。4.5 灰度期间使用独立高位号段为了保证回退灰度阶段可让目标端使用一个与 Oracle 当前号段不重叠的高位区间例如Oracle 当前主键1,000,000,000 1,987,654,321 金仓灰度号段 8,000,000,000 起这样有三点好处双端生成的订单不会直接冲突能快速识别订单由哪一端创建回退时可按号段导出金仓新增订单。前提是 Oracle 主键字段和所有下游系统都能容纳该高位范围。号段设计必须检查NUMBER(p,0)或目标类型上限Javalong、前端 JavaScript 数字精度消息队列字段类型日志、报表和第三方接口分库分表路由算法。4.6 权限改造业务账号至少要具备表的INSERT权限调用序列nextval所需权限执行触发器函数所需权限对相关模式的必要访问权。权限验证应使用真实业务账号而不是 DBA 账号SETROLE oms_app_role;INSERTINTOoms.t_order(order_no,customer_id,amount)VALUES(MIG_TEST_001,10001,88.60);SELECTorder_id,order_noFROMoms.t_orderWHEREorder_noMIG_TEST_001;5. 结果对比5.1 对象级校验迁移前后对照以下属性序列名称 所属模式 起始值 步长 最小值/最大值 是否循环 缓存大小 顺序属性 拥有者与权限 引用该序列的默认值 引用该序列的触发器 应用中的显式取号 SQL形成对象改造清单对象源端方式目标端方式验证结果SEQ_ORDER_IDOracle 序列CACHE 100KingbaseES 序列CACHE 100下一值高于历史最大值TRG_BI_ORDER_ID空值时取号改为列默认值或目标触发器显式历史主键不被覆盖T_ORDER.ORDER_IDNUMBER(19,0)NUMERIC(19,0)主键范围一致Java 下单接口selectKey保留或改为返回生成键并发回归通过批量导入显式主键显式主键主从关联一致5.2 数据级校验主表验证SELECTCOUNT(*)ASrow_count,COUNT(DISTINCTorder_id)ASdistinct_id_count,MIN(order_id)ASmin_id,MAX(order_id)ASmax_id,SUM(CASEWHENorder_idISNULLTHEN1ELSE0END)ASnull_id_countFROMoms.t_order;主从完整性验证SELECTCOUNT(*)ASorphan_countFROMoms.t_order_item iLEFTJOINoms.t_order oONo.order_idi.order_idWHEREo.order_idISNULL;订单号和主键映射验证SELECTorder_no,COUNT(*)FROMoms.t_orderGROUPBYorder_noHAVINGCOUNT(*)1;分批迁移校验SELECTmigration_batch,COUNT(*)ASrow_count,MIN(order_id)ASmin_id,MAX(order_id)ASmax_idFROMoms.t_orderGROUPBYmigration_batchORDERBYmigration_batch;5.3 并发验证结果模板以下为结果记录模板不应伪装成真实项目数据并发数持续时间成功插入重复主键P95 延迟结论50待实测待实测必须为 0待实测待填写100待实测待实测必须为 0待实测待填写200待实测待实测必须为 0待实测待填写正式投稿时应附上压测命令、线程数、连接池参数、服务器配置、执行时间、TPS、P95/P99 和数据库等待事件截图。并发验证矩阵5.4 切换门禁正式切换前建议设定以下硬门禁目标端重复主键数为 0主键空值数为 0主从孤儿记录数为 0目标序列下一值高于两端所有已用值历史显式主键插入不会被默认值或触发器覆盖业务账号可正常取号且无多余高权限50、100、200 并发测试均无重复键订单创建、取消、支付、拆单、合单和补单流程全部通过灰度号段可完整导出并回写 Oracle回退演练已完成。6. 风险与复盘6.1 灰度切换步骤冻结 Oracle 序列、触发器和主键列的结构变更记录源端所有序列定义和依赖对象执行历史全量迁移显式保留原主键完成目标端序列校准启动增量同步并持续比较两端最大主键使用独立高位号段开放少量金仓写入对灰度订单执行主从、支付和消息链路核对达到门禁后停止 Oracle 写入完成最后增量追平并统一目标端序列保留 Oracle 和差异流水进入回退观察窗口。灰度切换与回退路径图6.2 回退方案回退触发条件可以包括出现任意重复主键目标序列下一值进入历史数据区间主从关联出现孤儿记录高位号段被下游接口截断或转成科学计数法应用部分节点仍从 Oracle 取号形成双重生成并发性能明显低于迁移门禁增量同步无法在窗口内追平。回退步骤1. 关闭金仓订单写入口记录最后成功事务时间。 2. 冻结目标序列导出灰度号段内新增订单及所有子表记录。 3. 校验这些主键是否在 Oracle 字段、应用和接口范围内。 4. 按主表到子表顺序回放 Oracle避免外键失败。 5. 将 Oracle 序列校准到回放后最大主键之上。 6. 核对订单、明细、支付、日志和消息数量。 7. 恢复 Oracle 写入金仓改为只读排查环境。回退前不要尝试“回收”已经发出但未使用的序列值。序列值空洞不影响唯一性强行复用反而可能与延迟事务、消息重放或缓存中的旧值冲突。6.3 关键风险清单风险一迁移工具只迁移表遗漏序列和触发器。应把序列、默认值、触发器、函数、权限和应用 SQL 作为一个完整依赖组验收。风险二目标触发器覆盖历史主键。触发器必须只在主键为空时取号历史装载需专项验证。风险三MAX(id)1在在线环境下失效。校准过程必须冻结写入或把增量队列中的最大值纳入计算。风险四缓存空洞被误判为数据丢失。用业务订单号、状态和行数对账不用主键连续性对账。风险五改成身份列后回退困难。身份列方案要验证显式历史值插入、生成器重置和回写源端。风险六高位灰度号段超出下游精度。尤其要检查 JavaScript、Excel、JSON 消费方和第三方接口。风险七应用仍保留旧的NEXTVALSQL。数据库改成默认值后应用若继续提前取号可能产生更多空洞或两次取号。6.4 项目复盘本次实践最重要的认识是主键迁移不是一个序列 DDL 的迁移而是一条完整的“生成—传递—存储—关联—同步—回退”链路改造。一个可审计的交付包至少应包含Oracle 序列清单触发器和依赖对象清单应用取号 SQL 和 ORM 配置清单目标端对象改造脚本历史最大主键和安全起点计算记录并发压测脚本与结果主从完整性校验 SQL灰度号段设计切换门禁回退演练记录。只有当这些证据全部闭环才能证明迁移后的订单 ID 不仅“能生成”而且在高并发、历史装载、故障恢复和回退场景下都保持唯一、稳定和可追溯。附录 A对象改造脚本模板-- 1. 创建目标序列CREATESEQUENCE oms.seq_order_idSTARTWITH8000000000INCREMENTBY1MINVALUE1MAXVALUE999999999999999999CACHE100NOCYCLE;-- 2. 设置列默认值ALTERTABLEoms.t_orderALTERCOLUMNorder_idSETDEFAULTnextval(oms.seq_order_id::regclass);-- 3. 历史装载后校准SELECTsetval(oms.seq_order_id::regclass,(SELECTMAX(order_id)10000FROMoms.t_order),false);-- 4. 验证下一值SELECTnextval(oms.seq_order_id::regclass);-- 5. 验证重复与空值SELECTCOUNT(*)-COUNT(DISTINCTorder_id)ASduplicate_count,SUM(CASEWHENorder_idISNULLTHEN1ELSE0END)ASnull_countFROMoms.t_order;附录 B上线检查清单已扫描全部序列及参数。已扫描全部触发器、默认值和身份列。已搜索应用中的NEXTVAL、selectKey和 ORM 生成策略。已确认历史数据插入不会被触发器覆盖。已计算两端最大已用主键和增量队列最大值。已校准目标序列并验证下一值。已验证序列缓存空洞符合业务预期。已验证业务账号权限。已完成并发、回滚和批量插入测试。已完成主从孤儿数据检查。已验证高位号段的全链路兼容性。已完成灰度切换和回退演练。转载自https://blog.csdn.net/u014727709/article/details/163172999欢迎 点赞✍评论⭐收藏欢迎指正