数据库迁移这件事,听起来通常很简单。
源库导出,目标库导入。
如果两边数据库不同,再加一层结构转换。
理论上,大概就是:
lua代码解读复制代码MySQL ↓ dump ↓ 转换 ↓ DM8
真正做起来,我才发现,到了信创环境,这四步几乎每一步都可能出问题。
这次的环境是比较典型的信创项目:
代码解读复制代码源数据库:MySQL 5.7 目标数据库:达梦 DM8 目标操作系统:银河麒麟 网络环境:隔离网络 生产环境:已经部署在信创区 数据流向:只能进入,不能导出
最开始我以为,最大的麻烦会是 MySQL 和达梦之间的 SQL 方言兼容性。后来才发现,SQL 方言只是问题的一部分。
真正麻烦的是,数据库兼容性、迁移工具、网络隔离、Schema 机制、生产约束,全部叠加到了一起。
最后这件事变成了一次很典型的“看起来简单,实际处处有坑”的异构数据库迁移。
最直觉的办法当然是:
代码解读复制代码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 的确很有用,它至少可以完成:
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 张表
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 个不同的问题。
实际上只集中在几个模式。
部分表包含:
代码解读复制代码AUTO_INCREMENT
尤其还有一些比较特殊的字段:
scss代码解读复制代码DECIMAL AUTO_INCREMENT
或者并非主键的自增列。这种情况下,与其强行保证第一次建表就完全还原 MySQL 语义,不如先以“能正确导入历史数据”为目标。
因此第一次建表时先去掉:
代码解读复制代码AUTO_INCREMENT
历史数据本身会显式写入 ID,并不会受到影响。 自增语义可以等结构和数据迁完以后,再根据业务实际情况恢复。 这也体现出数据库迁移里一个很重要的原则:
迁移过程中,不一定要一步恢复所有运行时语义。可以先保证数据正确,再恢复约束和自动化行为。
另外几张表共同包含:
sql代码解读复制代码ON UPDATE CURRENT_TIMESTAMP
DTS 转换以后变成了:
sql代码解读复制代码ON UPDATE LOCALTIMESTAMP
但这个转换结果并不能直接作为 DM 的列定义执行。去掉以后,表立即能够正常创建。 如果业务确实依赖自动更新时间,后续可以再通过:应用逻辑、触发器、其他 DM 机制 恢复。
前面提到的:
scss代码解读复制代码decimal(10,2) unsigned
转换后变成:
arduino代码解读复制代码NOT NULL
这种问题最危险。因为它不是“语法错误”,而是转换过程里丢失了字段语义。
因此迁移后一定要抽查:
ini代码解读复制代码DESC table_name;
至少要覆盖:
sql代码解读复制代码BIGINT DECIMAL VARCHAR DATETIME TEXT BLOB BIT TINYINT
不能只看:
代码解读复制代码339/339
表数量相等只是第一层验证。
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,不同业务库出现的问题不一定相同。
工具可以减少工作量,但不能替代迁移验证。
这次还有一个让我印象特别深的坑。
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;
这一条看似简单,但生产迁移里真的非常重要。
迁移到数据阶段以后,又遇到一个非常奇怪的问题。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。
表数量一致
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 的理解大概变成了下面这些。
异构数据库迁移最好拆成:
代码解读复制代码结构 数据 约束 运行时语义
四个阶段。
无论评估结果多漂亮,都不应该认为:自动转换结果 = 最终结果
例如:
sql代码解读复制代码AUTO_INCREMENT ON UPDATE FOREIGN KEY TRIGGER
都可以在数据迁完以后恢复。它们不应该阻塞基础迁移。
15 张表失败,不意味着有 15 个问题。
通常是:
代码解读复制代码4 种语法 导致 15 张表失败
找到共同模式以后,批量修复远比一张张改有效。
结构:源 = 目标 对象:INVALID = 0 数据:COUNT 、MIN、MAX、SUM、业务分布
否则“执行成功”并不意味着“迁移成功”。
当生产数据只能进不能出以后,真正可靠的不是某个 GUI 工具。
而是一套:可离线、可重复、可校验、可回滚、可审计 的迁移流程。
最开始我只是想把一个 MySQL dump 导进达梦。后来一路碰到了:
sql代码解读复制代码JDBC DTS SQL 方言 Schema AUTO_INCREMENT ON UPDATE DECIMAL 精度 外键顺序 HTML 实体 DIsql 变量替换 网络隔离 生产数据单向流
最后才意识到,这件事真正考验的并不是会不会写几条 SQL。而是能不能在一个约束很多、信息并不完整、工具也不完全可靠的环境里,把一个大问题拆成若干可验证的小问题。
从这个角度看,所谓“信创数据库迁移”,真正需要迁移的不只是数据。还有原来那些默认成立的假设。
比如:
网络应该是通的。
SQL 应该是兼容的。
工具转换出来的东西应该能执行。
出了问题可以把数据拿出来分析。 当这些假设一个个失效以后,迁移方案才真正开始成形。而这大概也是这次折腾里,最值得记录下来的东西。
作者:Sailor_Infra
链接:https://juejin.cn/post/7685644759736844323
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。