接活:一套不肯停的老系统
去年九月,我被派到某地方金融客户现场,活儿是订单交易系统的数据库国产化替换。
源库一台 Oracle 11g,单机,跑了八年。核心交易表两亿多行,存储过程加触发器小三百个。到的那天下午,客户信息中心主任在会议室把话说得很直白:信创是硬指标,年内必须切到金仓数据库,业务不能停,一分钟都不行。
我当时没吱声,心里其实打鼓。这套系统历史包袱太重,Oracle 特性用得又深又杂,谁敢拍胸脯说迁过去就完事?但活儿派下来了,硬着头皮也得上。
目录
-
- 接活:一套不肯停的老系统
- 摸底:越摸越发凉
- [方案:KDTS 打头,KFS 兜底](#方案:KDTS 打头,KFS 兜底)
- 真刀真枪:四个让我后背发凉的瞬间
-
- [坑一:DATE 类型把时分秒弄丢了](#坑一:DATE 类型把时分秒弄丢了)
- 坑二:序列自增主键的水土不服
- 坑三:隐式转换把索引干废了
- [坑四:NULLIF 一个不起眼的参数差异](#坑四:NULLIF 一个不起眼的参数差异)
- 类型映射和数据校验:最该抠的两个地方
- 落地效果
- 写在最后
摸底:越摸越发凉
正式动手前,我关在机房里花了两天摸源库。怎么说呢,越摸心里越发凉。
自增主键全是"序列 + 触发器"那套老办法撑着,光序列就四十多个。PL/SQL 里 NVL、DECODE、SYSDATE 满天飞,还有几个存储过程靠 Package 级别的全局变量传值--这玩意儿我最怕,状态飘忽,排查起来要命。历史代码里隐式转换更是一抓一大把,字符串列直接跟数字 =,在 Oracle 上靠它那套宽松的类型推断将就了八年。
再就是割接窗口卡得死。客户给的窗口是凌晨两点到六点,四个小时。这四个小时里得追平增量、校验数据、切应用、跑冒烟,中间随便卡一下,天亮交易一开就是生产事故,得上报的那种。
方案:KDTS 打头,KFS 兜底
方案不复杂,我自己画了几个版本,最后定的就三步。
第一步,KDTS 做全量离线迁移,表结构、对象定义、存量数据一次性搬过来。第二步,KFS 做增量同步,存量搬完后它靠日志解析把源库持续产生的增量同步到金仓,撑起割接前的双轨运行。第三步是割接当晚,停应用、追平最后那点增量、校验数据、切连接串、起来冒烟。
KDTS 是必须上的,没得选。它支持自定义类型映射、并行迁移,搬完还能做源端和目的端的数据对比。金融场景数据一条都不能错,这功能对我来说就是定心丸。KFS 负责把割接窗口里那点增量兜住。俩工具一搭,四个小时我才敢交差。
| 迁移能力 | KDTS | KFS |
|---|---|---|
| 定位 | 离线全量迁移 | 异构实时同步 |
| 对象定义 | 支持所有对象定义迁移 | 仅表结构和主键 |
| 同步方式 | 一次同步 | 持续同步,支持断点续传 |
| 数据对比 | 迁移后支持源/目的数据对比 | 不涉及 |
| 在迁移中的角色 | 主力,必用 | 兜底增量,支撑双轨 |
整个方案串起来,我画了张图,大体就这么条路:
#mermaid-svg-Gma7PpLkpybrRuCl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gma7PpLkpybrRuCl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gma7PpLkpybrRuCl .error-icon{fill:#552222;}#mermaid-svg-Gma7PpLkpybrRuCl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gma7PpLkpybrRuCl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gma7PpLkpybrRuCl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl .marker.cross{stroke:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gma7PpLkpybrRuCl p{margin:0;}#mermaid-svg-Gma7PpLkpybrRuCl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster-label text{fill:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster-label span{color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster-label span p{background-color:transparent;}#mermaid-svg-Gma7PpLkpybrRuCl .label text,#mermaid-svg-Gma7PpLkpybrRuCl span{fill:#333;color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .node rect,#mermaid-svg-Gma7PpLkpybrRuCl .node circle,#mermaid-svg-Gma7PpLkpybrRuCl .node ellipse,#mermaid-svg-Gma7PpLkpybrRuCl .node polygon,#mermaid-svg-Gma7PpLkpybrRuCl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .rough-node .label text,#mermaid-svg-Gma7PpLkpybrRuCl .node .label text,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape .label,#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape .label{text-anchor:middle;}#mermaid-svg-Gma7PpLkpybrRuCl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .rough-node .label,#mermaid-svg-Gma7PpLkpybrRuCl .node .label,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape .label,#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape .label{text-align:center;}#mermaid-svg-Gma7PpLkpybrRuCl .node.clickable{cursor:pointer;}#mermaid-svg-Gma7PpLkpybrRuCl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl .arrowheadPath{fill:#333333;}#mermaid-svg-Gma7PpLkpybrRuCl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Gma7PpLkpybrRuCl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Gma7PpLkpybrRuCl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gma7PpLkpybrRuCl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Gma7PpLkpybrRuCl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gma7PpLkpybrRuCl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Gma7PpLkpybrRuCl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster text{fill:#333;}#mermaid-svg-Gma7PpLkpybrRuCl .cluster span{color:#333;}#mermaid-svg-Gma7PpLkpybrRuCl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Gma7PpLkpybrRuCl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Gma7PpLkpybrRuCl rect.text{fill:none;stroke-width:0;}#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape p,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Gma7PpLkpybrRuCl .icon-shape .label rect,#mermaid-svg-Gma7PpLkpybrRuCl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Gma7PpLkpybrRuCl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Gma7PpLkpybrRuCl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Gma7PpLkpybrRuCl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段三 割接
停应用追平增量
数据校验
切连接串
冒烟上线
阶段二 迁移
KDTS 全量搬迁
结构+对象+存量
KFS 增量同步
双轨运行对账
阶段一 评估
源库摸底
兼容性扫描
动手之前我先干了一件事--跑扫描脚本,把源库里可能踩雷的写法先揪出来。这步现在回想起来觉得特别值,省了后面不知道多少返工:
sql
-- 迁移前:扫描源库存储过程/函数源码里的兼容性风险点
-- 拿 all_source 视图,用正则匹配几类常见雷区
WITH risk_patterns AS (
SELECT '隐式转换(字符串列=数字)' AS risk, '\b(\w+_no|\w+_code|\w+_id)\s*=\s*[0-9]' AS pattern
UNION ALL
SELECT 'NULLIF第一参为NULL风险', 'NULLIF\s*\(\s*NULL' AS pattern
UNION ALL
SELECT '索引列上套了函数', 'WHERE\s+(UPPER|LOWER|SUBSTR|TO_CHAR)\s*\(' AS pattern
UNION ALL
SELECT '依赖Package全局变量', '\w+\.(get|set)_\w+\s*\(' AS pattern
UNION ALL
SELECT '外连接(+)老写法', '\(+\)' AS pattern
)
SELECT s.owner, s.name, s.type, s.line, r.risk,
LEFT(s.text, 120) AS snippet
FROM all_source s
JOIN risk_patterns r ON s.text ~ r.pattern
WHERE s.owner NOT IN ('SYS','SYSTEM','SYSMAN')
AND s.type IN ('PROCEDURE','FUNCTION','PACKAGE','PACKAGE BODY','TRIGGER')
ORDER BY s.owner, s.name, s.line;
一跑出来三百多条。我盯着那个结果列表看了半天,心凉了半截,但好歹是提前知道了雷在哪,对着清单逐个过,比两眼一抹黑迁完再排查强太多。
真刀真枪:四个让我后背发凉的瞬间
方案归方案,真迁起来坑是一个接一个往外蹦。我挑四个印象最深的讲。
坑一:DATE 类型把时分秒弄丢了
这坑差点出事。
割接前那个周三晚上,我在校验数据,账面金额对上了,正准备收拾东西回家。业务方一个电话打过来,说订单明细按时间段导出的报表,时间全是零点,没法看了。
我心里咯噔一下,困意全没了,赶紧回去查。
问题出在一张历史表上。Oracle 的 DATE 类型带时分秒,金仓数据库的 date 只精确到天。KDTS 默认会把 Oracle 的 DATE 映射成金仓的 timestamp,这规则本身没毛病。可偏偏这张表当年是手写的 DDL,负责迁移的同事图省事,照着字面 DATE 就建成了 date,数据一灌进去,时分秒全被截成 00:00:00。
好在发现得早,改列类型,重灌这张表,过去了。
但这事给我留了个教训,刻挺深:Oracle 的 DATE 和金仓的 date,名字一模一样,精度差着一截。类型映射必须盯死,别凭字面想当然。
sql
-- 修复:把误建成 date 的列改回 timestamp,时分别再丢
ALTER TABLE order_detail
ALTER COLUMN create_time TYPE TIMESTAMP WITHOUT TIME ZONE
USING create_time::TIMESTAMP WITHOUT TIME ZONE;
-- 顺手建个函数索引,把"按天查"也照顾到,省得以后又有人在索引列上套 ::date
CREATE INDEX IF NOT EXISTS idx_order_detail_day
ON order_detail ((create_time::DATE));
坑二:序列自增主键的水土不服
Oracle 那套"序列 + 触发器"自增方案,迁过来本身能跑。金仓数据库对序列、触发器,连 DUAL 表、ROWNUM 伪列都做了兼容,这点挺省心。
但坑不在数据库,在应用代码里。
有些老 SQL 显式调了 seq_order.nextval 拿主键,迁过来序列还在,接着用没毛病。可有个存储过程的触发器逻辑写得有 bug,新库里 nextval 取出来的值跟已有数据撞了,主键冲突直接报错。我查了半天,最后发现是触发器里 SELECT ... INTO 没处理好空值--老库上数据巧了没踩到,新库一换分布就现形了。
我本来想在老路上缝缝补补,改改触发器得了。后来一琢磨不行,这玩意儿以后就是个雷,指不定哪天又炸。索性把核心交易表的自增主键统一改成 IDENTITY 列,触发器那套老逻辑整个请出去。
这里有个关键点容易翻车:新序列的起始值得接上老序列当前值,不然接着往上发还是会撞。我第一次没注意,建完表插数据就报主键冲突,愣了一秒才反应过来。
sql
-- 把"序列+触发器"老方案整体迁到 IDENTITY 列
-- 重点:IDENTITY 起始值要接上老序列当前值,否则主键撞车
-- 1. 先看老序列走到哪了
SELECT last_number FROM user_sequences WHERE sequence_name = 'SEQ_ORDER';
-- 假设查出来是 200003456
-- 2. 建新表,id 直接用 IDENTITY,起始值接老序列 +1
CREATE TABLE orders_new (
id BIGINT GENERATED ALWAYS AS IDENTITY (START WITH 200003457) PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
amount NUMERIC(18,2) NOT NULL DEFAULT 0,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 3. 老数据原样导入,主键沿用老 id
INSERT INTO orders_new (id, order_no, amount, create_time)
SELECT id, order_no, amount, create_time FROM orders;
-- 4. 把序列推到比当前最大 id 更靠后的位置,留 1000 的缓冲
SELECT setval(pg_get_serial_sequence('orders_new','id'),
(SELECT MAX(id) FROM orders_new) + 1000, true);
-- 5. 确认无误,丢掉老触发器,切表名
DROP TRIGGER IF EXISTS trg_order ON orders;
ALTER TABLE orders RENAME TO orders_bak;
ALTER TABLE orders_new RENAME TO orders;
-- 6. 验一下自增是不是接着老 id 往下发
INSERT INTO orders_new (order_no, amount) VALUES ('SMOKE_001', 0.01)
RETURNING id;
坑三:隐式转换把索引干废了
这坑跟类型有关,但比第一个隐蔽。
订单表 order_no 是 VARCHAR,应用那边图省事,前端传进来的数字直接拼进 SQL:
sql
-- 别这么写:字符串列跟数字比,靠隐式转换对齐
SELECT * FROM orders WHERE order_no = 20240724001;
这写法在 Oracle 上跑了八年,相安无事。迁到金仓以后开始作妖--时灵时不灵,执行计划一会走索引一会全表扫,同一个接口测十次结果都不一定一样。
根子还是隐式转换。类型没对死,优化器在不同数据分布下拿的主意不一样,列一旦被函数包一层,索引就废了,跟没建一样。
改起来其实不难,难在改的是全应用的拼接逻辑,量大,几百个接口。我跟开发负责人商量了一下,分两步走:高频接口先改成参数化、类型对死,当周必须搞完;低频的排期慢慢改。底下这段是应用层该有的样子,参数绑死类型,别再让数据库去猜:
java
// 应用层:别拼 SQL,老老实实用 PreparedStatement 绑参
// 类型在绑定时就定死,隐式转换那点破事彻底没有可乘之机
public Order findByOrderNo(String orderNo) {
String sql = "SELECT id, order_no, amount, create_time "
+ "FROM orders WHERE order_no = ?"; // 字符串占位符
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, orderNo); // 绑死成 VARCHAR
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? mapRow(rs) : null;
}
} catch (SQLException e) {
throw new RuntimeException("查询订单失败 orderNo=" + orderNo, e);
}
}
坑四:NULLIF 一个不起眼的参数差异
这坑特别小,但真实存在。
NULLIF(value1, value2) 这函数,金仓数据库允许 value1 为 NULL,Oracle 不允许。就差这一点。
有个报表存储过程里写了类似 NULLIF(NULL, 0) 的逻辑。在 Oracle 上这个分支压根跑不到,所以八年了没人发现。迁过来以后行为变了,报表对不上,业务方又来找我。
这种点太细了,KDTS 不会替你发现,工具再强也管不到这种参数级别的语义差异。没办法,只能靠存储过程逐个回归测。后来我让团队专门整了份"函数差异清单",把这种参数层面的细微差别逐条标出来,回归的时候照着过一遍。
类型映射和数据校验:最该抠的两个地方
把前面这些坑归纳一下,迁移里最该抠的就两件事,一个是数据类型映射,一个是数据校验。
先说类型映射。这张表是我们在现场反复核对后定下来的规则,贴出来给后来人参考:
| Oracle 类型 | 金仓数据库类型 | 迁移注意点 |
|---|---|---|
| NUMBER(p,s) | numeric(p,s) | 精度标度照搬,别丢 |
| VARCHAR2(n) | varchar(n) | 字符/字节长度语义要确认 |
| DATE | timestamp | Oracle 的 DATE 带时分秒,必须映成 timestamp |
| CLOB / NCLOB | clob | 大对象,注意存储过程里 DBMS_LOB 调用 |
| BLOB | blob | 二进制大对象 |
| RAW | bytea | 二进制串 |
| LONG | text | 建议趁机改成 text |
再说数据校验。KDTS 自带源端和目的端的数据对比,但金融场景我不敢只信工具,又额外写了套校验脚本,对核心表做行数、金额合计、主键范围三项校验。割接当晚就靠它拍板。整个校验切换的流程,我画了下:
#mermaid-svg-AKJtquKEsdReX2UN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AKJtquKEsdReX2UN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AKJtquKEsdReX2UN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AKJtquKEsdReX2UN .error-icon{fill:#552222;}#mermaid-svg-AKJtquKEsdReX2UN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AKJtquKEsdReX2UN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AKJtquKEsdReX2UN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AKJtquKEsdReX2UN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AKJtquKEsdReX2UN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AKJtquKEsdReX2UN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AKJtquKEsdReX2UN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AKJtquKEsdReX2UN .marker.cross{stroke:#333333;}#mermaid-svg-AKJtquKEsdReX2UN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AKJtquKEsdReX2UN p{margin:0;}#mermaid-svg-AKJtquKEsdReX2UN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster-label text{fill:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster-label span{color:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster-label span p{background-color:transparent;}#mermaid-svg-AKJtquKEsdReX2UN .label text,#mermaid-svg-AKJtquKEsdReX2UN span{fill:#333;color:#333;}#mermaid-svg-AKJtquKEsdReX2UN .node rect,#mermaid-svg-AKJtquKEsdReX2UN .node circle,#mermaid-svg-AKJtquKEsdReX2UN .node ellipse,#mermaid-svg-AKJtquKEsdReX2UN .node polygon,#mermaid-svg-AKJtquKEsdReX2UN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .rough-node .label text,#mermaid-svg-AKJtquKEsdReX2UN .node .label text,#mermaid-svg-AKJtquKEsdReX2UN .image-shape .label,#mermaid-svg-AKJtquKEsdReX2UN .icon-shape .label{text-anchor:middle;}#mermaid-svg-AKJtquKEsdReX2UN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .rough-node .label,#mermaid-svg-AKJtquKEsdReX2UN .node .label,#mermaid-svg-AKJtquKEsdReX2UN .image-shape .label,#mermaid-svg-AKJtquKEsdReX2UN .icon-shape .label{text-align:center;}#mermaid-svg-AKJtquKEsdReX2UN .node.clickable{cursor:pointer;}#mermaid-svg-AKJtquKEsdReX2UN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AKJtquKEsdReX2UN .arrowheadPath{fill:#333333;}#mermaid-svg-AKJtquKEsdReX2UN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AKJtquKEsdReX2UN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AKJtquKEsdReX2UN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKJtquKEsdReX2UN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AKJtquKEsdReX2UN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKJtquKEsdReX2UN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AKJtquKEsdReX2UN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AKJtquKEsdReX2UN .cluster text{fill:#333;}#mermaid-svg-AKJtquKEsdReX2UN .cluster span{color:#333;}#mermaid-svg-AKJtquKEsdReX2UN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AKJtquKEsdReX2UN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AKJtquKEsdReX2UN rect.text{fill:none;stroke-width:0;}#mermaid-svg-AKJtquKEsdReX2UN .icon-shape,#mermaid-svg-AKJtquKEsdReX2UN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AKJtquKEsdReX2UN .icon-shape p,#mermaid-svg-AKJtquKEsdReX2UN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AKJtquKEsdReX2UN .icon-shape .label rect,#mermaid-svg-AKJtquKEsdReX2UN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AKJtquKEsdReX2UN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AKJtquKEsdReX2UN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AKJtquKEsdReX2UN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
02:00 停应用写入
KFS 追平最后增量
启动三项数据校验
行数比对
金额合计比对
主键范围比对
三项全平?
切 JDBC 连接串至金仓
定位差异表
回滚或重灌该表
冒烟测试
通过?
06:00 前对外放量
底下就是那段校验脚本,通过 DBLink 连源端,逐表对三项:
sql
-- 割接当晚:自动化数据校验脚本
-- 通过 DBLink(src_link) 连源端,逐表对 行数/金额/主键范围 三项
DO $$
DECLARE
v_tbl TEXT;
v_src_cnt BIGINT; v_dst_cnt BIGINT;
v_src_sum NUMERIC; v_dst_sum NUMERIC;
v_src_min BIGINT; v_dst_min BIGINT;
v_src_max BIGINT; v_dst_max BIGINT;
v_diff BIGINT;
BEGIN
FOREACH v_tbl IN ARRAY ARRAY['orders','order_detail','payment','refund'] LOOP
-- ① 行数
EXECUTE format('SELECT COUNT(*) FROM %I', v_tbl) INTO v_dst_cnt;
EXECUTE format('SELECT COUNT(*) FROM src_link.%I', v_tbl) INTO v_src_cnt;
v_diff := v_dst_cnt - v_src_cnt;
RAISE NOTICE '% 行数 源=% 目的=% 差异=%', v_tbl, v_src_cnt, v_dst_cnt, v_diff;
IF v_diff <> 0 THEN
RAISE EXCEPTION '% 行数不一致,差 %,立即停止割接', v_tbl, v_diff;
END IF;
-- ② 金额合计(一分钱都不能差)
EXECUTE format('SELECT COALESCE(SUM(amount),0) FROM %I', v_tbl) INTO v_dst_sum;
EXECUTE format('SELECT COALESCE(SUM(amount),0) FROM src_link.%I', v_tbl) INTO v_src_sum;
RAISE NOTICE '% 金额 源=% 目的=% 差额=%', v_tbl, v_src_sum, v_dst_sum, v_dst_sum - v_src_sum;
IF ABS(v_dst_sum - v_src_sum) > 0.01 THEN
RAISE EXCEPTION '% 金额不平,差 %', v_tbl, v_dst_sum - v_src_sum;
END IF;
-- ③ 主键范围
EXECUTE format('SELECT MIN(id),MAX(id) FROM %I', v_tbl) INTO v_dst_min, v_dst_max;
EXECUTE format('SELECT MIN(id),MAX(id) FROM src_link.%I', v_tbl) INTO v_src_min, v_src_max;
IF v_src_min <> v_dst_min OR v_src_max <> v_dst_max THEN
RAISE EXCEPTION '% 主键范围对不上,可能漏迁或重迁 (源 %..% / 目的 %..%)',
v_tbl, v_src_min, v_src_max, v_dst_min, v_dst_max;
END IF;
END LOOP;
RAISE NOTICE '===== 全部校验通过,可以切应用 =====';
END $$;
应用层切换也顺带说一句。JDBC 驱动换成金仓的,连接串和 Oracle 模式下的方言基本对得上。但有几个参数得调,prepareThreshold 和批次大小,这俩对批量插入性能影响挺大,我调了好几轮才找到合适的值:
java
// 金仓 JDBC 连接串(Oracle 兼容模式)
// prepareThreshold 控制几次执行后切换到服务端预编译,批量场景调大点
String url = "jdbc:kingbase8://10.0.0.12:54321/ks_db"
+ "?currentSchema=trade"
+ "&prepareThreshold=5"
+ "&defaultRowPrefetch=100";
// 批量插入别一条一条 round-trip,攒批一把送
try (PreparedStatement ps = conn.prepareStatement(
"INSERT INTO orders(order_no, amount, create_time) VALUES(?,?,?)")) {
conn.setAutoCommit(false);
for (Order o : batch) {
ps.setString(1, o.getOrderNo());
ps.setBigDecimal(2, o.getAmount());
ps.setTimestamp(3, Timestamp.from(o.getCreateTime()));
ps.addBatch();
}
ps.executeBatch();
conn.commit();
}
落地效果
前后跑了整一个月双轨。割接当晚四个小时内收工,核心交易表两亿行数据,零误差落库。我记得校验脚本最后打出"全部校验通过"那行 NOTICE 的时候,机房里几个人都长出一口气。
落地后的效果大致这样:
| 指标 | 迁移前(Oracle) | 迁移后(金仓数据库) |
|---|---|---|
| 核心查询平均响应 | 120ms | 95ms |
| 批量入库吞吐 | 基准 | 提升约 30%(调优后) |
| 割接窗口 | - | 4 小时内完成,零数据误差 |
| 双轨运行 | 无 | KFS 增量同步,割接前持续对账 |
| 存储过程兼容度 | - | 约 92% 直接可用,余下需适配 |
性能能稳中有升,说实话有点出乎我意料。后来想了想,很大程度上是迁移时顺手把那堆隐式转换、SELECT *、深分页的老毛病一起治了。说白了迁移就是个体检机会,趁着重构把历史技术债清一清,比单纯把数据搬过去值钱多了。
写在最后
干完这一票,我对"数据库迁移"这四个字的理解彻底变了。
它真不是把数据从 A 库导到 B 库那么简单,而是一次对整套系统的体检和重构。最容易栽跟头的,从来不是表结构、存储过程这种大块头--那些 KDTS 基本都能接住。真正咬人的,是 DATE 丢精度、隐式转换废索引、NULLIF 参数差一个 NULL 这种"名字差不多、行为差一截"的细节。
后来再碰到迁移后业务报异常,我基本都是按下面这棵决策树往下捋,八九不离十能快速定位:
#mermaid-svg-0tKcGP8hlc1OGCI7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0tKcGP8hlc1OGCI7 .error-icon{fill:#552222;}#mermaid-svg-0tKcGP8hlc1OGCI7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0tKcGP8hlc1OGCI7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .marker.cross{stroke:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0tKcGP8hlc1OGCI7 p{margin:0;}#mermaid-svg-0tKcGP8hlc1OGCI7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster-label text{fill:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster-label span{color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster-label span p{background-color:transparent;}#mermaid-svg-0tKcGP8hlc1OGCI7 .label text,#mermaid-svg-0tKcGP8hlc1OGCI7 span{fill:#333;color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node rect,#mermaid-svg-0tKcGP8hlc1OGCI7 .node circle,#mermaid-svg-0tKcGP8hlc1OGCI7 .node ellipse,#mermaid-svg-0tKcGP8hlc1OGCI7 .node polygon,#mermaid-svg-0tKcGP8hlc1OGCI7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .rough-node .label text,#mermaid-svg-0tKcGP8hlc1OGCI7 .node .label text,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .rough-node .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .node .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape .label,#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape .label{text-align:center;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node.clickable{cursor:pointer;}#mermaid-svg-0tKcGP8hlc1OGCI7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .arrowheadPath{fill:#333333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0tKcGP8hlc1OGCI7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0tKcGP8hlc1OGCI7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster text{fill:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 .cluster span{color:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0tKcGP8hlc1OGCI7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0tKcGP8hlc1OGCI7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape p,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0tKcGP8hlc1OGCI7 .icon-shape .label rect,#mermaid-svg-0tKcGP8hlc1OGCI7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0tKcGP8hlc1OGCI7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0tKcGP8hlc1OGCI7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0tKcGP8hlc1OGCI7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 时间全零点/报表错乱
主键冲突/写入报错
查询时灵时不灵
报表数值对不上
迁移后业务异常
症状?
DATE 类型映射
查是否误映成 date
序列起始值与触发器
查 IDENTITY 接续
隐式转换
查列与参数类型
函数参数差异
对照差异清单回归
改 timestamp 重灌
setval 推进 + 改 IDENTITY
应用参数化绑类型
逐存储过程回归
几条心得,留给后来人。
类型映射一定逐表逐列过,别信字面,尤其 DATE 和字符长度语义,这俩是重灾区。隐式转换这种历史债,迁移时能清就清,别留给生产去炸,到时候半夜被叫起来排查更难受。存储过程的函数差异 KDTS 发现不了,老老实实做回归测试,对照差异清单过。双轨期别省那点成本,KFS 增量同步加数据校验,是割接当晚敢按下回车键的底气。
最后说句实在话,金仓数据库在 Oracle 兼容上做得确实到位,序列、触发器、DUAL、ROWNUM 这些老特性基本都能接住,省了我们大量改造成本。但兼容归兼容,该较真的细节一个都不能放过。迁移这事,慢就是快,前头把坑踩明白了,后头割接才不至于手忙脚乱。