文章目录
-
- 每日一句正能量
- 导读
- [1. 背景与问题](#1. 背景与问题)
- [2. 环境与数据](#2. 环境与数据)
-
- [2.1 验证环境](#2.1 验证环境)
- [2.2 源端对象扫描](#2.2 源端对象扫描)
- [3. 复现过程](#3. 复现过程)
-
- [3.1 复现"历史主键被触发器覆盖"](#3.1 复现“历史主键被触发器覆盖”)
- [3.2 复现"序列起点低于历史最大值"](#3.2 复现“序列起点低于历史最大值”)
- [3.3 复现"序列空洞被误判为丢单"](#3.3 复现“序列空洞被误判为丢单”)
- [3.4 复现并发冲突](#3.4 复现并发冲突)
- [4. 方案实施](#4. 方案实施)
-
- [4.1 方案选择](#4.1 方案选择)
- [4.2 目标端保留序列](#4.2 目标端保留序列)
- [4.3 保留触发器时的改造](#4.3 保留触发器时的改造)
- [4.4 历史装载与序列校准](#4.4 历史装载与序列校准)
- [4.5 灰度期间使用独立高位号段](#4.5 灰度期间使用独立高位号段)
- [4.6 权限改造](#4.6 权限改造)
- [5. 结果对比](#5. 结果对比)
-
- [5.1 对象级校验](#5.1 对象级校验)
- [5.2 数据级校验](#5.2 数据级校验)
- [5.3 并发验证结果模板](#5.3 并发验证结果模板)
- [5.4 切换门禁](#5.4 切换门禁)
- [6. 风险与复盘](#6. 风险与复盘)
-
- [6.1 灰度切换步骤](#6.1 灰度切换步骤)
- [6.2 回退方案](#6.2 回退方案)
- [6.3 关键风险清单](#6.3 关键风险清单)
- [6.4 项目复盘](#6.4 项目复盘)
- [附录 A:对象改造脚本模板](#附录 A:对象改造脚本模板)
- [附录 B:上线检查清单](#附录 B:上线检查清单)

每日一句正能量
世界上最可怕的事,是你把别人当成了朋友,别人并没拿你当朋友。
人际关系中最深的孤独不是没有朋友,而是你付出的真心被对方视为寻常,甚至利用。这种不对等的情感投射,会让人产生自我怀疑。友谊需要双向确认,单方面的热情只是一厢情愿的冒险。
导读
本文以订单系统迁移为背景,讨论 Oracle 序列、
BEFORE INSERT触发器、应用显式取号和目标端自增机制之间的兼容改造。示例强调可复现验证,不把"SQL 能执行"当作迁移完成,而是把对象依赖、并发唯一性、历史数据装载、生成器校准和可回退性一并纳入验收。
1. 背景与问题
订单系统的主键生成看起来简单:插入一行,得到一个新的订单号。但在真实系统里,主键通常贯穿订单主表、明细、支付流水、物流任务、消息队列和外部接口。迁移时只要生成机制有一处改错,就可能出现重复键、子表找不到主表、历史订单被覆盖,甚至回退时新订单无法写回 Oracle。
本次演练中的 Oracle 源库存在三种取号路径:
sql
-- 路径一:应用显式取号
SELECT seq_order_id.NEXTVAL FROM dual;
-- 路径二:插入时不传主键,由触发器赋值
INSERT INTO t_order(order_no, customer_id, amount)
VALUES (:order_no, :customer_id, :amount);
-- 路径三:批量导入时显式传入历史主键
INSERT INTO t_order(order_id, order_no, customer_id, amount)
VALUES (:order_id, :order_no, :customer_id, :amount);
对应的 Oracle 对象如下:
sql
CREATE SEQUENCE seq_order_id
START WITH 100000000
INCREMENT BY 1
MAXVALUE 999999999999999999
NOCYCLE
CACHE 100
NOORDER;
CREATE OR REPLACE TRIGGER trg_bi_order_id
BEFORE INSERT ON t_order
FOR EACH ROW
WHEN (NEW.order_id IS NULL)
BEGIN
SELECT seq_order_id.NEXTVAL
INTO :NEW.order_id
FROM dual;
END;
/
真正的迁移难点不在语法替换,而在于以下问题:
- 目标端是否继续使用"序列+触发器",还是改为列默认值或身份列;
- 迁移历史数据时,触发器是否会覆盖原主键;
- 全量迁移和在线增量同时进行时,目标序列从哪里起步;
- 应用是否同时存在
NEXTVAL、触发器和 ORM 自动生成三套逻辑; - 序列缓存导致的空洞是否会被业务错误地判断为"丢单";
- 切换后产生的新主键能否在回退时写回 Oracle;
- 业务账号在目标端是否具有正确的序列和触发器权限。
Oracle 官方文档说明,序列通过 NEXTVAL 生成值,可配置起始值、步长、最大值、循环和缓存;缓存可以提升取号性能,但实例故障时未使用的缓存值可能丢失,因此序列天然不保证连续。KingbaseES 官方文档同样提供 CREATE SEQUENCE,并可通过 nextval、currval 和 setval 操作序列。两端功能相似,但对象语法、默认值表达式、权限模型和兼容模式仍应按实际版本验证。

主键生成机制兼容评估图
2. 环境与数据
2.1 验证环境
| 项目 | 源端 | 目标端 |
|---|---|---|
| 数据库 | Oracle 19c | KingbaseES V8 兼容环境 |
| 应用 | Java 订单服务 | 同一套迁移分支 |
| 数据规模 | 订单主表约 5000 万行 | 全量副本 |
| 峰值写入 | 约 3000 单/秒 | 按相同压测模型验证 |
| 主键类型 | NUMBER(19,0) |
NUMERIC(19,0) 或等价整数型 |
| 迁移方式 | 全量 + 增量同步 | 灰度切换 |
版本、数据量和性能指标必须替换为真实项目数据。本文数值仅用于展示验证方法。
2.2 源端对象扫描
先扫描序列:
sql
SELECT sequence_owner,
sequence_name,
min_value,
max_value,
increment_by,
cycle_flag,
order_flag,
cache_size,
last_number
FROM dba_sequences
WHERE sequence_owner = 'OMS'
ORDER BY sequence_name;
LAST_NUMBER 不能直接等同于"最后一个已使用值",尤其在启用缓存时。它只能作为辅助信息,真正安全的起点还要结合业务表最大主键和在线增量。
扫描触发器及状态:
sql
SELECT owner,
trigger_name,
table_name,
triggering_event,
trigger_type,
status
FROM dba_triggers
WHERE owner = 'OMS'
ORDER BY table_name, trigger_name;
提取触发器定义:
sql
SELECT owner, name, type, line, text
FROM dba_source
WHERE owner = 'OMS'
AND type = 'TRIGGER'
ORDER BY name, line;
扫描列默认值和身份列:
sql
SELECT owner,
table_name,
column_name,
data_default,
identity_column
FROM dba_tab_columns
WHERE owner = 'OMS'
AND (data_default IS NOT NULL OR identity_column = 'YES')
ORDER BY table_name, column_id;
除了数据库对象,还要搜索应用代码和配置:
text
NEXTVAL
CURRVAL
selectKey
useGeneratedKeys
GenerationType.SEQUENCE
GenerationType.IDENTITY
@SequenceGenerator
before insert
order_id
如果同一张表既有触发器,又有 MyBatis selectKey 或 JPA @SequenceGenerator,就必须明确谁负责生成主键。双重取号通常不会直接重复,但会造成大量空洞、逻辑分叉和难以回退。
3. 复现过程
3.1 复现"历史主键被触发器覆盖"
错误触发器常写成无条件赋值:
sql
CREATE OR REPLACE TRIGGER trg_bi_order_id
BEFORE INSERT ON t_order
FOR EACH ROW
BEGIN
:NEW.order_id := seq_order_id.NEXTVAL;
END;
/
当迁移工具显式插入历史主键时,该触发器仍然生成新值,导致:
- 目标端主键与 Oracle 不一致;
- 明细表仍引用旧主键,外键装载失败;
- 外部系统按历史订单 ID 查询不到数据;
- 回退时无法将目标端数据和源端订单关联。
正确原则是"仅在主键为空时取号":
sql
IF :NEW.order_id IS NULL THEN
:NEW.order_id := seq_order_id.NEXTVAL;
END IF;
迁移历史数据时还可以临时禁用触发器,但必须记录禁用窗口,并在启用前完成序列校准。
3.2 复现"序列起点低于历史最大值"
假设源表最大订单主键为:
sql
SELECT MAX(order_id) FROM t_order;
-- 结果:1087654321
目标端却按旧 DDL 创建:
sql
CREATE SEQUENCE seq_order_id START WITH 100000000;
历史数据导入后,一旦在线写入,序列迟早会撞上已经存在的主键。这个问题在小规模测试中可能不出现,因为序列尚未增长到冲突区间;正式运行一段时间后才爆发,风险很高。
安全起点至少满足:
text
目标序列下一值 > max(
Oracle 当前最大主键,
目标端已迁移最大主键,
增量同步队列中的最大主键,
灰度期间目标端已生成最大主键
)
建议额外预留安全步长,例如取上述最大值加 10000,但预留量必须结合峰值写入和同步延迟确定,不能随意拍脑袋。
3.3 复现"序列空洞被误判为丢单"
执行:
sql
SELECT seq_order_id.NEXTVAL FROM dual;
ROLLBACK;
SELECT seq_order_id.NEXTVAL FROM dual;
序列值通常不会因事务回滚而回退。启用缓存后,数据库重启或节点故障也可能造成未使用号段丢失。因此:
- 序列负责唯一取号,不负责连续编号;
- 对账不能用"最大 ID - 最小 ID + 1"推导订单数量;
- 财务凭证号、发票号等需要连续性监管的编号,应采用独立业务规则,而不是直接依赖数据库序列;
- 监控应关注重复、越界和生成失败,不应把空洞本身当成数据库错误。
3.4 复现并发冲突
创建最小测试表:
sql
CREATE TABLE t_order_pk_test (
order_id NUMERIC(19,0) PRIMARY KEY,
order_no VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
并发测试需要覆盖:
- 50、100、200 并发线程持续取号并插入;
- 每事务单行插入;
- 每事务批量插入 100~1000 行;
- 部分事务取号后主动回滚;
- 在线插入与历史主键导入并行;
- 重启连接池、切换节点或模拟故障后的继续写入。
测试结束后执行:
sql
SELECT COUNT(*) AS total_rows,
COUNT(DISTINCT order_id) AS distinct_ids,
MIN(order_id) AS min_id,
MAX(order_id) AS max_id
FROM t_order_pk_test;
重复键验证:
sql
SELECT order_id, COUNT(*)
FROM t_order_pk_test
GROUP BY order_id
HAVING COUNT(*) > 1;
不能只看 SQL 成功率,还应记录 TPS、平均延迟、P95/P99、序列锁等待、失败事务数和重试次数。

序列触发器改造流程图
4. 方案实施
4.1 方案选择
订单系统常见三种目标方案:
| 方案 | 优点 | 风险 | 适用建议 |
|---|---|---|---|
| 保留序列 + 触发器 | 与旧逻辑接近,改动较小 | 触发器隐式、排查链路长 | 大量旧应用依赖空主键插入时 |
| 序列 + 列默认值 | 逻辑更直观,显式主键仍可导入 | 应用若显式传 NULL 需验证 |
推荐作为多数表的改造方向 |
| 身份列/自增列 | DDL 简洁,ORM 支持较好 | 历史值装载、重置和回退更复杂 | 新表或调用链简单的表 |
不建议全库一刀切改成身份列。订单主表、明细表和接口表应分别评估。
4.2 目标端保留序列
根据实际 KingbaseES 版本和兼容模式确认语法。通用思路如下:
sql
CREATE SEQUENCE oms.seq_order_id
START WITH 2000000000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 999999999999999999
CACHE 100
NO CYCLE;
金仓文档说明,创建序列后可使用 nextval、currval 和 setval。目标表可将序列调用设置为默认值:
sql
CREATE TABLE oms.t_order (
order_id NUMERIC(19,0)
DEFAULT nextval('oms.seq_order_id'::regclass)
PRIMARY KEY,
order_no VARCHAR(64) NOT NULL,
customer_id NUMERIC(19,0) NOT NULL,
amount NUMERIC(18,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
该方案的关键优点是:
- 普通插入不传
order_id时自动取号; - 历史迁移显式传入
order_id时保留原值; - 主键生成依赖关系可从列默认值中看到;
- 比触发器更容易排查。
但应用显式传入 NULL 的行为必须实测,因为"省略列"和"传入 NULL"并不总是等价。
4.3 保留触发器时的改造
若旧应用大量依赖触发器,可先保持调用方式,降低首轮迁移风险。示意代码:
sql
CREATE OR REPLACE FUNCTION oms.fn_bi_order_id()
RETURNS trigger
AS $$
BEGIN
IF NEW.order_id IS NULL THEN
NEW.order_id := nextval('oms.seq_order_id'::regclass);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_bi_order_id
BEFORE INSERT ON oms.t_order
FOR EACH ROW
EXECUTE FUNCTION oms.fn_bi_order_id();
不同 KingbaseES 版本、数据库模式和兼容配置对过程语言、触发器语法可能存在差异,必须以项目环境实际执行结果为准,不能仅复制示例。
4.4 历史装载与序列校准
历史数据迁移完成后,先得到所有相关端的最大值:
sql
SELECT MAX(order_id) FROM oms.t_order;
目标端校准示意:
sql
SELECT setval(
'oms.seq_order_id'::regclass,
(SELECT MAX(order_id) FROM oms.t_order) + 10000,
false
);
第三个参数的具体语义需要按实际版本文档验证。校准后必须立即检查下一值:
sql
SELECT nextval('oms.seq_order_id'::regclass);
并确认它大于所有已用值。不要在没有停写或没有纳入增量最大值的情况下直接执行 MAX(id)+1,否则查询结束后新增的 Oracle 订单仍可能占用更高主键。
4.5 灰度期间使用独立高位号段
为了保证回退,灰度阶段可让目标端使用一个与 Oracle 当前号段不重叠的高位区间,例如:
text
Oracle 当前主键:1,000,000,000 ~ 1,987,654,321
金仓灰度号段: 8,000,000,000 起
这样有三点好处:
- 双端生成的订单不会直接冲突;
- 能快速识别订单由哪一端创建;
- 回退时可按号段导出金仓新增订单。
前提是 Oracle 主键字段和所有下游系统都能容纳该高位范围。号段设计必须检查:
NUMBER(p,0)或目标类型上限;- Java
long、前端 JavaScript 数字精度; - 消息队列字段类型;
- 日志、报表和第三方接口;
- 分库分表路由算法。
4.6 权限改造
业务账号至少要具备:
- 表的
INSERT权限; - 调用序列
nextval所需权限; - 执行触发器函数所需权限;
- 对相关模式的必要访问权。
权限验证应使用真实业务账号,而不是 DBA 账号:
sql
SET ROLE oms_app_role;
INSERT INTO oms.t_order(order_no, customer_id, amount)
VALUES ('MIG_TEST_001', 10001, 88.60);
SELECT order_id, order_no
FROM oms.t_order
WHERE order_no = 'MIG_TEST_001';
5. 结果对比
5.1 对象级校验
迁移前后对照以下属性:
text
序列名称
所属模式
起始值
步长
最小值/最大值
是否循环
缓存大小
顺序属性
拥有者与权限
引用该序列的默认值
引用该序列的触发器
应用中的显式取号 SQL
形成对象改造清单:
| 对象 | 源端方式 | 目标端方式 | 验证结果 |
|---|---|---|---|
SEQ_ORDER_ID |
Oracle 序列,CACHE 100 | KingbaseES 序列,CACHE 100 | 下一值高于历史最大值 |
TRG_BI_ORDER_ID |
空值时取号 | 改为列默认值或目标触发器 | 显式历史主键不被覆盖 |
T_ORDER.ORDER_ID |
NUMBER(19,0) |
NUMERIC(19,0) |
主键范围一致 |
| Java 下单接口 | selectKey |
保留或改为返回生成键 | 并发回归通过 |
| 批量导入 | 显式主键 | 显式主键 | 主从关联一致 |
5.2 数据级校验
主表验证:
sql
SELECT COUNT(*) AS row_count,
COUNT(DISTINCT order_id) AS distinct_id_count,
MIN(order_id) AS min_id,
MAX(order_id) AS max_id,
SUM(CASE WHEN order_id IS NULL THEN 1 ELSE 0 END) AS null_id_count
FROM oms.t_order;
主从完整性验证:
sql
SELECT COUNT(*) AS orphan_count
FROM oms.t_order_item i
LEFT JOIN oms.t_order o
ON o.order_id = i.order_id
WHERE o.order_id IS NULL;
订单号和主键映射验证:
sql
SELECT order_no, COUNT(*)
FROM oms.t_order
GROUP BY order_no
HAVING COUNT(*) > 1;
分批迁移校验:
sql
SELECT migration_batch,
COUNT(*) AS row_count,
MIN(order_id) AS min_id,
MAX(order_id) AS max_id
FROM oms.t_order
GROUP BY migration_batch
ORDER BY migration_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 取号,形成双重生成;
- 并发性能明显低于迁移门禁;
- 增量同步无法在窗口内追平。
回退步骤:
text
1. 关闭金仓订单写入口,记录最后成功事务时间。
2. 冻结目标序列,导出灰度号段内新增订单及所有子表记录。
3. 校验这些主键是否在 Oracle 字段、应用和接口范围内。
4. 按主表到子表顺序回放 Oracle,避免外键失败。
5. 将 Oracle 序列校准到回放后最大主键之上。
6. 核对订单、明细、支付、日志和消息数量。
7. 恢复 Oracle 写入,金仓改为只读排查环境。
回退前不要尝试"回收"已经发出但未使用的序列值。序列值空洞不影响唯一性,强行复用反而可能与延迟事务、消息重放或缓存中的旧值冲突。
6.3 关键风险清单
风险一:迁移工具只迁移表,遗漏序列和触发器。
应把序列、默认值、触发器、函数、权限和应用 SQL 作为一个完整依赖组验收。
风险二:目标触发器覆盖历史主键。
触发器必须只在主键为空时取号,历史装载需专项验证。
风险三:MAX(id)+1 在在线环境下失效。
校准过程必须冻结写入,或把增量队列中的最大值纳入计算。
风险四:缓存空洞被误判为数据丢失。
用业务订单号、状态和行数对账,不用主键连续性对账。
风险五:改成身份列后回退困难。
身份列方案要验证显式历史值插入、生成器重置和回写源端。
风险六:高位灰度号段超出下游精度。
尤其要检查 JavaScript、Excel、JSON 消费方和第三方接口。
风险七:应用仍保留旧的 NEXTVAL SQL。
数据库改成默认值后,应用若继续提前取号,可能产生更多空洞或两次取号。
6.4 项目复盘
本次实践最重要的认识是:主键迁移不是一个序列 DDL 的迁移,而是一条完整的"生成---传递---存储---关联---同步---回退"链路改造。
一个可审计的交付包至少应包含:
- Oracle 序列清单;
- 触发器和依赖对象清单;
- 应用取号 SQL 和 ORM 配置清单;
- 目标端对象改造脚本;
- 历史最大主键和安全起点计算记录;
- 并发压测脚本与结果;
- 主从完整性校验 SQL;
- 灰度号段设计;
- 切换门禁;
- 回退演练记录。
只有当这些证据全部闭环,才能证明迁移后的订单 ID 不仅"能生成",而且在高并发、历史装载、故障恢复和回退场景下都保持唯一、稳定和可追溯。
附录 A:对象改造脚本模板
sql
-- 1. 创建目标序列
CREATE SEQUENCE oms.seq_order_id
START WITH 8000000000
INCREMENT BY 1
MINVALUE 1
MAXVALUE 999999999999999999
CACHE 100
NO CYCLE;
-- 2. 设置列默认值
ALTER TABLE oms.t_order
ALTER COLUMN order_id
SET DEFAULT nextval('oms.seq_order_id'::regclass);
-- 3. 历史装载后校准
SELECT setval(
'oms.seq_order_id'::regclass,
(SELECT MAX(order_id) + 10000 FROM oms.t_order),
false
);
-- 4. 验证下一值
SELECT nextval('oms.seq_order_id'::regclass);
-- 5. 验证重复与空值
SELECT COUNT(*) - COUNT(DISTINCT order_id) AS duplicate_count,
SUM(CASE WHEN order_id IS NULL THEN 1 ELSE 0 END) AS null_count
FROM oms.t_order;
附录 B:上线检查清单
- 已扫描全部序列及参数。
- 已扫描全部触发器、默认值和身份列。
- 已搜索应用中的
NEXTVAL、selectKey和 ORM 生成策略。 - 已确认历史数据插入不会被触发器覆盖。
- 已计算两端最大已用主键和增量队列最大值。
- 已校准目标序列并验证下一值。
- 已验证序列缓存空洞符合业务预期。
- 已验证业务账号权限。
- 已完成并发、回滚和批量插入测试。
- 已完成主从孤儿数据检查。
- 已验证高位号段的全链路兼容性。
- 已完成灰度切换和回退演练。
转载自:https://blog.csdn.net/u014727709/article/details/163172999
欢迎 👍点赞✍评论⭐收藏,欢迎指正