从 MySQL 到达梦:一次信创隔离环境里的数据库迁移踩坑实录
数据库迁移这件事,听起来通常很简单。
源库导出,目标库导入。
如果两边数据库不同,再加一层结构转换。
理论上,大概就是:
lua
MySQL
↓
dump
↓
转换
↓
DM8
真正做起来,我才发现,到了信创环境,这四步几乎每一步都可能出问题。
这次的环境是比较典型的信创项目:
源数据库:MySQL 5.7
目标数据库:达梦 DM8
目标操作系统:银河麒麟
网络环境:隔离网络
生产环境:已经部署在信创区
数据流向:只能进入,不能导出
最开始我以为,最大的麻烦会是 MySQL 和达梦之间的 SQL 方言兼容性。后来才发现,SQL 方言只是问题的一部分。
真正麻烦的是,数据库兼容性、迁移工具、网络隔离、Schema 机制、生产约束,全部叠加到了一起。
最后这件事变成了一次很典型的"看起来简单,实际处处有坑"的异构数据库迁移。
一、第一条弯路:直接把 MySQL dump 灌进达梦
最直觉的办法当然是:
mysqldump database > database.sql
然后:
ini
START database.sql;
如果只是 MySQL 到 MySQL,这没什么问题。
但 MySQL dump 本质上不是一种"通用 SQL 格式",而是大量混合了 MySQL 方言和 mysqldump 控制语句的文件。
例如:
ini
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40014 SET FOREIGN_KEY_CHECKS=0 */;
还有:
ini
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
AUTO_INCREMENT
UNSIGNED
ON UPDATE CURRENT_TIMESTAMP
以及 MySQL 常见的:
arduino
COMMENT '字段说明'
这些东西到了 DM8 里,并不能指望全部直接执行。
所以第一条经验很简单:
异构数据库迁移时,不要把 mysqldump 理解成通用数据库备份格式。
它更准确的定义是:
MySQL 可以重新执行的一套 SQL 和控制语句。
而不是:
其他数据库能够直接消费的数据交换格式。
二、DTS 能解决问题,但并不是"一键迁移"
后来开始尝试达梦自带的 DTS 数据迁移工具。DTS 的确很有用,它至少可以完成:
sql
MySQL SQL
↓
语法分析
↓
类型映射
↓
DM SQL
而且它还有数据库对象评估功能。 这次一个库评估出来的结果甚至是:
sql
339 张表
兼容率 100%
不兼容对象 0
错误 SQL 0
看起来已经非常理想。但这里很容易产生一个误解:
"评估兼容率 100%"不等于"生成的 SQL 可以 100% 直接执行"。
事实上后面依然遇到了很多问题。 例如 DTS 转换结果里会出现:
ini
/****DMDTS CONVERT*** ENGINE=InnoDB*/
以及:
ini
/****DMDTS CONVERT*** DEFAULT CHARSET=utf8mb4*/
还有一些 MySQL 原始语句没有被彻底清理:
ini
/*!40101 SET character_set_client = @saved_cs_client */;
甚至某些字段会被错误转换。
例如原 MySQL:
arduino
discount_rate decimal(10,2) unsigned NOT NULL
转换后居然变成:
arduino
"discount_rate" NOT NULL
字段类型直接丢了。所以后来我逐渐形成一个判断:
DTS 应该被视为"辅助转换器",而不是最终裁判。
它能大幅减少人工转换工作量,但生成的 SQL 仍然需要:
转换
→ 清洗
→ 执行
→ 校验
→ 修补
而不是:
转换
→ 上生产
三、真正有效的办法:先迁结构,再迁数据
这次迁移过程中,最重要的一次思路调整,就是不再试图把:DDL + 数据 一次性全部解决。
而是明确拆成两条链路。
结构:
css
MySQL schema
↓
mysqldump --no-data
↓
DTS 转换
↓
清洗 DM DDL
↓
DM 建表
数据:
kotlin
MySQL data
↓
data-only dump / CSV
↓
转换或批量装载
↓
DM
这样最大的好处,是问题被拆小了。以前一个巨大 SQL 文件失败,只知道"导入失败"。 拆开以后可以明确知道:
是结构的问题?还是数据的问题?
是第 17 张表?还是第 17 张表的第 300 行?
是字段类型?还是数据内容?
对异构数据库迁移来说,可定位性非常重要。
四、第一次结构迁移:339 张表,最后成功 324 张
第一个数据库原本有:339 张表
DTS 转换后的 SQL 一开始执行时,一张都没有建出来。 检查后发现转换文件里依然混杂了大量 MySQL 内容:
arduino
/*!40101 ... */
以及 DTS 的说明性注释:
arduino
/****DMDTS CONVERT*** ... */
还有 MySQL 风格列注释:
sql
"id" bigint NOT NULL COMMENT '主键'
于是对整个 SQL 做了一次批量清洗:
sql
删除 /*!xxxxx ... */
删除 DTS CONVERT 注释块
删除 CREATE SCHEMA
删除 SET SCHEMA
删除 ENGINE / CHARSET 等残留
转换 COMMENT
转换 b'1' / b'0'
其中字段注释从:
sql
"id" bigint COMMENT '主键'
改为 DM 风格:
sql
"id" bigint
再单独:
vbnet
COMMENT ON COLUMN "table"."id" IS '主键';
处理以后重新执行。结果: 339 张 成功 324 张 失败 15 张
这时候就没有必要再反复执行整份 SQL 了。
直接对比:
sql
SELECT TABLE_NAME
FROM USER_TABLES;
和原始的 339 张表名单,就能精确得到缺失的 15 张。
五、15 张失败表告诉了我一件事:问题往往是成批出现的
这 15 张失败表并不是 15 个不同的问题。
实际上只集中在几个模式。
1. AUTO_INCREMENT
部分表包含:
AUTO_INCREMENT
尤其还有一些比较特殊的字段:
scss
DECIMAL AUTO_INCREMENT
或者并非主键的自增列。这种情况下,与其强行保证第一次建表就完全还原 MySQL 语义,不如先以"能正确导入历史数据"为目标。
因此第一次建表时先去掉:
AUTO_INCREMENT
历史数据本身会显式写入 ID,并不会受到影响。 自增语义可以等结构和数据迁完以后,再根据业务实际情况恢复。 这也体现出数据库迁移里一个很重要的原则:
迁移过程中,不一定要一步恢复所有运行时语义。可以先保证数据正确,再恢复约束和自动化行为。
2. ON UPDATE CURRENT_TIMESTAMP
另外几张表共同包含:
sql
ON UPDATE CURRENT_TIMESTAMP
DTS 转换以后变成了:
sql
ON UPDATE LOCALTIMESTAMP
但这个转换结果并不能直接作为 DM 的列定义执行。去掉以后,表立即能够正常创建。 如果业务确实依赖自动更新时间,后续可以再通过:应用逻辑、触发器、其他 DM 机制 恢复。
3. 字段类型转换丢失
前面提到的:
scss
decimal(10,2) unsigned
转换后变成:
arduino
NOT NULL
这种问题最危险。因为它不是"语法错误",而是转换过程里丢失了字段语义。
因此迁移后一定要抽查:
ini
DESC table_name;
至少要覆盖:
sql
BIGINT
DECIMAL
VARCHAR
DATETIME
TEXT
BLOB
BIT
TINYINT
不能只看:
339/339
表数量相等只是第一层验证。
4. 外键创建顺序
Quartz 的几张表:
qrtz_blob_triggers
qrtz_cron_triggers
qrtz_simple_triggers
qrtz_simprop_triggers
都依赖:
qrtz_triggers
如果子表先创建:
scss
FOREIGN KEY (...)
REFERENCES qrtz_triggers(...)
而父表还不存在,就会失败。 最后采用的办法非常直接:
先去掉外键
→ 全部建表
→ 后续再补外键
实际上我后来觉得,这可能本来就是异构迁移时更合理的办法。因为外键并不是"数据存在"的前提。反而迁移大量数据时,暂时没有外键通常更方便。
六、第二个库又踩了一遍,但坑不完全一样
第二个数据库有:
291 张表
清洗后第一次成功:
283 张
少了 8 张。
这一次主要遇到了两个新问题。
一个是 DTS 生成了明显错误的:
scss
DEFAULT CURRENT_TIMESTAMP(3) (3)
这种语句当然无法执行。
另一个是 MySQL 里存在:
scss
DECIMAL(65,0)
DECIMAL(65,2)
而 DM 对 DECIMAL 精度的支持范围不同。
最后统一改成:
scss
DECIMAL(38,0)
DECIMAL(38,2)
8 张表补完后,整个结构才最终完整。
这也进一步证明:
同样是 MySQL → DM,不同业务库出现的问题不一定相同。
工具可以减少工作量,但不能替代迁移验证。
七、一个非常隐蔽的坑:Schema 和用户
这次还有一个让我印象特别深的坑。
DM 的用户和 Schema 关系,与 MySQL 的"数据库"概念并不完全一样。因为前面为了清理 DTS SQL,我删除了:
sql
CREATE SCHEMA
SET SCHEMA
这意味着脚本会直接在:
当前登录用户
下面建表。结果有一次我用:SYSDBA
执行了业务库结构 SQL。后来一查:
sql
SELECT OWNER, COUNT(*) FROM DBA_TABLES GROUP BY OWNER;
出现:
SYSDBA 285
YYJX_YGJ_PROD 339
我才意识到,第二套业务表被建到了:SYSDBA 下面。
这件事本身很好修。但需要记住:
执行结构脚本之前,不要只确认数据库地址和端口,一定确认当前用户。
在执行之前固定先看:
sql
SELECT USER;
再执行:
sql
SELECT COUNT(*) FROM USER_TABLES;
确认 Schema 环境无误以后再:
ini
START schema.sql;
这一条看似简单,但生产迁移里真的非常重要。
八、为什么 SQL 里一个 都能让导入停下来
迁移到数据阶段以后,又遇到一个非常奇怪的问题。DIsql 不断提示:
输入 nbsp 的值:
输入 nbsp 的值:
输入 nbsp 的值:
第一反应会怀疑 SQL 文件编码坏了。最后才发现,业务数据里本身包含大量 HTML:
ini
"
<
>
DIsql 默认支持变量替换,而:& 恰好是变量前缀。 所以它看到: 会理解为:变量名 nbsp。于是开始要求人工输入变量。
解决办法只有一句:
sql
SET DEFINE OFF;
这件事让我重新认识了"数据迁移"。 以前往往只考虑:
字段类型
字符集
主键
索引
但真实业务数据里还有:
css
HTML
JSON
XML
特殊字符
转义字符
长文本
任何一个都可能碰到数据库客户端自己的语义。
九、信创隔离环境最大的难点,其实不是数据库
做到后面我越来越觉得,这次迁移最大的困难甚至不是 MySQL 和 DM 的兼容性。
而是:
生产环境已经进入信创隔离区,而且数据只能进,不能出。
这意味着很多我们平时习惯的操作都不存在了。
例如:生产导一份数据出来分析:不行。
失败以后把 SQL 和日志拿出来处理:可能也不行。
外部工具直连生产数据库:更不行。
于是整个迁移流程必须重新设计。
传统环境可以:
迁移
→ 发现问题
→ 导出来
→ 修改
→ 再迁
单向隔离环境里更合理的是:
外部准备
→ 一次性摆渡完整迁移包
→ 内部执行
→ 内部验证
即:
进入隔离环境之前,就应该尽可能把后续可能需要的工具、SQL、校验逻辑一起准备进去。
十、后来我开始把迁移看成"发布",而不是"拷数据"
如果再让我做一次类似项目,我不会再准备一个:
database.sql
然后拿进去碰运气。而是准备一个完整的迁移包:
erlang
migration_package/
├── 01_schema/
│ ├── schema.sql
│ └── patch.sql
│
├── 02_data/
│ ├── table_a.sql
│ ├── table_b.sql
│ └── ...
│
├── 03_verify/
│ ├── source_count.txt
│ ├── verify_count.sql
│ └── verify_business.sql
│
├── 04_rollback/
│ └── rollback.sql
│
└── README.txt
执行顺序固定:
Precheck
→ DDL
→ 数据
→ 校验
→ 提交
这其实已经不像传统意义上的"数据库迁移"。更像一次数据库发布。而且生产已经在隔离区以后,以后每一次补数都应该有:
输入
影响范围
批次边界
校验方法
回滚方式
而不是临时手工执行 SQL。
十一、数据校验不要只做 COUNT(*)
表数量一致
ini
339 = 339
只能证明"表基本建齐"。数据迁移也是一样。
比如:
sql
SELECT COUNT(*) FROM member_info;
两边都是:
100000
并不能证明数据完全正确。更稳妥的校验至少可以做:
scss
SELECT
COUNT(*) AS CNT,
MIN(id) AS MIN_ID,
MAX(id) AS MAX_ID,
SUM(id) AS SUM_ID
FROM member_info;
如果业务允许,还可以做一些状态分布:
sql
SELECT status, COUNT(*)
FROM member_info
GROUP BY status;
或者:
scss
SELECT DATE(create_time), COUNT(*)
FROM member_info
GROUP BY DATE(create_time);
这些统计结果比单纯行数更容易发现漏数、重复导入等问题。
而且特别适合:
数据不能导出,但可以在隔离区内部执行校验 SQL
的环境。
十二、最终总结出来的几个原则
这一轮折腾以后,我现在对 MySQL → DM 的理解大概变成了下面这些。
1. 不要追求"一把梭"
异构数据库迁移最好拆成:
结构
数据
约束
运行时语义
四个阶段。
2. 转换工具只是助手
无论评估结果多漂亮,都不应该认为:自动转换结果 = 最终结果
3. 先让表存在,再恢复高级语义
例如:
sql
AUTO_INCREMENT
ON UPDATE
FOREIGN KEY
TRIGGER
都可以在数据迁完以后恢复。它们不应该阻塞基础迁移。
4. 错误要批量归类
15 张表失败,不意味着有 15 个问题。
通常是:
4 种语法 导致 15 张表失败
找到共同模式以后,批量修复远比一张张改有效。
5. 每一步都要可验证
结构:源 = 目标 对象:INVALID = 0 数据:COUNT 、MIN、MAX、SUM、业务分布
否则"执行成功"并不意味着"迁移成功"。
6. 单向隔离环境里,迁移包比迁移工具更重要
当生产数据只能进不能出以后,真正可靠的不是某个 GUI 工具。
而是一套:可离线、可重复、可校验、可回滚、可审计 的迁移流程。
结语
最开始我只是想把一个 MySQL dump 导进达梦。后来一路碰到了:
sql
JDBC
DTS
SQL 方言
Schema
AUTO_INCREMENT
ON UPDATE
DECIMAL 精度
外键顺序
HTML 实体
DIsql 变量替换
网络隔离
生产数据单向流
最后才意识到,这件事真正考验的并不是会不会写几条 SQL。而是能不能在一个约束很多、信息并不完整、工具也不完全可靠的环境里,把一个大问题拆成若干可验证的小问题。
从这个角度看,所谓"信创数据库迁移",真正需要迁移的不只是数据。还有原来那些默认成立的假设。
比如:
- 网络应该是通的。
- SQL 应该是兼容的。
- 工具转换出来的东西应该能执行。
- 出了问题可以把数据拿出来分析。 当这些假设一个个失效以后,迁移方案才真正开始成形。而这大概也是这次折腾里,最值得记录下来的东西。