做数据库迁移,最怕的不是数据搬不过去,而是搬过去之后应用起不来。最近我完整跟完了一个 MySQL 到电科金仓(KingbaseES,下称金仓)的迁移项目,从结构评估、数据搬运到应用切换、性能调优,前后踩了不少坑,也总结出一套能直接复用的打法。这篇笔记不画饼,只讲真实项目里遇到的东西:哪些环节能用工具省事,哪些必须人工改写,哪些报错你迟早会碰到。适合正在做国产化替代的 DBA、后端开发,以及所有准备把 MySQL 业务迁到金仓的团队。
整个项目背景大概是这样的:业务系统是 Java Spring Boot 技术栈,ORM 用的 MyBatis,连接池用的 Druid,源库是 MySQL 5.7,接近两千张表,总数据量在 TB 级别,其中有几十张大表,单表过亿行的也有两三张。目标库是电科金仓,要求业务不停太久,切换窗口只有四小时。这个条件决定了我们不能靠“导出 SQL 再导入”这种粗暴方式硬来,必须提前把兼容性、数据校验、回退方案都设计清楚。后面我把整个过程拆成五个阶段来写,每个阶段都附上当时踩坑后的最终解法,希望能给后面做同类迁移的人省点时间。
1. 迁移前不要急着动手:先看懂金仓与 MySQL 的差异逻辑
1.1 金仓是什么,迁移的本质是什么
电科金仓(KingbaseES)是国内主流的国产关系型数据库产品,长期在党政、金融、能源、电信等领域落地,整体兼容 PostgreSQL 生态,同时对外提供 MySQL、Oracle 等常见数据库的兼容特性。很多人第一次接触它,会下意识觉得“既然兼容 MySQL,那把库搬过去就行了”。这个想法很危险。兼容不等于同构,MySQL 和 PostgreSQL 体系在数据类型、SQL 语法、存储引擎、事务特性上的差异是根深蒂固的,金仓做的兼容只是让大部分常规 SQL 能跑,但边界以外的写法一样会报错。
我更愿意把这次迁移的本质理解为“换一套 SQL 解释器”。数据是死的,怎么读、怎么写、怎么约束、怎么优化,完全由数据库的解析和执行逻辑决定。举个生活化的例子:搬家不是把箱子搬进新房子就结束,你还得看新家的门够不够宽、插座在哪个位置、家具摆不摆得进去。MySQL 里的表结构、存储过程、SQL 写法就是这些“家具”,金仓就是“新房”,能不能顺利住进去,取决于你提前做了多少适配。
所以迁移项目的第一个阶段,不应该是一上来就拉数据,而是先做差异分析。我当时花了整整两天,把项目的表结构、存储过程、SQL 日志、ORM 映射文件全部过了一遍,列出一份兼容性风险清单。后面所有的工作量评估、工具选型、人员分工,都是从这份清单推导出来的。没有这一步,后面一定会被各种莫名其妙的报错打乱节奏。
1.2 迁移方案选型:一次停机迁移还是增量同步
迁移方案没有银弹,完全取决于业务容忍度和数据规模。我们当时评估了两条路线。
第一条是停机迁移。在约定窗口内停应用、导数据、验证、切换,好处是流程简单,回退就是恢复原库,坏处是窗口不够用时非常被动。第二条是在线迁移加增量同步,源库继续跑业务,工具持续把增量变更同步到金仓,切换时短暂停写,做最终追平和切换。这条路线对工具要求高,但能把业务中断时间压到分钟级。
判断标准其实就三条:数据量多大、停机窗口多长、团队对工具的熟练度。我们数据量在 TB 级,窗口只有四小时,走纯停机迁移风险太高,最后选了“全量迁移先行 + 增量同步追平 + 窗口内切换”的组合方案。操作顺序是:先做一次全量数据迁移,同时开启增量同步,验证阶段持续对比两边的数据一致性;到了正式切换窗口,停写入,等增量位点追平,再做切换。这样既保证大白天的业务不受影响,窗口内要做的事情也收敛到“校验 + 切换”两件事。
这里必须多说一句:增量同步只是手段,不是目的。如果业务允许停两小时,数据量也不大,我反而建议用最简单的一次性停机迁移,少引入一个同步工具,就少一类故障源。
1.3 迁移前的对象清单与风险盘点
动手之前,我建议先做一张“迁移对象全景表”,把源库所有对象类型列清楚。我们当时的盘点表大概长这样:
| 对象类型 | 检查项 | 风险等级 | 备注 |
|---|---|---|---|
| 表 | 数量、数据量、字符集、存储引擎 | 高 | InnoDB 为主,MyISAM 需要额外关注锁行为 |
| 索引 | 索引类型、索引长度、列排序规则 | 高 | MySQL 的索引长度限制与 PG 体系不同 |
| 视图 | 是否依赖特定函数 | 中 | 涉及自定义函数时容易翻车 |
| 存储过程 | 数量、复杂度、依赖关系 | 极高 | MySQL 过程语法与 PG/金仓差异大 |
| 触发器 | 触发时机、触发表、逻辑 | 高 | 迁移后经常出现“不触发”或重复触发 |
| 定时任务 | 调度方式 | 中 | 需要重新在金仓侧建成调度任务 |
| 分区表 | 分区类型、分区键表达式 | 中 | MySQL 和 PG 语法差异明显 |
| 自增列 | AUTO_INCREMENT 用到的所有表 | 高 | 金仓使用序列或 IDENTITY 实现 |
这张表的价值在于让你在开工前就知道“雷区在哪里”。我们项目里存储过程是最重的负担,旧系统有上百个存储过程,里面有大量的游标、动态 SQL、异常处理,这类对象基本不可能靠工具自动翻译,只能手工重写。另外一个容易忽略的点是字符集,MySQL 的 utf8mb4 和金仓的 UTF8 看起来都是“UTF-8”,但排序规则、索引长度单位、客户端连接的编码设置都有差异,最好在验证阶段就拿真实中文数据做一遍读写测试。
2. 语法兼容性的边界:这些差异决定了你要改写多少 SQL
2.1 数据类型差异对照与处理
数据类型是结构迁移的地基,地基打歪了,后面数据导入、应用查询都会出问题。MySQL 的类型体系跟 PG 体系差距不小,尤其是无符号整数、自增列、JSON、ENUM 这些特性。
我整理了一份自己项目里实际用到的类型对照表,可以给读者参考:
| MySQL 类型 | 金仓建议类型 | 注意事项 |
|---|---|---|
| INT / BIGINT | INTEGER / BIGINT | 去掉显示宽度,如 INT(11) 改成 INTEGER |
| INT UNSIGNED | BIGINT 或 NUMERIC | 无符号范围超过有符号,需要评估是否扩位 |
| TINYINT(1) | SMALLINT 或 BOOLEAN | 看业务是当布尔用还是当数字用 |
| VARCHAR(n) | VARCHAR(n) | 注意字符数和字节数的索引长度差异 |
| DATETIME | TIMESTAMP | 注意默认值、时区行为不同 |
| TIMESTAMP | TIMESTAMP | 建议显式设置 TIME ZONE 相关参数 |
| TEXT / LONGTEXT | TEXT / CLOB | 大字段读取方式需在应用层验证 |
| BLOB | BYTEA | 驱动参数、流式读取方式需调整 |
| JSON | JSONB | 操作符完全不同,应用 SQL 必须改 |
| ENUM / SET | VARCHAR + CHECK | 建议改造为字典表或 CHECK 约束 |
| DECIMAL | NUMERIC | 一般直接对应,注意精度检查 |
处理策略上,结构迁移工具能把建表语句转成金仓方言,但工具只解决“能不能建出来”,不解决“语义对不对”。举例来说,MySQL 的 INT UNSIGNED 在迁移工具里可能被映射成 NUMERIC 或 BIGINT,看起来没报错,但如果应用里原来往这个字段塞负数的时候依赖 MySQL 的报错兜底,迁移后行为就变了。这种隐藏差异必须在 code review 阶段逐字段确认。
另外,MySQL 的 ENUM 类型在金仓里往往没有直接对应项,工具可能帮你转成 VARCHAR。从功能角度没问题,但从约束角度就弱化了:原来传一个不在枚举里的值,MySQL 会报错或警告,改成 VARCHAR 后就静默接受了。稳妥的做法是额外加 CHECK 约束,或者干脆做成字典表外键。
2.2 DDL 与 SQL 语法差异实例
这个部分是整个迁移里工作量最集中的地方。不是所有 SQL 都要改,但典型场景几乎都会命中。
先说自增列。MySQL 写起来极其自然:
CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这在金仓里要改成序列或者 IDENTITY 列。用 IDENTITY 最简单直接:
CREATE TABLE users ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(64) NOT NULL );注意 GENERATED BY DEFAULT 和 GENERATED ALWAYS 的区别。如果原来的应用会在 INSERT 语句里显式指定自增列的值,一定要用 BY DEFAULT,否则插入会报错。这个细节我们第一次迁移就踩了,某个数据修复脚本一直都带着 ID 值插入,结果在金仓上直接失败。
分页语法也是重灾区。MySQL 的LIMIT m, n表示跳过 m 行取 n 行,金仓以及整个 PG 系语法是LIMIT n OFFSET m。MyBatis 里类似写法非常多:
<!-- MySQL 写法 --> SELECT * FROM orders ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} <!-- 金仓写法 --> SELECT * FROM orders ORDER BY create_time DESC LIMIT #{pageSize} OFFSET #{offset}看起来只是换个位置,但如果项目里大量使用 PageHelper 这类分页插件,插件底层生成的方言 SQL 也得跟着换,否则分页结果可能对不上,甚至直接语法报错。
INSERT ... ON DUPLICATE KEY UPDATE是另一个高发点。MySQL 用户特别喜欢这种“存在即更新,不存在即插入”的写法,但金仓不认这个语法。改造时有几个方向:如果只是“存在就跳过”,可以用INSERT ... ON CONFLICT DO NOTHING;如果要“存在就更新某个字段”,PG 系的ON CONFLICT (uk_col) DO UPDATE SET ...可以替代;如果冲突判断逻辑特别复杂,我建议拆成先 SELECT 再 UPDATE/INSERT,虽然多一条语句,但业务逻辑最清晰。金仓对 ON CONFLICT 的支持情况要以实际版本为准,所以迁移前一定要用一条简单的冲突插入语句去目标库做冒烟测试。
字符串和时间函数的差异也很容易埋雷。我们项目里遇到最多的几个是:GROUP_CONCAT在 PG 系里对应string_agg;IFNULL对应COALESCE;DATE_FORMAT通常要改成to_char;DATE_ADD(now(), INTERVAL 1 DAY)要改成now() + INTERVAL '1 day'。这些函数名不熟的话,报错都看不懂,比如function date_format(timestamp without time zone, unknown) does not exist,看到这种提示第一反应应该是去函数对照表里查,而不是去猜。
2.3 索引、约束与分区表差异
索引和约束在三层迁移里优先级很高,因为它们在数据导入阶段直接影响导入速度和数据完整性。
MySQL 的索引定义里经常带 USING BTREE,这个在金仓里不是问题,能兼容就兼容,不能兼容的地方工具会去掉。真正麻烦的是索引长度限制。MySQL 在 utf8mb4 下,InnoDB 的索引前缀长度限制是 3072 字节(早期版本是 767 字节),所以有些大 varchar 列的索引会写成INDEX idx_name (name(50))这种前缀索引。PG 系体系对索引长度的处理和 MySQL 不一样,工具转换时不一定能自动处理这种前缀索引,我们当时的做法是:能改字段长度的改字段长度,不能改的评估是否用表达式索引,或者干脆去掉这列索引,靠应用层查询优化兜底。
外键命名也要注意。MySQL 允许外键约束名在库内唯一即可,金仓更接近 PG 的规则,约束名在某些场景下的可见范围不一样,导致工具转换时可能产生重名。这种问题通常在结构迁移时报错才暴露,处理办法就是批量重命名,规则统一成表名_列名_fk这种格式。
分区表方面,MySQL 的 RANGE 分区可以直接在 CREATE TABLE 语句里定义分区子句,而 PG 体系(金仓整体也沿袭这个思路)更推荐声明式分区:
-- MySQL 风格 CREATE TABLE logs ( id INT, created_at DATE ) PARTITION BY RANGE (YEAR(created_at)) ( PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025) ); -- 金仓更推荐的方式 CREATE TABLE logs ( id INT, created_at DATE ) PARTITION BY RANGE (created_at); CREATE TABLE logs_p2023 PARTITION OF logs FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');还有一个很容易忽略的点:MySQL 的 FULLTEXT 全文索引和 SPATIAL 空间索引,在金仓里基本没有直接对应实现。我们项目里有一张表用了 FULLTEXT,最后改造方向是:简单场景用金仓的全文检索能力重写,复杂场景建议把检索需求外置。空间索引这种更不建议硬迁,要么改造成应用层用经纬度范围计算,要么单独引空间计算组件。
3. 迁移实操:从结构、数据到应用三层突围
3.1 迁移工具链与整体流程
工具的选择直接决定项目是“有条不紊”还是“到处救火”。金仓官方提供了数据库迁移评估和同步的工具,市面上的通用 ETL 工具也能用,但我的建议是:评估和结构迁移优先用官方工具,因为它们对自家数据库的方言最熟;数据迁移可以结合官方工具和自研脚本,大表部分用能控制并发和批次的方案。
我们最终的流程分成四步:第一步,用评估工具扫描源库,生成兼容性评估报告;第二步,用迁移工具批量转换表结构,然后人工 review 差异;第三步,全量数据迁移加增量同步,期间做多轮数据校验;第四步,应用层改造和切换演练。每个步骤之间都有明确的完成标准和输出物,不达标不进下一阶段。这一步的价值在后期体现得非常明显:切换当天我们只花了不到两小时完成全部工作,剩下时间都在做性能抽查和备份验证,基本没有慌乱。
3.2 结构迁移:先建表,再建索引,最后建约束
结构迁移看着简单,实际上有一个顺序问题。我们第一次尝试时,用工具把 MySQL 的建表语句一次性转换到金仓,包括所有索引、外键、检查约束,结果大表结构创建特别慢,还不断报外键依赖错误。
后来调整成三步走:先只建表和字段,不建任何索引和约束;再批量建主键、唯一键和普通索引;最后建外键和 CHECK 约束。理由很简单:PG 体系在建表时带一堆索引约束,等于每个索引都要写一遍数据,大表会非常慢;而外键如果依赖的父表还没建好,或者数据还没导完,校验会失败。分批建的好处是可以清晰看到每类对象的报错,不至于所有问题挤在一起难以定位。
结构迁移完成后,我强烈建议做一次“影子库对比”:把同一套建表语句分别放到一个临时 MySQL 和金仓上,查询 information_schema 或系统目录,逐表对比字段个数、字段类型、默认值、可空性。我们靠这个对比找出了一百多处工具转换错误,绝大多数是 default 值表达式和类型宽度问题。这个动作虽然费时间,但比上线后让业务方发现问题划算得多。
3.3 数据迁移:大表要分片,小表要并行
数据迁移是大头,TB 级量级下不可能一条一条搬。我们实际用了组合方案。
第一种是官方迁移工具直连迁移,适合中小表,配置好源库和目标库连接,选择映射关系,工具自动完成。第二种是导出导入方案,对于官方工具处理不好的大表,先用 mysqldump 导出为文本,再清理掉 MySQL 特有的语法,用金仓的导入工具加载:
# 导出时跳过建表语句,只导数据,使用完整插入并统一字符集 mysqldump -h源库 -u用户 -p密码 --no-create-info --complete-insert \ --skip-comments --skip-add-locks --default-character-set=utf8mb4 \ 数据库名 表名 > 表名.sql # 文本里通常会残留反引号、ENGINE 等 MySQL 特有内容,先做文本清理 sed -i 's/`//g' 表名.sql但说实话,纯 sed 清理很容易翻车,因为数据本身可能包含特殊字符。我们当时对大表专门写了一个类似 DataX 的同步任务:按主键范围分片,每一片一个线程,分批提交,限速控制。这样既保证了并发,又避免大事务把目标库撑爆。导入时还有个技巧:关闭目标表索引和约束,导完成后再重建,速度能快好几倍。
数据校验不能只靠 count,我们做了三层校验:第一层,全表 count 对比,确认行数一致;第二层,关键表的聚合值对比,比如 SUM 指定列、MAX 时间;第三层,抽样表的逐行 checksum 对比。抽样表优先选业务核心表和数据字典表,因为这两类表最容易暴露字符集、精度、时间偏移问题。
3.4 JDBC 驱动、连接池与 ORM 适配
应用层改造是“搬过去之后应用能跑起来”的关键。拿 Java 项目来说,第一步就是换驱动。
# 原 MySQL 配置 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://192.168.1.10:3306/appdb?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai # 迁移到金仓后 spring.datasource.driver-class-name=com.kingbase8.Driver spring.datasource.url=jdbc:kingbase8://192.168.1.20:54321/appdb这里有个坑:原来 MySQL URL 后面带的一大堆参数,比如useSSL、serverTimezone、rewriteBatchedStatements,金仓驱动不认识,直接放在 URL 里会报Unsupported connection property。所以迁移时要把这些参数全部去掉,字符集和时区统一在数据库侧或服务端参数里配置。
连接池也要跟着调。Druid 的validationQuery原来是SELECT 1,这个金仓支持,但如果你配了别的验证语句就要注意方言兼容。更隐蔽的是 Druid 的 SQL 防火墙插件 wall,它内置的 MySQL 语法白名单可能不认识 PG 风格的 SQL,导致正常查询被拦截。我们当时直接先关掉了 wall filter,等业务验证通过后再按需开放。HikariCP 相对省心一些,需要重点检查的是connection-init-sql、schema这些配置项。
MyBatis 的 XML 映射文件里有大量 SQL 需要过一遍。除了前面说的分页语法,还有几个高频点:useGeneratedKeys拿到自增主键的配置要检查是否兼容;动态 SQL 里用了 MySQL 专有函数的需要替换;如果映射文件里直接写NOW(),金仓支持,但如果写SYSDATE()就要改成CURRENT_TIMESTAMP或now()。我们当时的做法是写一个脚本扫描 XML 里的函数名和关键字,先机器挑出可疑点,再交给开发人工确认,比纯人工逐文件翻效率高一个量级。
4. 常见问题与排查技巧实录
4.1 连接层的经典报错
应用切换后第一个报错往往不是 SQL 问题,而是连接问题。最常见的是No suitable driver found for jdbc:kingbase8://...。这个错误九成是驱动 JAR 没打进应用,或者 URL 前缀跟驱动注册名不匹配。排查顺序是:先确认com.kingbase8.Driver这个类在依赖里存在,再确认 URL 前缀是jdbc:kingbase8://,然后用一个最简单的 JDBC 测试类直连数据库,把问题收敛在应用还是环境。
还有一个很微妙的报错:连接池启动成功了,但执行第一条 SQL 就报function xxx does not exist或者字符集相关的异常。这通常是驱动版本和服务器版本不一致导致的。金仓的不同小版本之间协议或元数据查询方式可能有变化,所以应用侧驱动版本一定要和目标库版本配套,不要想当然用最新的。这类问题特别像“玄学”,其实查一下版本兼容矩阵就能定位。
4.2 SQL 执行与对象迁移报错
我们把迁移过程中高频出现的 SQL 层报错整理成了一个速查表,这里分享最有代表性的几个。
第一个是反引号报错。MySQL 写惯了的人建表、写 SQL 都喜欢带反引号,金仓不认识,直接报syntax error at or near ""。解决办法就是全局清理反引号。第二个是ON DUPLICATE KEY UPDATE语法不支持,这个前面说过,用ON CONFLICT改写。第三个是字符串函数不存在,比如FIND_IN_SET,这个函数 PG 系没有直接对应,需要改成position(',' || ? || ',' in ',' || col || ',') > 0` 这类写法,或者由应用层处理。
存储过程的报错是最让人头疼的。MySQL 存储过程喜欢用DECLARE ... CURSOR FOR、IF ... THEN ... END IF,金仓的过程语言更接近 PL/pgSQL,变量声明的位置、异常处理方式都不同。一个正常情况下几百行的存储过程,重写之后可能要拆成多个函数,还要重新设计事务边界。我的建议是:遇到复杂存储过程,先不要急着翻译,先看业务逻辑能不能搬到应用层,用 Java 或 Python 实现往往比在数据库里写过程更容易维护。如果必须留在数据库里,就用 PL/pgSQL 重写,逐段验证,别指望一次写完直接跑通。
4.3 迁移后性能问题定位与解决
数据过去了,应用起来了,不代表事情结束了。性能问题往往是压垮迁移的最后一根稻草。我们当时遇到的最大问题是:同一套 SQL,在 MySQL 上走索引毫秒级返回,到了金仓上变成全表扫描,大表查询直接超时。
第一个原因是统计信息缺失或过期。PG 体系默认会自动收集统计信息,但大批量导入之后,自动收集的触发频率可能跟不上,导致优化器拿到的是“空表”的统计值,选错了执行计划。解决办法简单粗暴:对迁移的核心表手动执行ANALYZE,甚至可以连续跑两次,让统计信息稳定下来。
第二个原因是隐式转换。最常见的情况是字段类型是 VARCHAR,SQL 条件里传了数字,或者两边字符集排序规则不一致,导致索引失效。排查时用执行计划看有没有Filter而不是Index Scan,基本就能确认。还有 LIMIT 深分页问题,MySQL 和 PG 一样都有这个问题,但金仓上表现更明显,LIMIT 100000, 10这种深翻页会把前十万行都翻一遍,优化方向是改成基于游标的 seek 分页,用WHERE id > ? ORDER BY id LIMIT 10替代。
慢 SQL 定位要靠工具。金仓沿袭 PG 生态的思路,我们可以打开慢查询日志或者查询统计视图,按总耗时排序找出 TOP SQL。我们当时专门写了一个定时任务,抓取慢 SQL 并自动发送到群里,连续跑了两天就摸清了主要瓶颈:某张核心大表的 JOIN 条件字段类型不一致,以及某个报表 SQL 用了函数包裹索引列,导致函数索引没走到。
4.4 数据一致性问题的排查
数据迁移后最常见的一致性问题有三个:中文乱码、时间偏移、精度丢失。
中文乱码的根源基本都在字符集。我们的做法是在迁移前先做一次端到端验证:挑一张含中文的大表,从 MySQL 导出、搬到金仓、再查出,用十六进制对比中文内容,确保数据从源头到目标库全程一致。注意客户端连接参数也要统一,如果你用命令行工具连金仓,客户端编码和服务端不一致,显示乱码不代表数据本身错,这时候要检查客户端编码设置。时间偏移问题集中在DATETIME和TIMESTAMP的换算上,有时候差值正好是 8 小时,去检查时区和 JDBC URL 参数即可。精度问题多半是 FLOAT/DOUBLE 迁移成 DECIMAL 后位数不对,或者反过来,解决方案是迁移前把需要精确计算的字段统一设计成 NUMERIC,并明确小数位数。
5. 切换与验收:怎么才算真正迁移成功
5.1 切换前验证清单
我们切换前整理了一份验证清单,每一项都有明确的方法和通过标准。这里挑几条供参考:
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 功能测试 | 全部核心流程走一遍 | 无阻断性缺陷 |
| 数据一致性 | 全表 count + 核心表 checksum | 与源库完全一致 |
| 性能基准 | 对 TOP 50 SQL 做回放 | 耗时在业务容忍范围内 |
| 并发压测 | 模拟高峰流量打金仓 | 连接数、CPU、IO 正常 |
| 备份恢复 | 做一次逻辑备份和恢复演练 | 恢复后数据完整 |
| 回退方案 | 记录回退步骤,验证回退脚本 | 能在窗口内回退 |
清单有一条原则:宁可标准严一点,也别把风险带到切换后。尤其是备份恢复演练,这个最容易被忽视,一旦切过去之后发现数据有问题要回退,如果备份方案本身没验证过,那才是大事故。
5.2 回退方案与切换窗口
切换窗口开始前,我们保留了 MySQL 环境整整一周,没有立刻销毁。切换动作本身分三步:停应用写入、等增量同步追平、把应用连接切到金仓。回退方案就是反着来:保存切换时刻的增量位点,一旦金仓侧验证不过,立刻切回 MySQL,再用同步工具反向追平数据。理论上回退一定是有损的,因为两边可能各有新增数据,实际执行时要提前跟业务方对齐“可接受丢失范围”。
这里有个实操技巧:正式切换前一定要做至少一次“全流程演练”。演练时故意制造几个故障,比如让某个服务启动失败、让某张表数据校验不过,看看团队的反应和预案是否可靠。演练暴露出来的问题,比正式环境上的事故要好处理一百倍。
5.3 迁移后的运维要点
切到金仓之后,运维方式要跟着数据库形态变。金仓整体接近 PG 体系,所以很多习惯要改:备份逻辑用逻辑备份工具,思路类似pg_dump/pg_restore,大批量数据变更后要手动做ANALYZE,有大量更新删除的表要关注版本膨胀问题,必要时要执行清理和维护操作。MySQL 的SHOW PROCESSLIST、SHOW ENGINE INNODB STATUS那套排查习惯,到金仓上要换成查系统视图、看统计信息、抓慢 SQL 的新思路。
监控指标也要重新梳理。重点看连接数、活跃会话、缓存命中率、磁盘 IO 延迟、慢 SQL 数量。我们切换后前两周每天早会都过一遍这些指标,发现慢 SQL 阈值就自动告警,及时处理了两类问题:一类是统计信息过期导致的执行计划漂移,一类是应用里残留的 MySQL 方言 SQL。这些问题如果放着不管,积累到后面就是稳定性大坑。
5.4 我踩过的几个深坑
最后分享几个我个人的深坑体会,可能比前文所有技术细节都值钱。
第一,不要太相信迁移工具。工具能帮你完成七成工作,剩下三成必须靠人工 review,特别是存储过程、触发器、默认值表达式、字符集相关配置。我们前期吃了这个亏,导致结构迁移返工了两天。第二,低估了 MySQL 方言的渗透范围。原以为 SQL 都写在 MyBatis 的 XML 里,结果发现还藏在定时任务脚本、数据修复脚本、运营后台的动态查询里,这类场景最容易被遗漏。第三,没有做充分的“影子验证”。我们应该在迁移全量数据的第二天,就用真实流量在金仓上跑一遍只读请求,而不是等到切换当天再验证,那样发现问题已经晚了。
项目收尾那天,我在复盘会上说了一句话:成功的迁移,不是切换那一刻不出错,而是出错的时候你能在十分钟内定位问题、半小时内回退或者修复。这套从评估、迁移、验证到切换的方法论,就是在给你争取这十分钟和半小时。希望这篇笔记能帮你少踩几个坑,真到要上手的时候,心里有底。