ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL迁移国产库实战:数据类型适配与SQL兼容性改造

MySQL迁移国产库实战:数据类型适配与SQL兼容性改造 去年年中我接了一个活儿把一套跑了六年的 MySQL 5.7 业务系统迁到国产数据库上去。这个系统说大不大32 个业务库、800 多张表、近 400 个存储过程但真正动手之后我才发现数据拷贝反而是最省事的环节真正耗掉预算和时间的是数据类型适配、SQL 兼容性改造这些细碎功夫。这篇就把我们这次迁移的完整过程、踩坑记录和成本控制思路整理出来给准备做同类项目的团队一个参考。先说结论MySQL 换国产库难点不在搬数据而在改代码和改表结构。而这两件事的复杂度绝大部分集中在数据类型适配这一个点上。把数据类型映射搞清楚能省掉至少六成的改造工作量。1. 动工之前先盘清楚自己有多少隐性成本很多人一听说迁移第一反应是找工具、测网速、准备存储觉得把 mysqldump 的数据灌进去就完了。我第一次接手这类项目时也是这么想的结果后来吃足了苦头。所以这次我学乖了动工前先做了完整的资产盘点。1.1 迁移成本不是拷贝数据那么简单迁移成本大致由四块组成数据搬迁成本、结构改造成本、应用适配成本、回归验证成本。数据搬迁是最透明的一块占用的就是带宽、存储和执行时间结构改造和应用适配是隐性大头尤其是当源库用了大量方言特性时几乎每一处都要人工确认回归验证则决定了项目能否真正上线它的成本通常被严重低估。我做过简单的测算模型把数据量、对象数量、SQL 总量带入后结构与应用适配大致占总成本的 55% 到 65%远远超过数据搬迁本身。也就是说如果你只盯着迁移工具跑得快不快那后面的适配工作一定会让项目延期。1.2 应用兼容性评估清单动手前我让人把整个应用侧的 SQL 做了一次全量扫描用的工具是抓取后端日志中的慢 SQL 与异常堆栈再配合业务代码仓库的关键词检索。扫描的目的不是找 bug而是收集三张清单DDL 清单所有建表语句、索引语句、修改表结构语句。用于盘点每个字段的类型、长度、默认值、注释、字符集签名。DML 清单所有 INSERT、UPDATE、DELETE、SELECT 语句。重点关注函数使用、隐式转换、排序规则、分页写法、自增主键回填方式。过程对象清单存储过程、函数、触发器、定时事件。这些对象往往包含复杂业务逻辑改造量极大。我建议任何团队在迁移前都做一次这样的静态扫描产出这三份清单。不要依赖记忆也不要相信我们只用了最基础的功能这种话。实际扫描出来绝大多数系统使用的 MySQL 特性都远超自己的想象。2. 数据类型适配的核心战场MySQL 与国产库的差异对照数据类型适配之所以是重灾区是因为 MySQL 经过这么多年发展形成了自己的一套宽松习惯。而国产数据库多数源自其他关系型数据库的体系在类型设计、长度语义、强制约束上比 MySQL 严格得多。同一个字段名在两边的行为可能完全不同。下面我把这次迁移中真正让我们头疼的几组差异逐一说明。2.1 INT/BIGINT 与自增主键的坑MySQL 里最常用的自增主键是INT或BIGINT配合AUTO_INCREMENT使用。国产数据库以典型的达梦、人大金仓这类为例对自增的支持有不同的实现方式部分数据库原生支持IDENTITY列语法上近似但不等同于AUTO_INCREMENT。部分数据库需要依赖序列SEQUENCE来模拟自增应用侧的 INSERT 语句可能需要显式调用序列的 NEXTVAL。单看这一点就知道不是类型映射对了就能跑通的。我遇到的实际场景是原表的主键列类型为INT(11)在 MySQL 中INT(11)只是显示宽度不影响取值范围但换到国产库后这个11的语义完全无效部分数据库甚至不解析这个括号。直接按原 DDL 建表可能会被拒绝或产生额外告警。我们的处理方案是先统一清洗 DDL将INT(n)全部改写为INT并将主键列单独抽出统一处理为目标库的自增/序列方案。另外如果业务代码里有SELECT LAST_INSERT_ID()获取自增值的逻辑一定要全局搜索替换因为目标库往往有完全不同的取回方式比如CURRVAL或 RETURNING 子句。这个坑几乎每个迁 MySQL 的项目都会遇到但很少有人在盘点阶段意识到。2.2 DECIMAL 精度和 VARCHAR 长度陷阱DECIMAL类型是最容易产生数据风险的地方。MySQL 中DECIMAL(10,2)表示总位数 10、小数位 2但某些国产库使用DECIMAL时默认精度可能不是 10如果你在迁移建表时把精度丢了钱和账目对不上是上线后才会暴露的雷。我在数据比对阶段就抓到过一批差异源库有 46 个字段使用了DECIMAL且分布在不同精度上迁移工具在自动建表时有的把精度截断成了默认值有的把UNSIGNED属性弄丢了结果导入完成后一校验几十万行数据的数值范围和预期不一致。这提醒我们迁移工具的自动映射只适合第一轮正式确定表结构之前必须做全库精度审查。VARCHAR的长度语义也有差异。MySQL 中VARCHAR(255)的 255 是字符数个别国产库沿用了传统数据库习惯按字节数定义长度或者反过来要求显式指定。虽然现在主流国产库都支持按字符数定义但在旧版本上仍可能出问题。如果目标库对 VARCHAR 总长度有限制比如某些库的行长度限制更紧超长字符串就只能升级为 TEXT/CLOB而这样又会带来新的问题TEXT/CLOB 类型无法直接加默认值不能参与某些索引ORDER BY 或 GROUP BY 的语义也不同。2.3 DATETIME/TIMESTAMP 与默认值差异这一块看着小而碎实际最影响开发效率。MySQL 里常见的写法create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这套写法在部分国产数据库中不能直接执行要么不支持ON UPDATE子句要用触发器替代要么CURRENT_TIMESTAMP的精度要求必须显式写出括号比如CURRENT_TIMESTAMP(3)要么日期时间类型被拆分成了DATE、TIME、TIMESTAMP三种DATETIME类型根本不存在。我们项目里遇到的情况是目标库支持DATETIME但DEFAULT CURRENT_TIMESTAMP在部分模式下被拒绝。处理方式是用触发器统一维护create_time和update_time把所有相关表的逻辑固化在脚本里批量生成而不是一个个改。另外千万注意时区问题MySQL 连接串里习惯于配置serverTimezoneAsia/Shanghai国产库的时区参数名、默认行为可能完全不同。同一套代码连上新库后时间字段整体偏移 8 小时或者 13 小时的情况我在实际项目中见过不止一次。这里有个建议对于日期时间字段的校验不要只看表结构要把应用写入的样本值、读取出来的显示值、时区配置三级对齐后再放行。尤其是跨时区的业务系统要把时区转换逻辑显式固化在连接层不要依赖数据库默认值。3. 从 DDL 到 SQL逐层过一遍兼容性改造数据类型适配的终点不是建出来的表字段类型对了而是应用发出的每条 SQL 都能在目标库上稳定执行。这一步我们采用的是逐层改造法从 DDL 到 DML 到过程对象一层层扫过去。3.1 表结构转换的要点与常见报错表结构转换阶段我建议直接做双轨对照把源库的 DDL 和迁移工具自动生成的 DDL 放到同一个 diff 工具里逐字段比对而不是只相信其中一边。我们当时发现很多隐蔽问题都出在字段注释丢失导致后续数据字典维护成本上升UNSIGNED属性丢失导致负数写入时报错或溢出字符集不一致源库是 utf8mb4目标库默认可能不是导致中文排序结果不同ENUM、SET类型的映射部分数据库要改成VARCHAR加 CHECK 约束部分可以直接支持需要逐项判断。这里特别说下ENUM和SET。MySQL 的 ENUM 使用非常随意很多业务代码甚至直接往 ENUM 字段里插数字索引国产库对 ENUM 的支持往往不如 MySQL 宽松。我们处理的原则是优先改写为VARCHAR 应用层校验而不是保留 ENUM。因为迁移后如果语义发生变化ENUM 内部存储的数字与字符串的映射关系不一样最容易造成查询结果错乱。常见报错可以分为几类我们在团队内部整理了一张表方便每个开发自己对照报错类型根因方向处理建议字段类型无法解析长度/精度写法不兼容改用目标库标准类型定义去掉括号默认值函数不存在CURRENT_TIMESTAMP 等语义差异调整连接模式或用触发器方案字符集/排序规则无效utf8mb4 或 general_ci 不受支持统一为库级字符集SQL 中去掉显式排序自增列建表失败语法不兼容替换为 IDENTITY 或序列方案存储过程编译失败变量声明/游标用法差异按过程对象改造清单逐项修复提示不要把表结构转换和线上切换分开看。表结构没对后续的 SQL 改造等于在沙地上盖楼。我宁愿多花两三天把 DDL 完全对齐也不愿意上线后因为类型不匹配频繁修补丁。3.2 SQL 语句层面的函数替换方案DML 层面的兼容性问题比 DDL 更隐蔽。因为 DDL 不执行就报错而 DML 是在特定的数据组合下才会出问题。我们经过了三个轮次的 SQL 适配下面几个函数替换场景是最常见的。第一个是字符串处理函数。MySQL 的SUBSTRING_INDEX、GROUP_CONCAT、FIND_IN_SET在部分国产库中不存在或不完全等价。GROUP_CONCAT在 MySQL 里用来做行转列极其顺手但目标库如果支持类似功能名字可能叫LISTAGG或者WM_CONCAT返回长度限制也不一样。我们当时的处理方式是在中间层维护一张函数映射表能替换的用 SQL 改写不能替换的拉出来单独写应用层逻辑。第二个是日期函数。MySQL 的DATE_FORMAT写法灵活很多国产库虽然支持同名函数但格式符含义存在差异比如%H与%h在部分库里可能混用。另外UNIX_TIMESTAMP、FROM_UNIXTIME这类函数在目标库上的返回精度和入参类型也可能不一致。我们因为日期函数转换导致过线上一个小故障某统计报表在整点时刻多出一行数据最后定位到是时间边界条件在源库和目标库的精度不同造成的。第三个是分页写法。MySQL 的LIMIT offset, count太深入人心但目标库可能要求LIMIT count OFFSET offset或者要求使用FETCH FIRST ... ROWS ONLY。这个改起来不难但工作量极大——几百条 SQL 如果靠手工改会非常痛苦所以我们在后面工具链环节专门做了自动化替换脚本。我把常用的函数替换和注意事项简化如下IFNULL→ 部分库支持同名函数不支持的用COALESCENOW()→ 一般通用但注意返回精度必要时用SYSTIMESTAMPDATE_ADD/DATE_SUB→ 可替换为 INTERVAL n DAY写法GROUP_CONCAT→ 确认目标库的行转列函数名称与长度限制LIMIT m, n→ 全局替换为LIMIT n OFFSET m或对应方言INSERT ... ON DUPLICATE KEY UPDATE→ 语义差异极大需要逐条评审。3.3 存储过程与定时任务的改造存储过程是这次迁移中花时间最长的部分。MySQL 的存储过程语法本身兼容性就一般换到国产库后几乎所有的过程都需要编译验证。我们当时 400 个左右的过程对象第一轮编译通过率只有六成剩下四成集中在三类问题上游标写法MySQL 的DECLARE cursor_name CURSOR FOR SELECT ...在部分国产库中需要先声明变量、再声明游标顺序不同会导致编译失败异常处理DECLARE EXIT HANDLER FOR SQLEXCEPTION的上下文语义在目标库上需要调整动态 SQLPREPARE、EXECUTE、DEALLOCATE这套语法不是每个库都完全一致尤其是拼接 SQL 时数据类型隐式转换的差异会直接影响执行计划。定时任务的改造也容易被忽略。MySQL 的EVENT是在数据库内部创建定时调度很多国产库也有类似能力但语法和调度粒度不同。我们当时有一部分定时任务原本依赖 MySQL EVENT迁移时全部统一改成了应用层的调度框架比如独立的 job 服务这样做的好处是不再依赖数据库本身的事件机制后续再做数据库版本升级或高可用切换少了一层耦合。如果你的团队没有独立调度框架也可以保留数据库事件但要提前确认目标库的 event/agent 能力否则上线后你会发现某个数怎么一直不更新查半天才发现是定时任务没跑。4. 工具链与自动化如何把改造工作量压下来既然适配工作量这么大能不能靠工具减少人肉成本我的答案是能但工具只是辅助核心映射规则还得是人来定。4.1 迁移工具对比与选型思路我们当时评估了三条路径使用厂商自带的迁移工具、使用第三方通用迁移工具、自研脚本。最后是组合使用因为各有优劣。厂商自带工具的优点是对自家数据库的适配最完整尤其是数据类型映射、自增方案、函数兼容性往往内置了最佳实践。缺点是偏向一键迁移遇到不支持的方言时黑盒化严重出了问题很难排查。第三方通用工具的优点是灵活性高、对源库的方言容忍度好缺点是映射规则普遍偏保守大量字段会被映射成宽泛类型需要人工二次修正。自研脚本则适合处理批量、规则清晰的改造场景比如建表语句清洗、LIMIT 分页改写、函数名替换。我建议的选型思路第一轮用第三方工具或厂商的全量迁移能力把数据和基础结构拉过来建立基线第二轮基于基线做全量差异校验第三轮用自研脚本对已知规则做批处理改造。层次化推进而不是指望某一个工具从头到尾全自动搞定。4.2 增量同步与机房切换的实战节奏对于不能接受长时间停机的业务增量同步是必选项。我们当时的节奏设计是三阶段全量迁移先停写或者低峰期做全量快照把全部历史数据导入目标库增量追平通过日志解析方式持续同步源库的增量变更到目标库追平到分钟级延迟切换窗口在业务低峰期暂停源库写入最后一次增量追平校验通过后切换读流量再切换写流量。关于增量同步的坑多说一句不同源的日志解析机制差别很大且并不仅仅支持 MySQL binlog 这一种协议。如果你的目标库是另一套体系增量同步往往需要通过中间件或自定义接口实现而不是简单配置一个 binlog 订阅就完事。我们当时花了相当多时间调试同步任务的位点一致性和幂等性期间还遇到因为大事务导致的延迟突刺。建议大家提前做好延迟监控和自动告警不要到切换窗口才发现同步落后太多那会非常被动。4.3 脚本化改写正则与模板结合自研脚本改造是最能体现迁移成本优化的部分。我们针对已知的规则集写了一个 Python 脚本流水线大致做了这几件事DDL 清洗读入 CREATE TABLE 语句正则匹配\b(INT|TINYINT|SMALLINT|MEDIUMINT|BIGINT)\(\d\)去掉显示宽度匹配UNSIGNED属性按字段精度表决定保留或忽略。默认值替换匹配DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP统一替换为触发器方案或目标库兼容写法。分页改写匹配LIMIT\s(\d)\s*,\s*(\d)替换为LIMIT \2 OFFSET \1并对每条替换后的 SQL 做语法校验。函数映射维护一个函数替换词表例如IFNULL→COALESCESUBSTRING_INDEX→ 固定改写模板替换后人工抽查。这套脚本的价值不只是减少手工量更重要的是可重复执行。因为改造过程不是一次性的你改完一批跑一轮测试发现问题可能又要从源库重新导出最新 DDL 再做一次。脚本化的映射规则让这一过程从小时级降到分钟级。提示没有银弹。脚本改写永远会漏掉特例所以必须在流水线后接一轮全量 SQL 静态检查 核心路径功能回归别把脚本输出当最终版直接上线。5. 验证与复盘迁移完不等于结束迁移完成、应用切流成功只能算上线离没问题还有很长一段路。数据一致性、功能正确性、性能稳定性每一项都需要系统性的验证。5.1 数据一致性校验方案数据校验建议分三层第一层是行级总量校验每个表的行数必须一致。这个简单但能立刻发现漏数据或重复数据。第二层是字段级 HASH 校验对每一行数据进行归一化处理后计算 HASH 值两边比对。对待特殊字段要单独处理比如浮点数的精度差异、日期时间的时区差异、文本类型的末尾空格规则这些如果不归一化HASH 比对结果永远是红的变成狼来了。第三层是抽样明细比对特别是 DECIMAL、日期时间、长文本字段按业务主键抽样后逐字段比对。我们当时抽了十万行左右的关键业务数据专门核对金额类字段、状态类字段和交易时间字段。三层校验做完后我把结果打印成一张报表每个表一行列出差值数和具体的差异样例。这份报表不仅是验收依据也是和业务方、管理方沟通的最有效工具比任何口头汇报都有说服力。5.2 性能回放与慢 SQL 治理数据一致不代表性能一致。同一条 SQL在 MySQL 上走索引在国产库上可能因为优化器差异转换成全表扫描。这个必须通过压测和回放来验证。我们的做法是从源库的慢日志和审计日志里提取一段时间的真实 SQL脱敏后在目标库上基于相同数据量进行回放对比每个 SQL 的耗时分布。回放结果出来后重点看两类从快变慢的 SQL 和新增的超时 SQL。性能治理时优先排查以下几点统计信息是否更新某些国产库在导入大量数据后不会自动做统计信息收集需要手动ANALYZE或等价操作索引是否真正被使用通过执行计划确认不能只看建了索引就放心隐式转换是否导致索引失效比如字段是 VARCHARSQL 里传了数值类型MySQL 会偷偷转换目标库可能直接放弃索引分页深度深分页在 MySQL 上就有性能问题换库后可能更严重需要改为基于游标或延迟关联的方案。慢 SQL 治理没有捷径就是一条条过。但有了回放工具至少能知道哪些需要过省去了靠用户反馈来查问题的时间。5.3 实际花费的钱和预期差多少最后说说成本。我们初始估算时把大头押在了数据搬迁上实际执行完发现完全反了。用一张表总结一下我们的估算偏差希望对你做预算有帮助成本项预估占比实际占比差异原因数据搬迁35%12%网络和导入工具的吞吐比预期高很多表结构改造15%28%DECIMAL 精度、ENUM、默认值等细节远超预期SQL 兼容改造25%35%函数差异、存储过程改造工作量被严重低估回归验证与联调25%25%和数据搬迁没有明显偏差但耗时绝对值不小所以如果你现在要启动类似项目我的建议是预算里至少把数据搬迁这一块砍掉一半加到类型适配 SQL 改造 回归验证上。工具迁移跑得再快也不如团队对类型差异的理解深入来得有价值。6. 遗留问题与事后反思有些坑是必然要踩的项目收尾时我团队内部做过一次复盘。有几个认知上的转变我觉得值得说出来因为它直接影响下一个项目的启动方式。6.1 不要迷信兼容模式不少国产数据库提供了一种兼容 MySQL的运行模式听起来好像是救命稻草。实际用下来我的感受是兼容模式解决的是能不能跑起来的问题解决不了跑得好不好、跑得对不对的问题。它为了兼容性可能在语义上做了妥协比如默认开启了一些宽松的排序规则、日期格式或者放宽了类型长度限制。这些妥协放在生产环境里就是看不见的数据隐患。所以我们的原则是开发联调阶段可以用兼容模式节省时间但上线前必须切到标准化模式把所有隐藏的兼容性依赖显式暴露出来逐一修复。6.2 迁移项目实际上是一个存量改造项目很多人把数据库迁移理解为一个导入导出的工程低估了存量代码改造的工作量。实际走下来这更像是一个存量改造项目业务 SQL 像胶水一样粘在 MySQL 的特有语法上你要做的是一点点把这些胶水剥离换成更通用、更规范的表达方式。从好的方面看这次改造倒逼我们把很多能跑就行的 SQL 重写成了更健壮的版本比如去掉了隐式转换、消除了深分页也算是一种技术债的偿还。6.3 打磨一套自查清单比临时拉人救火更重要如果我们一开始就把上面提到的差异点整理成一份自查清单在第一轮盘点时就逐项打勾很多返工会完全避免。比如下面这份简化版你下一个项目可以直接拿去改一版用[ ] 全库 DECIMAL 字段精度、标度是否全部对齐[ ] 所有自增主键和取回自增值的代码是否确认方案[ ] ENUM/SET 类型是否已全部评审并确定改写方案[ ] TIMESTAMP/DATETIME 默认值与更新行为是否一致[ ] 时区配置是否在连接层统一[ ] LIMIT、GROUP_CONCAT、IFNULL 等函数是否全量扫描[ ] 存储过程、触发器、定时任务是否全部编译通过[ ] 数据校验三层总量、HASH、抽样是否跑完[ ] 性能回放中的从快变慢 SQL 是否清零[ ] 是否已关闭兼容模式并完成回归把这份清单走完不敢说迁移一定成功但至少不会在最后一刻才发现某个字段类型把业务数据搞坏了。最后再分享一个小技巧迁移完成后保留一套源库 目标库的双写环境一段时间。不要急着销毁老库很多业务问题只有在真实流量下才会暴露有了双写环境你可以在切流后的前两周持续做数据比对和差异分析。等连续跑过两到三个业务高峰、差异为零之后再停掉双写这才算真正收尾。这个习惯让我避免过至少一次因为日期时间边界差异导致的数据返工也推荐你试试。
返回列表