ARTICLE DETAIL

资讯详情

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

MySQL跨库迁移零修改实战:语义兼容与内核优化全解析

MySQL跨库迁移零修改实战:语义兼容与内核优化全解析 1. 为什么“零修改”迁移在工程上这么难其实很多团队接到数据库迁移任务时第一反应都是把 SQL 拷过来跑一遍能跑通就算兼容。但真实情况是数据库兼容性从来不是一个“能不能执行”的问题而是“执行出来的结果是不是和原来一模一样”的问题。MySQL 兼容性之所以值得做深度解析是因为大量应用真正依赖的是源数据库的“语义惯性”——空值是 NULL 还是空串、字符串比较是否区分大小写、事务并发下读取的到底是哪个版本的数据。这些行为一旦发生变化系统不会立刻崩掉出错的是业务数据本身。我去年参与过一个从 Oracle 迁移到 MySQL 8.0 的工程24 个业务模块200 多张表。前期做兼容性扫描时发现单独的 SQL 改写其实不难难的是几类“运行时才暴露”的差异。比如源库默认隔离级别是 READ COMMITTED而 InnoDB 默认是 REPEATABLE READ源库里空字符串写入后查询结果就相当于 NULLMySQL 里这两个是完全不同的值源库的自增主键由序列生成业务代码直接引用 sequence.nextvalMySQL 根本没有这个对象。这些问题没有一个能在“语法能不能跑通”的层面暴露出来全部要放在语义层和行为层去排查。所以我一般会对项目组说“零修改”不是拍脑袋定出来的目标而是通过兼容性评估、内核级优化、迁移流程控制最终逼近出来的结果。它不是一种无条件承诺更像一个工程约束数据库侧必须把语义差异消化掉应用侧只在万不得已时才动手。1.1 语法兼容、语义兼容、行为兼容三个完全不同的问题语法兼容最简单无非是函数名、关键字、操作符的差异。比如 Oracle 的 NVL、SYSDATE到了 MySQL 里要换成 IFNULL/COALESCE、NOW()。这种问题用脚本扫描加人工 review 就能覆盖绝大部分。语义兼容比语法兼容高一个难度等级。典型例子是空字符串和 NULL 的关系。Oracle 里写入 会被当成 NULL所以在 WHERE 条件里判断col IS NULL能查到那些“空值”数据MySQL 里 是实实在在的空字符串和 NULL 是两种值。结构迁移时如果不把源库数据的“空值”语义理清楚业务原本依赖的空值判断就会悄悄漏数据。这种差异看起来微小上线后影响的是报表统计、去重逻辑非常隐蔽。行为兼容是最高难度。它指的是在相同并发、相同事务隔离级别下数据库表现出的锁行为、死锁概率、读取快照语义要尽量一致。MySQL 的 REPEATABLE READ 有一类间隙锁gap lock在并发写入场景下比 READ COMMITTED 更容易出现锁等待和死锁。如果不做内核级参数对齐迁移后的系统可能在测试环境一切正常一到业务高峰就频繁锁等待这是很多切换事故的根源。1.2 零修改的合理边界在哪必须承认有些数据库特性在 MySQL 里没有直接等价物。Oracle 的物化视图MySQL 只能用普通视图加定时刷表来模拟Oracle 的自治事务AUTONOMOUS_TRANSACTION在 MySQL 里没有一个语句能搞定序列对象与触发器生成主键的方式也完全不同PostgreSQL 的部分窗口函数写法、SQL Server 的表值函数迁移时都需要改写。我做评估时通常把改造点分为三档。第一档是数据库侧完全可以消化的比如字符集、sql_mode、隔离级别、连接参数工作量几乎为零。第二档是需要 SQL 级等价改写的比如 NVL 换 IFNULL、ROWNUM 分页换 LIMIT工作量可控影响面主要在持久层。第三档是必须做业务逻辑调整的比如自治事务、序列引用、跨库关联查询要单独排期不能藏着。真正健康的迁移做法是把第一档全部吃掉把第二档控制到最小把第三档提前暴露并单独管理。能做到这一步“零修改”就不是一句空话而是一个可交付的工程结果。2. 内核维度拆解MySQL 兼容性的本质是语义对齐很多人听到“内核级优化”第一反应是改几个内存参数。实际上在迁移场景里内核级优化的真正目标是让行为对齐。MySQL 的兼容性不是一个开关而是由存储引擎、事务系统、优化器、字符集体系和参数体系共同决定的。要理解兼容性就得先理解下面四层。2.1 数据类型与隐式转换NUMBER 到 INT/BIGINT/DECIMAL 的映射源库类型到 MySQL 类型的映射是所有结构迁移的第一步也是最容易埋雷的一步。Oracle 的 NUMBER 不带精度时业务里可能存整数、小数、大数迁移时统统映射成 DOUBLE 是最快但最危险的做法浮点精度会让金额字段出现 0.10.2 不等于 0.3 的问题。正确做法是先统计每一列的取值范围整型映射 INT/BIGINT带小数的映射 DECIMAL(p,s)超大整数值用 DECIMAL(65,0) 顶着。-- 迁移前用元数据表查一下字段类型分布 SELECT table_name, column_name, data_type, numeric_precision, numeric_scale FROM information_schema.columns WHERE table_schema SOURCE_SCHEMA AND (data_type NUMBER OR data_type NUMERIC) ORDER BY table_name, ordinal_position;核心原则是宁可先用宽类型接收也不要为了“看起来合适”而牺牲精度。Oracle 的 VARCHAR2(4000) 迁到 MySQL 时如果一行里多个大 varchar 很容易超出 65535 字节的 row size 限制这种情况下要改用 TEXT 类型。CLOB 对应 TEXT 或 LONGTEXTBLOB 对应 LONGBLOB注意 LONGTEXT 参与临时表和排序时占用空间很大必要时要限制返回字段避免整行大文本被拉到内存。隐式转换是另一个经典暗坑。MySQL 里如果 varchar 列和数字比较比如WHERE order_no 123456MySQL 会把 order_no 转换成数字再比较结果是这一整列都没法走索引。这类问题在老系统里几乎必有尤其那些压根没注意列类型的表。迁移后的慢查询排查第一优先就是看这类隐式转换。2.2 空值语义空字符串和 NULL 的“天地之别”Oracle 的神奇行为是往 VARCHAR2 列插入 实际存储的是 NULL判断条件里col 在 Oracle 里永远匹配不到数据因为 IS NULL 才是它的归宿。MySQL 遵循标准 就是空字符串NULL 就是 NULL两者都是合法值。这意味着三件事要处理。第一存量数据在迁移时必须明确转换策略Oracle 中的 NULL 和原来写入的 其实都是 NULL是按 NULL 搬还是统一转成 MySQL 的 这必须由业务语义决定不能由迁移工具默认拍板。第二SQL 条件里的col 在 MySQL 能匹配到空串但匹配不到 NULL原来“查空值”的逻辑可能漏数据col IS NULL又查不到空串。第三NOT NULL 约束的行为也变了Oracle 中 INSERT 一个 到 NOT NULL 列会直接报错因为被当作 NULLMySQL 却能成功插入空串。我的经验是在兼容性评估阶段把所有对空值的依赖条件清单列出来逐条确认目标行为。不要指望一条 SQL_MODE 或一个全局替换能解决所有空值问题这类问题必须靠业务语义来定。实践上很多团队会先在 MySQL 侧统一成空串如果应用侧大量使用IS NULL判断则反过来用 NULL 统一。重点是不改 SQL 的前提下先把数据行为对齐。2.3 事务隔离与锁行为RR 和 RC 的差距不只是文档里的一句话Oracle 默认隔离级别是 READ COMMITTEDPostgreSQL 默认也是 READ COMMITTEDSQL Server 默认同样是 READ COMMITTED。而 MySQL InnoDB 的默认隔离级别是 REPEATABLE READ。许多迁移团队忽略了这件事结果就是原本在源库并发下不会冲突的写事务迁移后开始出现 gap lock 导致的锁等待甚至死锁。间隙锁的作用是防止幻读在 REPEATABLE READ 级别下一个范围查询不仅仅锁住已有的行还会锁住范围内可以插入新行的间隙。两个事务分别插入不同的行但落入了相同的间隙就可能互相等待。这类问题在低并发测试时完全无法复现只有生产流量涌进来才会爆发。对大多数 OLTP 应用来说把隔离级别调整为 READ COMMITTED 是最稳妥的内核级对齐手段[mysqld] transaction-isolationREAD-COMMITTED这里要补充一句将隔离级别改为 RC 会降低可重复读保障。如果源库本来就是 RC业务里没有依赖“一个事务内多次读取结果必须一致”的写法改成 RC 是安全的。但如果源库是 RR比如从 MySQL 5.7 迁到 8.0就不要动。迁移的本质是向“源库语义”对齐而不是向“MySQL 默认值”看齐。2.4 字符集、排序规则与大小写敏感性MySQL 有四级字符集设置服务器级、数据库级、表级、列级。这既是灵活性也是混乱的根源。常见的坑是数据库建好了是 utf8mb4表却是 latin1列又继承了别的排序规则最终导致 join 时隐式转换、索引失效或者排序结果错乱。从商业数据库迁移过来的系统源库字符集往往是 AL32UTF8Oracle或者 GBK下意识就想着 MySQL 用 utf8mb4 接住一切这方向没错但要特别关注排序规则collation。utf8mb4_general_ci、utf8mb4_unicode_ci、utf8mb4_0900_ai_ci对大小写和重音的敏感度不同直接影响唯一索引行为和查询结果。如果源库对字符串比较区分大小写迁移后却默认用了不区分大小写的 collation业务里WHERE user_code AbCd会突然多查出来几条记录。可参考的铁律是全链路统一 utf8mb4 加业务期望的 collation连接层用SET NAMES utf8mb4JDBC URL 里声明characterEncodingUTF-8useUnicodetrue。如果旧系统希望区分大小写选择utf8mb4_bin最接近二进制比较避免utf8mb4_0900_ai_ci带来的意外宽松匹配。表名大小写在 Linux 上受lower_case_table_names控制0 表示区分、1 表示不区分迁移前就要定好因为改这个参数需要重启实例且影响已建立的库名属于项目初期必须敲定的基础设施参数。3. 跨库迁移最常见的八个“不兼容现场”虽然源库五花八门但迁移到 MySQL 时踩到的差异高度相似。我把实际项目里遇到频率最高的八类问题列出来每类都给出判断标准和处理思路方便大家对照做兼容性清单。下面先给一张 Oracle 到 MySQL 的常见类型映射参考表Oracle 类型MySQL 映射备注NUMBER(10)INT整型NUMBER(12,2)DECIMAL(12,2)金额类必须用 DECIMALNUMBERDECIMAL(65,0) 或 DECIMAL(p,s)不带精度时按实际范围映射VARCHAR2(n)VARCHAR(n)注意一行多个大 varchar 的 row size 限制DATEDATETIMEOracle 的 DATE 带时分秒TIMESTAMPDATETIME/TIMESTAMP需要自动更新时用 TIMESTAMPCLOBLONGTEXT大文本BLOB/RAWLONGBLOB/VARBINARY二进制数据NUMBER(1) 0/1TINYINT(1)布尔语义建议按业务来定3.1 分页逻辑差异Oracle 传统写法是WHERE ROWNUM N加上外层排序很容易出现“先取 N 行再排序”和“排序后再取 N 行”的逻辑错位。12c 以后开始支持OFFSET n ROWS FETCH NEXT m ROWS ONLYSQL Server 用TOPPostgreSQL 用LIMIT/OFFSET。MySQL 只认LIMIT offset, count。这里要小心的不是语法本身而是分页的语义。源库如果用了 ROWNUM 和排序混合迁移前一定要人工确认是先全量排序再分页还是先分页再排序。很多老系统为了性能故意用ROWNUM N提前截断逻辑上并不是严格分页。这类 SQL 迁到 MySQL 时必须重写不能直接套 LIMIT。3.2 日期时间的默认值与范围Oracle 的 DATE 类型本身就带时分秒迁移到 MySQL 时很多人直接映射成 DATE结果时间部分神秘消失。正确映射是只存年月日用 DATE需要时分秒用 DATETIME需要自动记录当前时间用 TIMESTAMP 配合DEFAULT CURRENT_TIMESTAMP。TIMESTAMP 的范围到 2038 年虽然听起来很远但存“排期、到期时间”这类字段时确实会踩建议统一用 DATETIME范围到 9999 年没有 2038 问题。日期函数差异也很常见TO_CHAR 换 DATE_FORMATTO_DATE 换 STR_TO_DATESYSDATE 换 NOW()。更需要注意的是默认值。老库可能允许日期列默认值是 SYSDATEMySQL 里 DATETIME 默认值可以用表达式8.0.13 之后支持更完整但如果源库存在 0000-00-00 这类非法日期MySQL 在严格模式下会直接拒绝。迁移时要么清洗数据要么临时调低 sql_mode等数据修好再恢复。3.3 自增主键与序列这可能是最难实现“零修改”的一块。Oracle 开发者喜欢直接SELECT seq.NEXTVAL FROM dual然后插入主键SQL Server 也类似。MySQL 只有 AUTO_INCREMENT 这一套。如果业务代码大量硬编码了序列调用这部分几乎不可能零修改只能二选一应用层可改造把 seq.nextval 改成 insert 后通过LAST_INSERT_ID()拿主键。应用层不可改造在 MySQL 里用一张序列表加存储函数模拟序列。虽然能实现SELECT nextval(seq)风格的调用但并发压力会集中在一行热数据上性能和锁风险都很高只推荐低频场景使用。存量数据的自增起点也要手工处理。MySQL 默认从 1 自增如果不处理新插入的主键可能和老数据冲突。正确做法是迁移后执行ALTER TABLE t AUTO_INCREMENT 1000000把起点放在存量最大值之后。3.4 存储过程、触发器与函数MySQL 的存储过程语法和 Oracle/PostgreSQL 差异很大。变量声明方式、FOR 循环、异常处理、动态 SQL、包对象Oracle 的 PACKAGE都没有直接对应物。触发器迁移时不要指望语法直接翻译而是要按 MySQL 的触发器语法重写并且验证触发器在复制链路中的副作用。如果一个触发器里写了很多业务逻辑迁移到 MySQL 后反而建议借机把逻辑上浮到应用层因为 MySQL 存储过程的调试、版本管理、性能诊断都比主流商业库麻烦长期维护成本偏高。3.5 布尔值与特殊类型Oracle 没有原生布尔列类型常用 NUMBER(1) 0/1 表达。MySQL 的 BOOLEAN 本质是 TINYINT(1)写入 TRUE/FALSE 会变成 1/0。这本身没太大问题但要小心 JDBC 读取 tinyint(1) 时可能被驱动转换成 boolean如果字段里存了 2行为就会变得诡异。迁移时如果源库某个 NUMBER(1) 列语义上是个枚举千万不要映射成 BOOLEAN老老实实映射 TINYINT 或 SMALLINT。还有 ENUM 类型虽然方便但扩展性差加一个枚举值可能就要 DDL。从其他库迁过来的系统如果不是性能敏感建议映射成 VARCHAR 加应用侧校验让后续扩展更容易。3.6 视图和物化视图普通视图的迁移相对容易MySQL 8.0 也支持WITH CHECK OPTION。真正麻烦的是物化视图。Oracle 和 PostgreSQL 的物化视图在 MySQL 中没有直接等价物常见替代方案是用定时任务刷新一张实体表或者应用层做缓存。这里需要业务侧接受一个延迟如果之前物化视图刷新频率是分钟级替代方案设置同样的刷新粒度即可。另一个容易忽略的是视图定义里的函数引用。Oracle 视图里的 TO_CHAR、TRUNC到了 MySQL 要全部改成 DATE_FORMAT、CAST。视图层次深、嵌套多时改写工作量不小。建议专门做一轮“视图展开审查”先分析依赖关系把公共表达式提取出来再批量生成新定义。3.7 连接认证与驱动兼容这一点最容易在新老环境切换时炸掉。MySQL 8.0 默认认证插件是 caching_sha2_password老版本 MySQL 5.7 用的是 mysql_native_password。如果应用还在用老 JDBC 驱动连接 8.0会直接报 Public Key Retrieval is not allowed、Unsupported authentication plugin 或 SSL 相关错误。处理办法有两个。短期方案把应用账号的认证方式改回 mysql_native_passwordCREATE USER app% IDENTIFIED WITH mysql_native_password BY password;长期方案升级应用驱动到支持 caching_sha2_password 的版本同时留意 SSL 配置设置正确的连接参数。迁移项目里很多“新集群起不来”的故障其实都是这类连接层问题和业务 SQL 一点关系都没有但在兼容性评估时却经常被漏掉。3.8 SQL_MODE 带来的隐性行为切换SQL_MODE 是 MySQL 独有的参数组合直接影响服务的严格程度。STRICT_TRANS_TABLES开启时插入非法日期、超长字符串会直接报错关闭时MySQL 会静默地把非法值转成边界值或 NULL。ONLY_FULL_GROUP_BY开启后SELECT 里的非聚合列必须出现在 GROUP BY 里老系统那套“随便 select 非聚合列”的写法会立刻报错。迁移期有一个很现实的选择要不要临时关闭某些模式来减少改造量我的建议是STRICT_TRANS_TABLES和NO_ENGINE_SUBSTITUTION必须保留它们是数据完整性的底线ONLY_FULL_GROUP_BY可以临时关闭但要多做一层静态扫描把依赖它的 SQL 都找出来排期改造不能让它成为永久缓存。如果交付后三个月还没处理那这个项目的“零修改”就是从技术债务里借出来的早晚要还。4. 内核级优化迁移前就把系统调到业务期望的形态迁移和从零建库不同从零建库可以用默认参数起步迁移却要为目标工作负载提前定型。我的习惯是在存量数据搬迁之前就把下面几组参数调好避免数据搬完了再重启导致业务中断。4.1 InnoDB 缓冲池、redo log 和刷盘策略innodb_buffer_pool_size是 MySQL 性能的第一大变量。对纯 OLTP 系统通常可以设置到物理内存的 60% 到 70%比如 128G 内存的机器给 80G。不是越大越好要给连接缓存、排序缓冲、临时表和操作系统页缓存留出余量。迁移前可以先看一组状态值SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read_requests; SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_reads;逻辑读和物理读的比值能反映缓存命中率。如果物理读占比过高优先调大 buffer pool而不是急着调 SQL。innodb_log_file_size同样关键默认值偏小会导致高频 checkpoint日志写入压力大。8.0.30 以上可以直接用innodb_redo_log_capacity控制 redo log 总容量老版本则调innodb_log_file_size到 4GB 或 8GB。刷盘策略方面innodb_flush_log_at_trx_commit的取值要结合业务对数据丢失的容忍度。金融核心交易建议保持默认 1每次提交都刷盘报表、日志、缓存类业务设成 2 可以大幅降低磁盘 IO 压力代价是数据库崩溃时可能丢失最近一秒的事务。innodb_flush_method推荐 O_DIRECT绕开操作系统页缓存避免双重缓冲浪费内存。4.2 binlog、复制与 GTID高可用底座要提前铺好迁移工程一般会面临很长的增量同步窗口binlog 配置直接影响同步的可靠性和速度。binlog_format必须设置为 ROW这是数据一致性校验和下游消费的最稳选择。ROW 格式下 binlog 会明显膨胀所以要同时调大max_allowed_packet否则一个大事务比如批量 UPDATE 几十万行可能在传输时直接中断。GTID 是现代 MySQL 复制的最佳实践。开启 GTID 后主从切换、增量追平、故障定位都简单得多。迁移期如果要搭临时复制链路追增量GTID 还能避免手工定位 binlog 文件名和 position。参考配置[mysqld] server-id1003306 gtid_modeON enforce_gtid_consistencyON binlog_formatROW binlog_row_imageFULL log_binmysql-bin sync_binlog1 max_allowed_packet256Msync_binlog1和innodb_flush_log_at_trx_commit1的配合是“双 1”配置数据可靠性最高但写压力也最大。迁移前期如果只想快速追平数据不想被磁盘 IO 拖慢可以暂时降级切生产前必须调回来。4.3 连接层与并发控制参数MySQL 默认max_connections是 151对生产系统基本都太小。迁移项目里应用侧连接池如果不收敛很容易打满连接数。DBA 可以先看SHOW STATUS LIKE Threads_connected的峰值再按应用连接池的 maxActive 加起来得出理论峰值加 30% 余量设置。我见过一些系统把 max_connections 直接开到 5000MySQL 为每个连接分配的内存和资源随之膨胀反而把 CPU 拖垮。更合理的方式是同时收敛应用连接池比如 HikariCP 的 maximumPoolSize 控制在 50 以内微服务实例少开几个比把数据库连接数顶上去更健康。table_open_cache和thread_cache_size也要按需调整表数量多的系统要把 table_open_cache 调高短连接频繁的系统调高 thread_cache_size。4.4 SQL_MODE 与隔离级别迁移期的“兼容模式”怎么开SQL_MODE 没有绝对标准核心是保持一个可解释、可审计的状态。我建议的基线配置是sql_modeSTRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION如果老系统存在零日期、除零等问题可以在迁移验收阶段临时加上NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO帮助暴露脏数据等数据清洗完成后再回到稳定状态。ONLY_FULL_GROUP_BY是否开启取决于存量 SQL 的整改进度不强制一开始就打开但要列入后续改造清单。隔离级别在前面已经提过这里强调一个原则向源库看齐。源库默认 RC迁移后就把 MySQL 的 global 层设置成 RC源库是 RR就保持 MySQL 的 RR。不要因为“MySQL 默认是 RR”就什么都不动也不要因为“网上说 RC 性能好”就乱改。隔离级别影响的是事务行为改这个参数的代价远大于收益。5. 零修改迁移工程的五个落地阶段理论讲完落到地上就是流程。这一节把整个迁移项目拆成五个阶段每个阶段的关键动作和验收标准都写清楚。这套流程是多个 TB 级数据项目打磨下来的从停机窗口 2 小时到周末窗口都有覆盖。5.1 阶段一对象与 SQL 静态扫描开始迁移的第一个动作不是导出数据而是把所有元数据拉出来形成完整对象清单表、索引、约束、主键、视图、触发器、存储过程、函数、序列、同义词、物化视图。MySQL 侧可以从 information_schema 快速生成存量清单SELECT table_schema, table_name, table_type, engine FROM information_schema.tables WHERE table_schema target_db;对象清单的意义不只是统计数量而是为改造量估算打底。比如发现源库有 300 个视图其中 80 个引用物化视图那 DDL 层改造的工作量基本就锁死了。同时业务 SQL 的静态扫描要并行启动重点搜NVL、SYSDATE、ROWNUM、TO_CHAR、seq.nextval、TRUNC(date)、CONNECT BY这些关键词统计出需要 SQL 改写的百分比。看到这个数字你才能判断“零修改”是不是可达成的目标。5.2 阶段二结构与存量数据搬迁结构迁移建议手写 DDL 脚本或者用成熟工具转换后再人工 review。自动转换能缩短起点但绝不能直接信任输出结果因为数据类型映射、字符集、自增起点这些细节工具判断不了业务语义。数据搬迁则根据数据量分两派数据量小于 200G、停机窗口充足用 mysqldump 或 mydumper 逻辑导出导入。mysqldump --single-transaction --set-gtid-purgedOFF --default-character-setutf8mb4适合低负载环境mydumper 可以多线程导出导入速度更快。数据量超过几百 G、希望缩短停机时间用物理备份方案比如 Percona XtraBackup 对源 MySQL 做全量备份在目标库恢复后直接作为种子库。但物理备份只适用于 MySQL 到 MySQLOracle 到 MySQL 只能走逻辑导出加 CDC 增量路线。搬数期间另一个重要动作是建索引的顺序。如果先建好所有索引再导数据插入速度会明显变慢。常见实践是先导数据再建二级索引最后加约束。外键约束如果源库就有建议保留但要提前提醒业务方MySQL 的外键在 DDL 变更时比商业库更容易锁表后续扩容要小心。5.3 阶段三增量同步与复制链路搭建存量搬完后源库还在继续产生新数据这时需要增量同步把两端拉齐。如果源库也是 MySQL最直接的方式就是搭主从复制-- 在目标库作为新主库的备机执行MySQL 8.0.23 使用 CHANGE REPLICATION SOURCE TO CHANGE REPLICATION SOURCE TO SOURCE_HOSTsource_host, SOURCE_PORT3306, SOURCE_USERrepl, SOURCE_PASSWORDrepl_password, SOURCE_AUTO_POSITION1; START REPLICA;如果源库是 Oracle 或 PostgreSQL则要借助 CDC 工具解析源库日志把变更流写入目标 MySQL。这个阶段最容易踩的坑有三个。第一源库 binlog 保留时间太短复制链路故障后无法找回位点所以迁移前要把 binlog 过期时间临时调长比如 8.0 里设置binlog_expire_logs_seconds604800也就是 7 天。第二大事务导致max_allowed_packet不够同步直接中断。第三字符集不一致导致增量应用到目标库时乱码或长度超限。建议在增量阶段每天跑一次数据量对账把关键表的 count(*) 和源库比对发现问题越早后面切流越从容。5.4 阶段四数据一致性校验增量追平到接近实时后一致性校验是切换前最后一道防线。最简单的手段是抽样对比但抽样很难发现数据倾斜。生产环境我推荐使用 Percona Toolkit 的 pt-table-checksum它会按主键把表分块算出每块的校验和在主从两边比对pt-table-checksum h127.0.0.1,uchecksum,p****,Dsource_db \ --replicate-check-only这个工具不仅能发现不一致还会输出差异块的确切范围方便定向修复。对于没有主键的大表历史表里经常有需要提前补主键或者在工具上做特殊处理否则没法分块。修复不一致时不要直接全表删了重灌那会把增量位点搞乱最好针对差异块做定向修正或者把该表的增量回放逻辑检查一遍定位是哪个环节导致的数据偏差。5.5 阶段五灰度切换与回滚预案切换阶段最重要的原则是永远不要把所有流量一次性切过去。哪怕停了写也要先让部分只读流量、部分报表任务或者干脆某个低风险业务模块先走新库跑一段时间观察错误日志和慢查询。有一个很简单的切流策略先切 5% 的读流量跑 30 分钟再切 30%跑 1 小时没问题了再全切写流量。这个过程虽然占时间但能避免很多“上线即事故”的场景。回滚预案必须在切换前写死源库保留只读状态数据反向同步机制要提前验证。如果切换后新库出了问题要能快速把源库恢复为可写并利用停机窗口把增量数据反向追回。这里有个特别容易忽略的细节反向追数据时JDBC 连接、应用缓存、消息队列里的未消费消息都要考虑进去不是数据库两边数据一致就万事大吉。6. 迁移后性能回退的排查套路与内核侧调优余量迁移上线不代表项目结束。真正开始打仗的是上线后的前两周各种慢 SQL、锁等待、CPU 飙升会在真实流量下暴露出来。这一节我把最常见的性能回退场景和排查手段整理出来。6.1 先从 EXPLAIN 开始找执行计划差异拿到一条慢 SQL第一件事不是改参数而是看它的执行计划在源库和目标库上的差异。MySQL 的 EXPLAIN 输出里尤其要注意 type 列如果从 ref 变成了 ALL全表扫描基本可以断定索引选择出了问题如果 key 列是 NULL说明优化器压根没选索引。EXPLAIN SELECT * FROM orders WHERE customer_id 123 AND status PAID;最常见的原因有三类统计信息还没更新优化器对行数估计错误字段类型或字符集不同导致联接条件需要隐式转换源库用了函数索引或表达式索引MySQL 没有直接对应物。排查阶段可以先执行ANALYZE TABLE orders刷新统计信息再看执行计划是否变化。这一步能解决大约三成的执行计划问题。6.2 字符集隐式转换导致索引“消失”执行计划从 ref 变成 ALL还有一种非常隐蔽的原因表字段是 utf8mb4另一个表字段是 utf8mb4_bin 或 latin1JOIN 时 MySQL 必须做字符集转换索引就废了。还有一种情况是两边字段类型一个是 BIGINT、一个是 INT虽然不会报错但比较时也要走转换。检查方法很简单直接看两张表的字段定义SHOW CREATE TABLE对不上就调整 DDL 或 SQL让两边类型完全一致。我见过不少团队在这上面耗了几天最终发现只是utf8mb4_general_ci和utf8mb4_0900_ai_ci的差异统一排序规则后查询速度立竿见影。6.3 深分页、大事务与自增锁策略从其他库迁过来的系统分页习惯往往是从第 1 页开始翻翻到几千页以后才会出问题。深分页必须扫描前面所有的行再丢弃MySQL 处理LIMIT 1000000, 20时会一路扫到第 1000020 行这是索引也救不了的。应对方式有几类业务上限制最大翻页深度用游标分页比如WHERE id last_id ORDER BY id LIMIT 20或者用覆盖索引先查出主键再回表取整行。迁移后做一轮慢查询扫描深度分页几乎必现。大事务同样要盯紧。源库如果是 Oracle重做日志和 UNDO 空间都很大DBA 对单个大事务可能没概念MySQL 的 redo 和 binlog 在默认配置下对大事务极不友好。迁移后如果发现 binlog 写入暴涨应用端又有长事务就要考虑把批量操作拆成小批次每批 1000 到 5000 行提交一次。如果用到INSERT ... SELECT这类批量插入还要注意自增锁的粒度。innodb_autoinc_lock_mode2配合 ROW 格式 binlog 能减少锁竞争但前提是 binlog_format 必须为 ROW否则主从自增值会不一致。6.4 统计信息、优化器开关与慢查询闭环MySQL 的优化器不是万能的但它的决策依据是透明的。慢查询日志是迁移后最重要的观测入口[mysqld] slow_query_logON slow_query_log_file/var/log/mysql/slow.log long_query_time1 log_queries_not_using_indexesON开了之后每天用 pt-query-digest 跑一遍按总执行时间和执行次数排序。排在最前面的通常就是那么十几条 SQL把它们全部优化完整体性能就能上一个台阶。不要一上来就碰 optimizer_switch最多考虑把range_optimizer_max_mem_size调大帮助优化器在复杂 IN 条件上生成更优计划。大多数情况下把统计信息维护好、把数据模型和索引修对就已经能解决九成的问题。迁移后头两周应用连接池的重试逻辑和 MySQL 的行为也要协同。MySQL 8.0 在部分错误场景下会直接断开连接老应用如果没配好连接有效性检查会出现大量 Communications link failure。这也是内核级优化的一部分属于数据库和中间件的衔接地带。根据我个人经验迁移项目里最容易被低估的从来不是数据量而是这些运行时的行为差异。提前做好兼容性拆解、内核参数对齐和灰度回滚预案比事后排查要省力得多。
返回列表