【金仓数据库征文】Oracle迁移金仓KES V9R3C18实战:从踩坑到丝滑的进阶之路
从Oracle 9i一路用到19c,可以说是看着Oracle长大的。但这几年信创改造的大潮来了,手里十几个核心系统陆续从Oracle往国产数据库迁。前前后后踩过不少坑,也摸索出了一套相对成熟的迁移方法论。
今天就来聊聊我们用金仓KES V9R3C18 MySQL兼容版替换Oracle的实战经历。有人可能会问:Oracle迁移为什么用MySQL兼容版?其实很简单——我们的应用层之前为了兼容MySQL做过一轮改造,代码里大量的MySQL语法,直接迁到Oracle兼容版反而要改更多。MySQL兼容版提供了一个很好的中间态,应用改动最小,迁移成本最低。
废话不多说,直接上干货。
一、迁移平滑验证:先跑通,再跑好
迁移这事儿,最怕的就是"拍脑袋决策、拍胸脯保证、拍屁股走人"。十几年的DBA经验告诉我,任何迁移都必须有完整的验证流程,一步都不能省。
1.1 环境准备
我们的源库是Oracle 12c R2,单实例+Data Guard容灾,承载着一套订单管理系统。目标库是金仓KES V9R3C18 MySQL兼容版,部署在同一机房的两台物理机上,做主备集群。
# 金仓安装后的基本信息检查
$ cd /opt/Kingbase/ES/V9/Server/bin
$ ./ksql -U system -d test -p 54321 -c "SELECT version();"
-- 验证数据库版本和兼容模式
SELECT version();
-- KingbaseES V9R3C18 ...
-- 查看兼容模式
SHOW dbcompatible;
-- mysql
1.2 结构迁移与数据校验
第一步是表结构迁移。我们没有直接用迁移工具生成的SQL,而是先人工过了一遍,把明显不兼容的地方先改掉。
-- Oracle 源表结构(示例)
CREATE TABLE ORD_ORDER (
ORDER_ID NUMBER(18) PRIMARY KEY,
ORDER_NO VARCHAR2(64) NOT NULL,
CUST_ID NUMBER(18) NOT NULL,
TOTAL_AMT NUMBER(12,2) DEFAULT 0,
ORDER_STATUS VARCHAR2(20) DEFAULT 'INIT',
REMARK VARCHAR2(500),
CREATE_TIME DATE DEFAULT SYSDATE,
UPDATE_TIME DATE,
CONSTRAINT UK_ORDER_NO UNIQUE (ORDER_NO)
);
-- 金仓适配后的表结构
CREATE TABLE ord_order (
order_id BIGINT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL,
cust_id BIGINT NOT NULL,
total_amt DECIMAL(12,2) DEFAULT 0,
order_status VARCHAR(20) DEFAULT 'INIT',
remark VARCHAR(500),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME,
UNIQUE KEY uk_order_no (order_no)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
类型映射这块我总结了一张表,给大家做个参考:
| Oracle类型 | 金仓MySQL兼容版 | 说明 |
|---|---|---|
| NUMBER(p,s) | DECIMAL(p,s) / BIGINT / INT | 根据精度选择,整数用整型更高效 |
| VARCHAR2(n) | VARCHAR(n) | 直接映射 |
| CHAR(n) | CHAR(n) | 直接映射 |
| DATE | DATETIME | Oracle DATE含时分秒,对应DATETIME |
| TIMESTAMP | DATETIME(6) | 毫秒精度对应 |
| CLOB | TEXT / MEDIUMTEXT / LONGTEXT | 按数据量选 |
| BLOB | BLOB / MEDIUMBLOB / LONGBLOB | 按数据量选 |
| RAW(16) | BINARY(16) / CHAR(32) | GUID类的可以存十六进制字符串 |
1.3 应用连通性验证
应用层我们用的是SpringBoot + MyBatis,之前连Oracle用的是ojdbc8驱动,切换到金仓只改了数据源配置:
# Oracle 配置(旧)
spring:
datasource:
driver-class-name: oracle.jdbc.OracleDriver
url: jdbc:oracle:thin:@//10.0.0.10:1521/orcl
username: order_user
password: xxxxxx
# 金仓配置(新)
spring:
datasource:
driver-class-name: com.kingbase8.Driver
url: jdbc:kingbase8://10.0.0.20:54321/test?useSSL=false&characterEncoding=utf8
username: order_user
password: xxxxxx
这里有个坑:金仓的默认端口是54321(PostgreSQL风格),不是Oracle的1521,也不是MySQL的3306。第一次配置的时候我下意识写成了5432,连了半天连不上,后来才反应过来是54321。
1.4 双写验证
光把数据迁过去还不够,必须验证数据一致性。我们用了DTS工具做全量+增量同步,然后跑了三天双写验证:
-- 每天抽样比对关键表数据量
SELECT 'ord_order' AS table_name, COUNT(*) FROM ord_order
UNION ALL
SELECT 'ord_order_item', COUNT(*) FROM ord_order_item
UNION ALL
SELECT 'ord_payment', COUNT(*) FROM ord_payment
UNION ALL
SELECT 'ord_refund', COUNT(*) FROM ord_refund;
-- 抽样比对具体数据(取最近100条订单)
SELECT order_id, order_no, total_amt, order_status, create_time
FROM ord_order
ORDER BY create_time DESC
LIMIT 100;
三天双写期间,我们每天做一次全量数据比对,差异率控制在万分之一以下(考虑到双写延迟),才敢切流量。
二、语法、类型与驱动适配:踩过的坑比路还长
这部分是迁移的重头戏。Oracle的语法太丰富了,什么CONNECT BY树形查询、MERGE INTO、DECODE、NVL、ROWNUM、分析函数……迁的时候一个一个冒出来。
2.1 分页查询:ROWNUM vs LIMIT
Oracle的分页用三层嵌套ROWNUM,金仓MySQL兼容版直接用LIMIT:
-- Oracle 分页写法
SELECT * FROM (
SELECT t.*, ROWNUM rn FROM (
SELECT * FROM ord_order ORDER BY create_time DESC
) t WHERE ROWNUM <= 30
) WHERE rn > 20;
-- 金仓 MySQL 兼容版写法
SELECT * FROM ord_order
ORDER BY create_time DESC
LIMIT 10 OFFSET 20;
如果用MyBatis的PageHelper插件,切换到MySQL兼容模式后,分页插件自动适配,代码完全不用改。这一点确实省了不少事。
2.2 空值处理:NVL vs IFNULL/COALESCE
-- Oracle
SELECT NVL(remark, '无备注') FROM ord_order;
-- 金仓(推荐用COALESCE,SQL标准函数,两边都能用)
SELECT COALESCE(remark, '无备注') FROM ord_order;
-- 或者
SELECT IFNULL(remark, '无备注') FROM ord_order;
经验之谈:迁移的时候尽量把Oracle特有的函数换成SQL标准函数,比如用COALESCE替代NVL,用CASE WHEN替代DECODE。这样以后再换数据库也不用改了。
2.3 DECODE vs CASE WHEN
DECODE是Oracle独有的函数,必须转成CASE WHEN:
-- Oracle DECODE 写法
SELECT order_id,
DECODE(order_status,
'INIT', '待支付',
'PAID', '已支付',
'SHIPPED', '已发货',
'DONE', '已完成',
'CANCEL', '已取消',
'未知') AS status_name
FROM ord_order;
-- 金仓 CASE WHEN 写法(SQL标准,Oracle也兼容)
SELECT order_id,
CASE order_status
WHEN 'INIT' THEN '待支付'
WHEN 'PAID' THEN '已支付'
WHEN 'SHIPPED' THEN '已发货'
WHEN 'DONE' THEN '已完成'
WHEN 'CANCEL' THEN '已取消'
ELSE '未知'
END AS status_name
FROM ord_order;
2.4 日期函数:SYSDATE vs NOW()
-- Oracle 日期函数
SELECT SYSDATE FROM DUAL; -- 当前时间
SELECT ADD_MONTHS(SYSDATE, 3) FROM DUAL; -- 加3个月
SELECT TRUNC(SYSDATE, 'MM') FROM DUAL; -- 当月第一天
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD') FROM DUAL; -- 格式化
SELECT TO_DATE('2024-01-15', 'YYYY-MM-DD') FROM DUAL; -- 转日期
-- 金仓 MySQL 兼容版
SELECT NOW(); -- 当前时间
SELECT DATE_ADD(NOW(), INTERVAL 3 MONTH); -- 加3个月
SELECT DATE_FORMAT(NOW(), '%Y-%m-01'); -- 当月第一天(用DATE_FORMAT拼凑)
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d'); -- 格式化
SELECT STR_TO_DATE('2024-01-15', '%Y-%m-%d'); -- 转日期
有个小技巧:如果Oracle代码里大量用了SYSDATE,可以在金仓里建一个视图或者函数来模拟:
-- 模拟Oracle的DUAL表(金仓MySQL兼容模式下DUAL表可用)
SELECT 1 FROM DUAL; -- 可以直接用
-- 模拟SYSDATE
CREATE FUNCTION sys_date() RETURNS DATETIME
RETURN NOW();
-- 然后就可以这样用
SELECT sys_date();
2.5 树形查询:CONNECT BY vs 递归CTE
这是最头疼的一个。Oracle的CONNECT BY用起来很方便,金仓这边得用递归CTE:
-- Oracle 树形查询(查部门树)
SELECT dept_id, dept_name, parent_id, LEVEL
FROM sys_dept
START WITH parent_id = 0
CONNECT BY PRIOR dept_id = parent_id
ORDER SIBLINGS BY dept_name;
-- 金仓 递归 CTE 写法
WITH RECURSIVE dept_tree AS (
-- 锚点成员:根节点
SELECT dept_id, dept_name, parent_id, 1 AS lvl
FROM sys_dept
WHERE parent_id = 0
UNION ALL
-- 递归成员
SELECT d.dept_id, d.dept_name, d.parent_id, dt.lvl + 1 AS lvl
FROM sys_dept d
INNER JOIN dept_tree dt ON d.parent_id = dt.dept_id
)
SELECT dept_id, dept_name, parent_id, lvl AS level
FROM dept_tree
ORDER BY lvl, dept_name;
递归CTE虽然写起来长一点,但逻辑清晰,而且是SQL标准语法,可移植性更好。
2.6 MERGE INTO vs INSERT … ON DUPLICATE KEY UPDATE
Oracle的MERGE INTO是个很强大的功能,金仓MySQL兼容版用ON DUPLICATE KEY UPDATE实现类似效果:
-- Oracle MERGE INTO
MERGE INTO ord_stat t
USING (SELECT order_date, COUNT(*) cnt FROM ord_order GROUP BY order_date) s
ON (t.stat_date = s.order_date)
WHEN MATCHED THEN UPDATE SET t.order_count = s.cnt
WHEN NOT MATCHED THEN INSERT (stat_date, order_count) VALUES (s.order_date, s.cnt);
-- 金仓 MySQL 兼容版
INSERT INTO ord_stat (stat_date, order_count)
SELECT order_date, COUNT(*) as cnt
FROM ord_order
GROUP BY order_date
ON DUPLICATE KEY UPDATE order_count = VALUES(order_count);
注意:ON DUPLICATE KEY UPDATE依赖表上的唯一索引或主键,需要先确保stat_date上有唯一约束。
2.7 序列:SEQUENCE vs AUTO_INCREMENT
-- Oracle 序列
CREATE SEQUENCE seq_order_id START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;
SELECT seq_order_id.NEXTVAL FROM DUAL;
SELECT seq_order_id.CURRVAL FROM DUAL;
-- 金仓 MySQL 兼容版(自增主键,最常用)
CREATE TABLE ord_order (
order_id BIGINT AUTO_INCREMENT PRIMARY KEY,
...
);
-- 插入后获取自增ID
SELECT LAST_INSERT_ID();
-- 如果确实需要Oracle风格的序列(比如跨表共用一个ID生成器)
-- 可以用一张序列模拟表
CREATE TABLE sys_sequence (
seq_name VARCHAR(50) PRIMARY KEY,
current_val BIGINT NOT NULL DEFAULT 0,
increment_val INT NOT NULL DEFAULT 1
);
-- 获取下一个值的函数
DELIMITER //
CREATE FUNCTION nextval(p_seq_name VARCHAR(50)) RETURNS BIGINT
BEGIN
UPDATE sys_sequence
SET current_val = LAST_INSERT_ID(current_val + increment_val)
WHERE seq_name = p_seq_name;
RETURN LAST_INSERT_ID();
END //
DELIMITER ;
-- 使用
INSERT INTO sys_sequence(seq_name, current_val, increment_val)
VALUES ('seq_order_id', 0, 1);
SELECT nextval('seq_order_id'); -- 1
SELECT nextval('seq_order_id'); -- 2
2.8 存储过程迁移:PL/SQL vs PL/pgSQL
这又是一块硬骨头。Oracle的PL/SQL和金仓的PL/pgSQL语法差异不小,我之前写开发实战那篇的时候已经踩过一轮了。
-- Oracle 存储过程(计算订单等级)
CREATE OR REPLACE PROCEDURE sp_calc_order_level(
p_order_id IN NUMBER,
p_level OUT VARCHAR2
) IS
v_total NUMBER(12,2);
BEGIN
SELECT total_amt INTO v_total
FROM ord_order
WHERE order_id = p_order_id;
IF v_total >= 10000 THEN
p_level := '大客户';
ELSIF v_total >= 5000 THEN
p_level := '中等客户';
ELSIF v_total >= 1000 THEN
p_level := '普通客户';
ELSE
p_level := '小客户';
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_level := '订单不存在';
WHEN OTHERS THEN
p_level := '计算异常';
END;
/
-- 金仓 PL/pgSQL 版本
CREATE OR REPLACE FUNCTION sp_calc_order_level(p_order_id BIGINT)
RETURNS VARCHAR AS $$
DECLARE
v_total DECIMAL(12,2);
v_level VARCHAR(20);
BEGIN
SELECT total_amt INTO v_total
FROM ord_order
WHERE order_id = p_order_id;
IF NOT FOUND THEN
RETURN '订单不存在';
END IF;
IF v_total >= 10000 THEN
v_level := '大客户';
ELSIF v_total >= 5000 THEN
v_level := '中等客户';
ELSIF v_total >= 1000 THEN
v_level := '普通客户';
ELSE
v_level := '小客户';
END IF;
RETURN v_level;
EXCEPTION
WHEN OTHERS THEN
RETURN '计算异常';
END;
$$ LANGUAGE plpgsql;
-- 调用
SELECT sp_calc_order_level(1001);
核心差异总结一下:
- Oracle是PROCEDURE,金仓用FUNCTION + RETURNS类型(MySQL兼容模式下也支持PROCEDURE语法,但用函数更灵活)
- 变量声明要放在DECLARE块里
- 函数体用$$包裹,末尾加LANGUAGE plpgsql
- NO_DATA_FOUND异常在金仓里用IF NOT FOUND判断
- OUT参数改成RETURN返回值
2.9 驱动与连接池适配
连接池我们用的是HikariCP,切换到金仓后调了几个关键参数:
spring:
datasource:
hikari:
maximum-pool-size: 30 # 连接池大小,根据实际情况调整
minimum-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 连接超时30秒
idle-timeout: 600000 # 空闲连接超时10分钟
max-lifetime: 1800000 # 连接最大生命周期30分钟
connection-test-query: SELECT 1 # 连接测试语句
金仓驱动用的是kingbase8-8.6.0.jar,从官网下载后放到项目lib目录或者Maven私服。如果项目里用了Druid连接池,记得配置validation-query为SELECT 1。
三、迁移后性能优化:从能用到好用
迁过去只是第一步,跑得好才是真的好。我们迁移完之后做了三轮优化,性能从"勉强能用"提升到了"比Oracle还快"。
3.1 索引优化
首先用慢查询日志把跑得慢的SQL捞出来:
-- 查看当前慢查询阈值
SHOW long_query_time;
-- 设置慢查询阈值为2秒
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;
-- 查看慢查询日志路径
SHOW VARIABLES LIKE 'slow_query_log_file';
然后用EXPLAIN逐条分析:
EXPLAIN SELECT o.*, c.cust_name
FROM ord_order o
LEFT JOIN bas_customer c ON o.cust_id = c.cust_id
WHERE o.order_status = 'PAID'
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 20;
我们发现的典型问题:
- Oracle的组合索引在金仓里顺序不对,导致索引失效
- 隐式类型转换(字符串和数字比较)导致索引失效
- 缺少覆盖索引,回表开销大
优化后的索引建表语句:
-- 订单状态+创建时间 联合索引(支持状态筛选+时间排序)
CREATE INDEX idx_order_status_time ON ord_order(order_status, create_time DESC);
-- 客户ID 索引(关联查询用)
CREATE INDEX idx_order_cust_id ON ord_order(cust_id);
-- 订单号 唯一索引(之前有,但确认一下)
-- CREATE UNIQUE INDEX uk_order_no ON ord_order(order_no);
-- 订单明细的订单ID索引
CREATE INDEX idx_item_order_id ON ord_order_item(order_id);
3.2 分区表优化
订单表数据量比较大,我们按月份做了范围分区:
-- Oracle 分区表
CREATE TABLE ord_order (
...
) PARTITION BY RANGE (create_time) (
PARTITION p202401 VALUES LESS THAN (TO_DATE('2024-02-01', 'YYYY-MM-DD')),
PARTITION p202402 VALUES LESS THAN (TO_DATE('2024-03-01', 'YYYY-MM-DD')),
...
);
-- 金仓 MySQL 兼容版分区表
CREATE TABLE ord_order (
order_id BIGINT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL,
cust_id BIGINT NOT NULL,
total_amt DECIMAL(12,2) DEFAULT 0,
order_status VARCHAR(20) DEFAULT 'INIT',
remark VARCHAR(500),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME,
UNIQUE KEY uk_order_no (order_no, create_time) -- 分区键必须包含在唯一键里
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')),
PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')),
PARTITION p202406 VALUES LESS THAN (TO_DAYS('2024-07-01')),
PARTITION p202407 VALUES LESS THAN (TO_DAYS('2024-08-01')),
PARTITION p202408 VALUES LESS THAN (TO_DAYS('2024-09-01')),
PARTITION p202409 VALUES LESS THAN (TO_DAYS('2024-10-01')),
PARTITION p202410 VALUES LESS THAN (TO_DAYS('2024-11-01')),
PARTITION p202411 VALUES LESS THAN (TO_DAYS('2024-12-01')),
PARTITION p202412 VALUES LESS THAN (TO_DAYS('2025-01-01'))
);
注意这个坑:MySQL的分区表要求所有唯一索引(包括主键)都必须包含分区键。所以如果主键是order_id,分区键是create_time,那主键必须改成(order_id, create_time)组合键,或者用非分区的自增主键+分区辅助索引。
3.3 SQL优化实战
举个典型的优化案例:统计每日订单金额。
-- 慢SQL(全表扫描)
SELECT DATE(create_time) AS order_date,
COUNT(*) AS order_count,
SUM(total_amt) AS total_amount
FROM ord_order
WHERE create_time >= '2024-01-01'
AND create_time < '2024-07-01'
GROUP BY DATE(create_time)
ORDER BY order_date;
-- 优化1:加索引
CREATE INDEX idx_order_time_amt ON ord_order(create_time, total_amt);
-- 优化2:如果数据量特别大,考虑预聚合表
CREATE TABLE ord_daily_stat (
stat_date DATE PRIMARY KEY,
order_count INT DEFAULT 0,
total_amount DECIMAL(14,2) DEFAULT 0
);
-- 每天凌晨跑定时任务更新
INSERT INTO ord_daily_stat(stat_date, order_count, total_amount)
SELECT DATE(create_time), COUNT(*), SUM(total_amt)
FROM ord_order
WHERE create_time >= CURDATE() - INTERVAL 1 DAY
AND create_time < CURDATE()
ON DUPLICATE KEY UPDATE
order_count = VALUES(order_count),
total_amount = VALUES(total_amount);
-- 查询直接走预聚合表,毫秒级返回
SELECT * FROM ord_daily_stat
WHERE stat_date >= '2024-01-01'
AND stat_date < '2024-07-01'
ORDER BY stat_date;
3.4 参数调优
金仓的参数配置跟Oracle思路不太一样,但核心参数就那几个:
-- 查看当前配置
SHOW shared_buffers; -- 共享缓冲区,类似Oracle SGA
SHOW effective_cache_size; -- 优化器预估的可用缓存
SHOW work_mem; -- 排序操作内存
SHOW maintenance_work_mem; -- 维护操作内存(建索引等)
SHOW wal_buffers; -- WAL缓冲区
SHOW max_connections; -- 最大连接数
-- 建议配置(根据服务器内存调整,假设32G内存)
-- shared_buffers = 8GB (物理内存的1/4)
-- effective_cache_size = 24GB (物理内存的3/4)
-- work_mem = 64MB (每个排序操作的内存)
-- maintenance_work_mem = 2GB (维护操作内存)
-- wal_buffers = 64MB (WAL缓冲区)
-- max_connections = 500 (最大连接数)
这些参数在kingbase.conf里改,改完重启或者reload生效。
四、异构数据实时同步:平滑切换的保障
迁移过程中,零停机切换是我们追求的目标。实时同步是关键一环。
4.1 同步方案选型
我们对比了几种方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 金仓DTS | 官方工具,兼容性好,配置简单 | 功能相对基础 |
| Oracle GoldenGate | 功能强大,支持异构同步 | 贵,配置复杂 |
| DataX + 定时增量 | 开源免费,灵活 | 实时性差,适合T+1 |
| Canal + 自定义消费 | 开源,实时性好 | 需要开发,运维成本高 |
我们最终选了金仓DTS做主迁移+全量校验,配合Canal做增量同步和双写兜底。
4.2 DTS全量迁移
# DTS 工具基本用法(命令行版)
# 1. 配置源端(Oracle)
# 2. 配置目标端(金仓)
# 3. 选择要迁移的表
# 4. 执行全量迁移
# 5. 启动增量同步
# 迁移完成后的数据量校验脚本
# 可以用 DTS 自带的校验功能,也可以自己写脚本
4.3 双写架构设计
为了保证切换平滑,我们做了应用层双写:
// 伪代码:双写逻辑
@Service
public class OrderService {
// Oracle数据源(主)
@Autowired
private OrderMapper oracleOrderMapper;
// 金仓数据源(备)
@Autowired
private OrderMapper kingbaseOrderMapper;
@Transactional(transactionManager = "oracleTransactionManager")
public void createOrder(Order order) {
// 先写Oracle
oracleOrderMapper.insert(order);
// 异步写金仓(不影响主流程)
CompletableFuture.runAsync(() -> {
try {
kingbaseOrderMapper.insert(order);
} catch (Exception e) {
// 记录失败日志,后续补偿
log.error("写金仓失败,orderId: {}", order.getOrderId(), e);
}
});
}
}
双写跑了一周,确认两边数据一致后,才把读流量切到金仓,又跑了一周确认没问题,最后把写流量也切过去,Oracle降级为只读兜底。
4.4 回滚预案
迁移这事儿,必须有回滚方案。万一金仓出问题,得能秒级切回Oracle。
# 灰度切换配置(Nacos或配置中心)
migrate:
# 读写路由策略
# oracle-only: 全走Oracle
# write-both: 双写,读走Oracle
# read-kingbase: 双写,读走金仓
# kingbase-only: 全走金仓(Oracle只读兜底)
mode: read-kingbase
# 灰度比例(按用户ID取模)
gray-ratio: 100 # 100表示全量,0表示不切
切换顺序:oracle-only → write-both → read-kingbase → kingbase-only。每一步至少观察24小时,没问题再往下走。出问题随时往上回退。
五、避坑清单:老DBA的血泪总结
最后给大家列一份避坑清单,都是我踩过的坑,能帮一个是一个。
迁移前必查
- 确认金仓版本和兼容模式(MySQL / Oracle / PG)
- 评估Oracle特有语法的使用量(CONNECT BY、MERGE、分析函数等)
- 盘点存储过程、函数、触发器、包、Job数量
- 确认应用层ORM框架和驱动版本
- 准备性能基准测试用例(迁移前后对比)
迁移中注意
- 数据类型映射要精确,特别是NUMBER精度
- 字符集统一用UTF8/UTF8MB4,避免乱码
- 主键生成策略要提前确定(自增/序列/雪花算法)
- 分区表的分区键必须包含在所有唯一索引中
- 存储过程逐行改造,不要批量自动化转换(容易出问题)
迁移后优化
- 收集统计信息(类似Oracle的DBMS_STATS)
- 重建索引,检查索引有效性
- 慢SQL优化,逐条分析
- 连接池参数调优
- 备份恢复策略验证
运维层面
-
监控告警对接(金仓有自己的监控工具KMonitor)
-
备份策略调整(物理备份+逻辑备份)
-
运维人员培训(金仓的管理工具是KDTS、ksql等)
-
建立应急回滚机制
写在最后
从Oracle迁到金仓,说简单也简单——毕竟都是关系型数据库,SQL标准在那儿摆着;说难也难——Oracle积累了几十年的语法糖和特性,迁的时候一个一个往外冒。
我的体会是:迁移不是简单的"换个数据库地址",而是一次系统架构的梳理和优化。趁着迁移的机会,把历史遗留的烂SQL、冗余索引、不合理的表结构都整治一遍,迁完之后系统反而比以前更快更稳。
金仓KES V9R3C18 MySQL兼容版给我的整体感觉是:兼容性超出预期,常见的MySQL语法基本都能直接跑,性能也够用。对于已经做过MySQL适配、或者本身就是MySQL技术栈的团队来说,迁移成本很低。
当然,Oracle生态里一些比较深的特性(比如RAC、ASM、高级复制、Oracle Text等),迁移成本会高一些。但对于绝大多数业务系统来说,金仓完全够用了。
信创这条路,刚开始走的时候觉得难,走过来了发现也就那么回事。关键是要有完整的方法论和足够的验证。希望我的经验能帮到正在迁移路上的同行们。




