做数据库这一行,最怕听到的三个字是什么?不是“删库了”,是“要迁移”。如果前面再加个定语——“不停机迁移”,那基本意味着接下来几周你都要跟闹钟过不去了。业务方说“不能停”,领导说“数据不能丢”,开发说“接口不能改”,最后压力全堆在负责迁移的人身上。这篇文章不聊那种PPT里的方法论,老老实实把我这些年做不停机数据迁移踩过的坑、验证过有效的流程,掰开揉碎讲一遍。
先说明白一件事:所谓“不停机”,不是真的全程没有任何感知,而是把可感知的影响降到业务可接受范围内——比如某个瞬间的延迟升高、个别慢查询,但绝不能出现连接中断、数据丢失、主键冲突这类硬故障。这个定位很重要,你如果一开始就承诺“绝对无感”,后面的方案设计会处处被动。
这篇文章适合谁看?正在准备迁移方案的运维和DBA,刚接手数据同步任务的后端开发,以及被临时拉去支援迁移项目的测试同学。不管你是MySQL还是Oracle,底层思路通用,工具选型上我会分别给出建议。
1. 不停机迁移的核心思路与方案选型
1.1 为什么“先停服再迁移”不再适用
早年间做数据迁移,最稳妥的方案就是半夜两三点挂维护页,停写、导出、导入、校验、切流量,天亮之前搞定。这套流程放到今天,问题越来越明显:业务是7x24小时的,凌晨三点照样有海外订单、有定时任务、有对账程序在跑;即使能停,随着数据量增长,恢复服务的时间越来越不可控。我见过一次案例,预估4小时能迁完的库,实际跑了12个小时,业务方从上到下全在群里质问,场面非常难看。
不停机迁移真正的挑战在于:数据是活的。导出的时候数据在变,导入的时候数据还在变,切流量的那一秒数据依然在变。所以方案的核心不是“怎么把数据搬过去”,而是“怎么让源库和目标库在任意时刻都尽可能保持一致”,并在切换瞬间处理掉最后那几秒的增量。
1.2 三种主流方案对比
我实操下来,真正能落地的不停机方案有三类,各有明确的适用边界:
| 方案 | 核心原理 | 适用场景 | 主要成本 |
|---|---|---|---|
| 在线DDL/逻辑复制 | 用工具(如gh-ost、pt-osc、OGG、DataX)解析binlog或归档日志,增量同步到目标库 | MySQL结构变更、同构数据库迁移 | 需要binlog开启、工具部署 |
| 双写迁移 | 应用层同时写新旧两套库,历史数据先搬迁,新数据双写,校验后切读 | 从MySQL迁到异构数据库(如Oracle、TiDB)、业务可以改造 | 应用代码改动大、需要灰度开关 |
| 快照+增量追平 | 基于物理快照或逻辑备份建立基线,持续追平增量,最后以秒级窗口完成切换 | 数据量极大、同构迁移、硬件下架搬迁 | 依赖存储能力、切换窗口仍需谨慎 |
我个人的经验法则是:能用逻辑复制的不要上双写,能上双写的不要做物理迁移。逻辑复制对应用透明,风险集中在工具本身;双写虽然灵活,但代码改动引入的bug往往比数据迁移本身还多。下面重点讲逻辑复制和双写的组合打法,这是目前实践中最稳的一条路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前的摸底:这些事不做完,不要碰数据
2.1 数据量评估与同步链路摸底
很多人拿到迁移任务,第一反应是去查库有多大,然后就开始搭同步工具。这远远不够。我建议至少收集以下几类数据:
- 库表清单:哪些表是大表、哪些是热表、哪些有自增主键、哪些外键关联复杂;
- 写入峰值:一天的写入量级、高峰时段的TPS、binlog产生速率,这决定增量同步的延迟上限;
- 保留策略:有没有归档表、临时表、日志表,这些表是否要一起迁;
- 字符集与排序规则:源库和目标库的字符集不一致会导致乱码,甚至隐式转换导致索引失效。
拿MySQL来举例,在迁移前用一条SQL就能把表体量摸清楚:
sql复制SELECT
table_schema,
table_name,
table_rows,
ROUND(data_length / 1024 / 1024, 2) AS data_mb,
ROUND(index_length / 1024 / 1024, 2) AS index_mb
FROM information_schema.tables
WHERE table_schema = 'your_db'
ORDER BY data_length DESC;
注意,table_rows是估算值,InnoDB引擎下不准,但用来排优先级足够了。真正精确的数据量要在导出阶段校验。
还有一件事很多人忽略:确认binlog_format是不是ROW。如果线上库是STATEMENT或MIXED,做增量同步时遇到非确定性函数(比如NOW()、UUID())会出现源库和目标库数据不一致的情况。迁移前必须确认:
sql复制SHOW VARIABLES LIKE 'binlog_format';
SHOW VARIABLES LIKE 'binlog_row_image';
binlog_format需要是ROW,binlog_row_image建议是FULL,否则UPDATE语句只记录变更列的旧值,某些同步工具会校验失败。
2.2 反向依赖与回滚预案
数据迁移不是“把数据搬过去就完了”,它牵扯到上下游所有系统。我见过最典型的事故:迁移完成当天一切正常,第二天凌晨定时任务跑批,发现报表系统还在连旧库,新库的数据没被读取,导致当天所有报表数据缺失。这类问题不是数据迁移本身造成的,是上下游依赖没有梳理干净。
所以在动手前,至少要拉出这么一份清单:
- 哪些应用连接了源库(IP、端口维度);
- 哪些定时任务、消息队列消费者在读写源库;
- 哪些下游通过binlog或CDC订阅了源库的数据变更;
- 源库是否有跨库join、存储过程、触发器。
回滚预案同样要在动手前设计好。我的习惯是:每一次操作都要有一个反向操作的明确步骤。比如我建立了同步链路,回滚就是停掉同步、确认旧库无损;我改了应用连接串,回滚就是改回旧配置并发布。回滚不是靠临场发挥,是提前写成文档、甚至演练过的。
2.3 迁移目标库的参数基线校正
说到Oracle迁移数据,很多人以为把数据导过去就完了,实际上目标库的参数和源库不一致,会在切换后引发一连锁性能问题。比如源库是MySQL,迁到Oracle后,原本在MySQL里走索引的查询,到了Oracle因为优化器版本不同、统计信息缺失,执行计划完全不一样。我的做法是在迁移前先做一轮SQL兼容性review,把应用里用到但目标库不支持的语法、函数、数据类型提前标出来,该改SQL的改SQL,该做转换的做转换。
如果你是从Oracle迁到MySQL,重点看这几类问题:Oracle的NVL换成IFNULL或COALESCE、ROWNUM换成LIMIT、SYSDATE用NOW()替代、空字符串和NULL的语义差异(Oracle里空字符串就是NULL,MySQL不是)、大字段类型LONG换成TEXT/JSON、分页查询的写法。这些看起来是细枝末节,真到了切流量那天,每一条都可能变成线上故障。
3. 核心实操:从基线同步到最终切换
3.1 搭建增量同步链路
无论你最终选择哪条迁移路径,增量同步链路是核心中的核心。MySQL场景下我推荐用官方生态的工具,配置简单、社区成熟。以MySQL到MySQL为例,典型架构是:
- 源库开启binlog,格式为ROW;
- 部署一个同步服务(如Canal),伪装成从库拉取binlog;
- 同步服务把解析后的变更写入目标库;
- 通过监控面板观察同步延迟。
这里有一个容易被忽视的点:同步账号的权限。Canal拉取binlog需要一个专用账号,权限至少要包含SELECT、REPLICATION SLAVE、REPLICATION CLIENT。用root账号跑同步虽然省事,但违反安全基线,一旦同步配置泄露,等于把整个库的管理权限交给了第三方进程。我见过不少团队图省事直接root,后来审计被查,整改又花了双倍时间。
配置好同步后,第一件事不是迁移历史数据,而是确认同步链路本身稳定。在源库执行几条INSERT、UPDATE、DELETE,去目标库比对结果。等确认基础功能正常了,再开始全量迁移——因为全量迁移会持续很久,这段时间增量数据必须靠这个链路攒下来。
3.2 历史数据全量搬迁与追平
全量搬迁的常见做法是使用mysqldump或mydumper导出,再导入目标库。这里要特别注意,mysqldump默认会在导出过程中申请全局读锁,这在不停机场景下是不可接受的。解决方案有两种:
- 使用mydumper,通过MVCC机制在事务内读取一致性快照,不加锁;
- 使用mysqldump加--single-transaction参数,同样基于InnoDB的MVCC,导出期间不阻塞写入。
mydumper导出的速度通常比mysqldump快不少,尤其是并行导出多张表的时候。但mydumper的并行导入有个坑:如果表之间存在外键约束,并行导入会频繁报外键错误。稳妥的做法是先禁用外键检查(SET FOREIGN_KEY_CHECKS=0),导入完成后再启用并校验。
全量导入完成后,开始增量追平阶段。这个阶段的关键指标是同步延迟,我用一个监控SQL看主从之间的秒级差距:
sql复制SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master 字段
如果用的是Canal,可以在管理后台直接看到延迟时间。当延迟在几十秒以内并且稳定下降时,就可以准备切换了。但请记住:追平到0是不现实的,因为业务还在持续写入。切换的目标是把“未同步的增量”控制在一个极小的窗口内,并通过后续操作补齐。
3.3 双写策略的落地细节
如果业务需要迁移到异构数据库,或者你有强校验需求,双写几乎是必选项。双写不是简单地在代码里写两个库,它的核心是保证两边的数据一致性,同时不影响主链路性能。
我的做法是引入一个异步双写组件:主业务照常写旧库,事务提交后,通过消息队列把变更事件异步发给新库。这样做的优势是:新库写入失败不会阻塞主业务,可以通过重试机制慢慢补齐。代价是架构复杂度上升,需要额外维护MQ和消费端。
双写改造中最容易翻车的是顺序问题。比如同一个订单先更新了状态,又修改了金额,两个消息如果被并发消费,目标库可能出现金额先更新、状态后更新的错乱情况。解决方式是给每条消息带一个版本号或时间戳,消费端做幂等和乱序处理。
对于采用逻辑复制方案的迁移,双写不是必需的,因为同步链路已经承担了增量写入的职责。一般只有当同步工具无法覆盖某些数据类型或DDL操作时,才需要双写兜底。这个判断要在方案设计阶段就做好,不要迁到一半发现同步顶不住,再回来改代码。
3.4 流量切换的五步操作法
切换是整个迁移过程中最紧张的时刻,同时也是最不需要“临场发挥”的时刻。我有一套固定的五步操作流程,每次迁移都按这个走:
**第一步:进入只读模式。**在源库执行SET GLOBAL read_only=ON,并在应用层断开写连接。这个操作目的是停止源库新数据的产生,让同步链路把最后一批增量追平。
**第二步:确认增量彻底追平。**观察同步延迟降为0,源库和目标库的数据校验通过。
**第三步:切换读流量。**把应用的读连接串切到目标库,观察日志,确认查询正常、错误率没有上升。
**第四步:切换写流量。**将应用的写连接切到目标库,同时关闭源库的read_only。这一步操作要快、要果断,不要在新旧库之间犹豫反复。
**第五步:持续观察。**重点是连接数、慢查询、错误日志和主键冲突。观察时间至少覆盖一个业务高峰周期,一般我习惯观察24小时。
这套流程看起来很普通,但每一步都对应着实际踩过的坑。第一次做不停机迁移的时候,我在第三步就把流量切了,结果发现源库还有一个定时任务在凌晨执行批量更新,导致源库和新库又产生了新的数据差异,不得不再做一次增量补齐并二次切换。从那以后,切换前必须确认所有定时任务和批处理脚本都已经停掉或改指目标库,这个排查项写进了我的切换checklist里。
4. 常见问题与排查技巧实录
4.1 数据不一致的典型场景
数据不一致是不停机迁移的头号问题,也是最难排查的问题。我把这几年遇到的不一致场景做了个分类:
增量丢失型:同步进程异常重启导致binlog位点回退,一部分事务没有被消费。排查方式是查看同步工具记录的位点信息和目标库的实际数据,两者差距明显增大时,基本可以断定是这个原因。处理方式是重新定位binlog位点,从最近一个可靠位点重新同步。
乱序执行型:两个并发事务在源库的提交顺序和同步到目标库的顺序不一致。这在MySQL的ROW格式binlog下较少发生,但在跨地域、跨机房的同步链路中,网络延迟会让乱序概率上升。排查方式是比对同一主键的update_time字段,发现可疑数据时去源库确认最终值。
类型转换型:源库和目标库字段类型不一致导致精度丢失。例如源库DECIMAL(10,2)迁移到目标库变成DECIMAL(10,0),金额小数位被截断。这类问题在异构迁移中防不胜防,唯一的办法是在迁移演练阶段做全量数据比对,用脚本逐字段比对而不是只比记录数。
4.2 主键冲突与自增偏移
用逻辑复制做不停机迁移时,主键冲突几乎是必现问题。原因很简单:源库的自增ID已经增长到一个值,目标库的自增起始值如果没有提前设置,导入历史数据后继续写入时,新ID可能跟已有的历史数据ID撞车。
处理方法是在全量导入完成后、增量同步启动之前,把目标库的自增起始值调整到位:
sql复制-- MySQL中查询源库最大自增ID
SELECT MAX(id) FROM your_table;
-- 在目标库设置自增起始值(假设最大ID是100000)
ALTER TABLE your_table AUTO_INCREMENT = 100001;
这个操作必须在启动增量同步之前完成,否则一旦目标库已经开始写入,再调整自增起始值就可能出现间隙或撞车。
如果你是从MySQL迁到Oracle,情况又不一样。Oracle没有自增字段,用的是SEQUENCE。迁移时需要把每个表对应的SEQUENCE的NEXTVAL设置到源库当前最大ID之上,否则插入新数据就会违反主键约束。常见的错误是只迁移了表和索引,忘了迁移SEQUENCE,导致应用插入时报ORA-00001唯一约束冲突。
另外,有网友反馈“小米迁移数据后图库拍摄日期不对”,这个虽然和数据库迁移不是一回事,但背后的道理是相通的——迁移过程中时间类元数据的解析和还原是最容易出问题的环节之一。数据库迁移同样如此,尤其是时间字段的时区不一致,会在迁移后产生微妙的数据偏差。MySQL和Oracle对TIMESTAMP的存储方式和时区处理逻辑完全不同,迁移前必须统一时区口径,建议全部以UTC时间存储,展示层做本地化转换。
4.3 同步延迟飙高与性能抖动
同步延迟在迁移过程中一般比较平稳,但某些情况下会突然飙高。最常见的原因是目标库在执行大事务或批量DDL,导致同步线程被阻塞。有一次我遇到同步延迟从2秒突然跳到30分钟,查了才发现是目标库正在跑一个批量UPDATE的优化任务,拿到了大量行锁,同步线程在等待锁释放。
应对方案是在同步低峰期把大事务拆小,或者给同步账号在目标库加一个锁等待超时设置:
sql复制SET GLOBAL innodb_lock_wait_timeout = 10;
但要注意,锁等待超时设置太短会让同步任务频繁失败,太短反而不利于恢复。个人建议保持默认50秒即可,实在需要调整,可以降到20秒左右,同时同步工具要配置自动重连和断点续传。
还有一类性能抖动来源于全量导入和增量同步同时进行时的IO竞争。全量导入阶段磁盘IO基本被打满,增量同步的binlog拉取也会变慢。处理方式有两种:一是限速,比如mydumper用--rows参数控制单批导出行数;二是错峰,在业务低峰期做全量导入,白天只跑增量同步。实际操作中我通常两种都用,白天限速跑增量,晚上放开速度跑全量。
4.4 问题排查速查表
最后整理一份速查表,覆盖我遇到过的大部分迁移问题,可以直接保存下来当参考:
| 症状 | 可能原因 | 优先排查动作 |
|---|---|---|
| 同步延迟持续增长 | 目标库有大事务、锁竞争、磁盘IO瓶颈 | 看目标库的慢查询、锁等待、IO使用率 |
| 目标库主键冲突 | 自增起始值未调整或SQL不含主键 | 查同步日志的错误信息,调AUTO_INCREMENT |
| 数据行数一致但内容不一致 | 类型转换、字符集转换、时区问题 | 抽样比对全字段,重点看金额、日期、长文本 |
| 切换后慢查询增多 | 目标库统计信息缺失、索引缺失、执行计划变化 | 跑ANALYZE TABLE,对比慢SQL执行计划 |
| 同步中断且无法续传 | binlog被清理、位点失效、网络闪断 | 确认binlog保留时间,必要时重建同步链路 |
| 应用写入超时 | 双写链路阻塞、目标库性能瓶颈 | 先切回单写,恢复后再处理同步问题 |
| 迁移后日期数据错乱 | 时区配置不一致、字符串转日期格式错误 | 统一时区设置,校验日期字段边界值 |
4.5 演练比一切预案都重要
最后想强调一件很多人会跳过的事:迁移演练。无论方案写得多完美,工具配置得多熟练,没有完整演练过一遍就直接上生产,都是在赌运气。
我每次做迁移,至少演练两轮。第一轮在测试环境跑通全流程,验证脚本和步骤;第二轮在预发环境按生产数据量做一次压测,顺便把切换耗时和业务影响摸清楚。演练过程中出现的每一个问题都要记录,更新到正式迁移方案里。
有一次演练发现,全量导出的耗时比预估多了3倍,原因是源库有一张十几亿行的日志表,信息schema统计出来的数据量和实际差距非常大。这个发现让团队重新调整了迁移方案,把日志表改为不迁移,通过归档的方式单独处理,才保证了正式迁移的时间窗口可控。
5. 迁移完成后的持续观察与收尾
切换完成不代表迁移结束,恰恰相反,切换后24小时才是真正考验方案质量的阶段。我的习惯是列一个48小时观察清单:
- 前2小时:每15分钟看一次应用错误日志、慢查询、同步状态;
- 前24小时:每4小时做一次数据抽检,重点比对交易金额汇总、订单状态分布;
- 24小时到48小时:确认无异常后,清理源库的只读配置、回收临时同步账号、下线迁移工具。
还有一个必须做的操作是关闭或降级源库的写入入口,防止个别服务因为配置遗漏还在写旧库。具体做法是在源库账号层面回收写权限,或者在网络层限制源库的访问来源。如果不做这个收尾,可能出现新旧库“双主”长期并存,一旦两边写入冲突,后续的数据修复成本会非常高。
针对一些大表,切换后我还会跑一次全字段级别的比对,而不是只对比行数。很多团队在迁移验收时只看COUNT(*)是否一致,这远远不够。行数相同但数据不一致的案例我在前面已经提过,最典型的就是DECIMAL精度丢失和时区偏移,这类问题必须全字段扫描才能发现。对于超大的表,可以用分批抽样加哈希比对的方式,效率比逐行比对高很多。
我个人在实际操作中最深的一个体会是:不停机迁移从来不是一个技术问题,而是一个项目管理问题。技术方案再完备,只要有一个依赖方没有通知到位、一个定时任务没有纳入排查范围、一个账号权限没有提前申请,整个迁移都可能功亏一篑。所以每次迁移前,我都会把各环节的负责人拉齐,明确每一步的操作人、确认人和回滚决策人,确保关键时刻不需要现找领导拍板。
还有一个一直沿用到今天的小技巧:切换到目标库后,保留源库的数据环境至少一周,只读不写。一旦目标库真出现早期没发现的问题,至少还有回退到源库读数据的余地。一周过后,等目标库运行稳定了,再下线源库。这个“源库保留期”的设定,已经帮我在两次迁移事故中争取到了宝贵的修复时间。
