关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

迁移不是搬完就完:双写期的订单数据该用什么方法做一致性校验

发布时间:2026-09-24

行数对得上、延迟是 0,为什么切完还是出了错

先说一个我见过太多次的翻车形态。迁移项目的看板上,同步延迟一直显示 0 秒,源库和目标库的行数比对也是一致的,团队按计划把读写全部切到新库,老库保留只读一周之后回收。切流后第三天,客服开始接到投诉:有客户在 App 里看到的订单状态是"已发货",但物流系统里根本没有这笔单的运单记录。

翻回去查,这批订单的 status 字段在老库是 3、在新库是 2,差异记录两千多条。那时候老库已经回收,只剩切换当天的一份逻辑备份,而这批订单在备份之后又发生过状态变更,等于原始值永远拿不回来了。最后只能靠业务侧的人工流水一条条补,前后折腾了将近两周,还赔进去一批客户的信任。

这里最要命的一点是:监控当时是全绿的。延迟 0 说明同步链路通畅,行数对得上说明没有整行丢失——但这两件事都证明不了"内容一致"。同步工具报的成功,只代表它把收到的事件重放了一遍;它没收到、或者收到了但被下游逻辑改写的那部分,它一概不知。行数比对也是同理,一条记录被 UPDATE 了三次和一次,行数完全一样。

所以这篇要立的判断是这个:迁移的成败不取决于"搬得完",而取决于"敢不敢切、能不能回滚"。数据拷贝只是第一步,真正的技术含量在双写期——两边同时写入、流量逐步切换的那段时间。双写的核心不是"写两次",而是建立一套可量化的校验机制:先做全量基线比对,再做增量持续比对,把不一致率做成一条曲线,只有这条曲线持续收敛到可接受范围并且稳定一段时间,才具备切流和停写老库的条件。没有校验的双写,只是把一份不确定性变成两份。

下面这些结论可以先记住:

  • 行数一致不等于内容一致:整行丢失能被行数抓到,字段被改写抓不到,这是最常见的盲区。
  • 延迟 0 不等于没丢过写:延迟反映的是当前积压量,不反映历史事件有没有被完整消费过。
  • 双写必须有闭环:基线比对、增量比对、修复、复验,四件事缺一件都等于裸奔。
  • 切换决策靠曲线不靠感觉:不一致率要连续若干天低于某个阈值,而不是"看着差不多"。
  • 回滚链路要先于切流建好:新库到老库的反向同步不通,切换就是一道单向门。

迁移的六个阶段,每个阶段都得有能判定的出口条件

很多人把迁移理解成一次性的搬运,其实它更像一段有明确关卡的流程。我习惯把它拆成六段,每段都必须有一个能写进验收单的出口条件——那种"观察一段时间再说"的表述不算条件,因为没人知道观察多久算够,一旦出了问题也追不责。

阶段 这一阶段在做什么 出口条件(可判定) 最容易出的问题 需要提前准备的回滚动作
全量拷贝期 存量数据一次性搬运,记录一个一致的快照点(binlog 位点 / WAL LSN / 一致性快照 ID) 快照点已固化并落盘记录;全量行数、主键最大最小值、分片 checksum 三项均一致 边拷贝边写入导致快照漂移;大表拷贝把线上 IO 打满 保留源端快照文件与目标端导入清单,导入失败可整表重来
增量追平期 用 CDC 订阅快照点之后的变更,把目标库追到与源库同步 同步延迟 P99 连续 30 分钟低于 1 秒,积压事件数持续为 0,且追平后全量抽检一致 大事务导致延迟反复抖动;DDL 变更让订阅中断 订阅位点可重置,目标库可清库重放;保留重放脚本与位点记录
双写期 应用同时写两边,读写仍全部走老库,新库只做影子写入与校验 双写失败率低于 0.01%;增量比对日不一致条数收敛且连续 7 天低于阈值 一边成功一边失败的部分成功;异步重试带来的乱序覆盖 双写可一键降级为单写老库;失败队列可重放、可丢弃
读流量灰度 按用户 ID 或分流比例把读请求逐步切到新库,比对两边返回差异 灰度到 50% 后影子读差异率低于 0.001%,且无 P0/P1 级业务告警持续 48 小时 同一用户请求被分到不同库造成体验跳变;慢查询在新库放大 分流开关可按比例秒级回退到 0,路由配置版本化管理
写流量切换 停止双写,单写新库;老库降级为备份或只读,反向同步链路启用 切换前新老库延迟低于 1 秒并冻结写入 30 秒;切换后订单创建成功率与 TPS 回到基线 ±5% 内 切换瞬间仍有在途请求写老库;反向链路没开导致无法回退 反向同步(新→老)已跑通并完成演练;回滚判定人与触发阈值已书面确认
观察与下线 新库独立承载全量读写,老库只读保留,到期后回收 连续 14 天核心业务指标无异常,账务对账全部平账,且至少完成一次完整对账周期(含月结) 留得太久成本翻倍;收得太早遇到月末对账才发现历史差异 老库保留期限内可随时重新切回;下线前必须导出一份可恢复的冷备

这张表里最容易被跳过的是第三列。实际项目里,绝大部分团队能做到第一列和第二列,也就是"知道要做什么"和"真的在做",但到出口条件这一步就变成拍脑袋了。我见过有人把"双写跑一周"当成出口条件,结果那一周里不一致条数一直在涨,只是没人去看——双写跑了七天,只是让不一致积累得更久。

全量拷贝:快照点必须能对齐到具体位点

全量拷贝阶段真正的技术活不是"拷",是"记"。你必须在开始拷贝的那一刻,同时记录一个可以精确对齐的位点:MySQL 是 binlog 的 file + position(或 GTID)、PostgreSQL 是 WAL 的 LSN、MongoDB 是 oplog 的时间戳。没有这个位点,后面的增量追平就没有起点,只能从头重来。

拷贝过程本身要限速。一张两亿行的订单表,如果不限速直接全表扫描导出,很容易把线上主库的 IO 和缓冲池打满,表现为业务侧突然变慢,但监控上数据库 CPU 并不高——这种"查不出原因的慢"最折磨人。常见做法是按主键分片导出,每片之间加 sleep,或者在从库上做导出,把压力挪走。

增量追平:判据是延迟趋近于 0,而不是"追上了"

增量追平的出口条件要写得能测:同步延迟的 P99 连续 30 分钟低于 1 秒,积压事件数持续为 0。注意是 P99 不是平均值,平均值会把偶尔几次几十秒的抖动抹平,而那种抖动往往对应一个大事务或者一次 DDL,恰恰是最容易出问题的时候。

还有个细节:追平之后要再跑一次全量抽检。因为"延迟为 0"只说明当前没有积压,不说明历史事件都正确重放了。如果订阅过程中有事件被跳过、或者下游触发器改写过写入值,延迟照样是 0。

双写写在哪一层:应用层、数据层、代理层的三种代价

双写不是只有一种做法,选在哪一层实现,决定了后面校验的难度和出问题时的影响面。三种位置各有各的账要算。

应用层双写:最灵活,也最容易漏

在业务代码里写两遍,是最直观的做法。好处是语义最清楚——你知道每个写入点的业务含义,可以针对不同表做差异化处理,比如订单主表双写、日志表不双写。坏处同样明显:侵入大,写入路径多的时候容易漏。一个成熟的订单系统,写订单的路径可能有下单接口、支付回调、退款流程、后台运营改单、定时任务补偿五个以上,漏掉任何一个,那部分数据就永远只在一边。

应用层双写最容易踩的两个坑,值得单独说。

第一个是部分成功。老库写成功、新库写失败(或者反过来),这时候返回什么?返回成功,那这条记录就永久不一致;返回失败,用户会看到下单失败,但老库其实已经落库了,再点一次就是重复订单。正确做法是:主库(通常是老库)写成功即算业务成功,新库写入失败要落到补偿队列里异步重试,同时把双写失败率作为一级监控指标。如果新库失败率超过某个阈值,直接降级为单写,别硬撑。

第二个是异步重试带来的乱序。这个坑更隐蔽。假设同一条订单先发生 UPDATE status=2,再发生 UPDATE status=3,两次都写失败进了重试队列。重试的时候如果并发跑,完全可能 3 先到、2 后到,最终新库停在 status=2。解决办法有两个方向:一是给同一主键的变更事件做串行化(按主键哈希到一个队列,保证顺序);二是用版本号或时间戳做乐观覆盖,新库只接受版本号更大的更新。我更推荐前者,因为后者的前提是两边时钟可比,而时钟这件事本身就不可靠。

数据层同步:对应用透明,但要接受延迟和顺序的不确定性

通过订阅 binlog/WAL 做数据层同步,应用层完全不用改,这是它最大的吸引力。代价是:它天然存在延迟,且它对业务语义一无所知。同步工具看到的是"某行某字段从 A 变成 B",它不知道这是正常改单还是程序 bug,也不会因为这笔订单金额异常就停下来报警。

有个常被忽略的点是 DDL。很多 CDC 工具对 DDL 的支持是残缺的,源库加了一列,订阅链路可能直接断掉,也可能默默忽略。所以迁移期间原则上要冻结 DDL,实在要改,必须走"先改目标库、再改源库、再重启订阅"的流程,并且改完立刻跑一次结构比对。

中间件或代理层双写:折中,但引入了新的故障域

在数据库代理层做双写(或者基于 ShardingSphere 这类的中间件能力),对应用透明的同时又能拿到一定的语义信息。代价是代理层成了新的单点,它自身的可用性、连接池容量、超时策略都要单独设计和压测。而且代理层双写同样逃不掉部分成功的问题——代理也得决定一边失败时怎么办。

选哪个?我的看法是这样:如果写入路径收敛、团队对代码掌控力强,应用层双写最可控,因为你能保证"该写的都写了";如果系统老了、写入点散落在各种脚本里,数据层同步更现实,但校验必须做足,因为你无法保证完整性,只能靠比对发现问题。别指望某一种方案能规避校验,三种方案都只是减少了某一类问题,没有一种能替你确认数据是对的。

先把"一致"定义清楚,否则校验出来的差异没法判

这是很多团队跳过、但后面一定会付出代价的一步。开始比对之前,必须先写清楚三件事:要做到强一致还是最终一致;允许的最大延迟窗口是多少;窗口内和窗口外的差异分别怎么处理。

强一致的意思是任何时刻读两边结果都一样。在跨库场景下这基本做不到,除非引入分布式事务,代价是写入延迟和可用性都大幅下降,对订单这种高写入量场景不现实。

最终一致的意思是允许短暂不一致,但保证在没有新写入的情况下,经过一个有限时间 T 之后两边收敛。这个 T 就是允许延迟窗口。

关键在于:窗口内的差异不是错误,是预期状态。老库刚写入一条订单、新库还没同步过来,此时比对必然不一致,这是正常的。真正要报警的是"超出窗口仍不一致"——比如 T 定为 5 秒,一条记录在 60 秒后还是不一致,那就说明有事件丢了、被改写了、或者根本没被双写。

所以校验系统必须区分两类差异:待确认差异(pending diff)——在窗口内,只是还没追上,放进队列等待复验;确认差异(confirmed diff)——超出窗口 T 之后仍然不一致,进入修复流程。混在一起统计的话,你会得到一条永远降不下来的曲线,然后慢慢就没人看这个指标了,这比不做校验更糟。

T 定多大?取决于业务。订单创建后一般会立刻查一次,T 定 5 到 10 秒比较合适;如果业务上有"下单后 30 秒才轮询"的设计,T 可以放宽到 60 秒。但注意,T 越大,你发现问题的速度越慢,出问题时的影响面也越大。T 不是越大越安全,而是越大越迟钝。

四种校验手段:全量比对、增量比对、影子读、业务汇总

校验不是跑一个脚本就完事,四种手段覆盖的是不同层面的问题,成本也差很多。我的建议是四种都上,但按阶段有侧重:全量比对在切流前做基线,增量比对贯穿全程,影子读在读灰度期开,业务汇总是最后的兜底。

全量比对:分片、并行、限速,还要能断点续比

全量比对的思路是按主键把表切成片,每片算一个 checksum(可以是行数 + 关键字段的聚合哈希,也可以是整片所有行的逐字段哈希),两边对同一片分别计算再比对。分片是为了并行和限速,也是为了定位——发现第 137 片不一致,你就知道去查主键范围落在哪一段。

具体执行上有几个要注意的:比对一定要在从库或者只读副本上跑,别打主库;并发度按从库负载动态调,白天低、夜里高;比对要能断点续比,跑了六个小时被中断就得从头来的脚本,在大表上根本跑不完。还有一点,全量比对期间数据还在变,所以比对结果必然有噪声——解决办法是同一片比对两次不一致时,取该片的精确行级比对做二次确认,而不是直接报差异。

大表怎么权衡?两亿行的表做逐行全字段比对,成本非常高。实用的做法是抽样 + 全量结合:先做一遍分片级的 checksum 全量比对(便宜,能定位到片),对不一致的片再下钻到行级(贵,但范围已经很小)。再加上一层随机抽样,比如每天随机抽 10 万条主键做逐字段比对,用来发现"checksum 恰好碰撞"这种极低概率但确实存在的情况。

增量比对:订阅两边变更流,按主键做字段级差异

增量比对是双写期的主力手段。做法是同时订阅老库和新库的变更流,把变更事件按主键归拢到比对队列,对同一主键比对变更后的字段值差异。因为只比变更过的行,量比全量小几个数量级,可以做到准实时。

这里有个实现细节容易被低估:两边变更流的时钟基准不同,事件到达顺序也不同。所以比对不能简单地"事件来了就比",而是要按主键维护一个待比对集合,每条主键在最后一次变更之后的 T 秒(就是前面定义的延迟窗口)没有被再次变更时,才拿出来做比对。这个"静默期"机制是过滤掉窗口内正常差异的关键。

影子读:最强也最贵的手段

影子读的做法是:真实的读请求在新老库各执行一次,老库的结果返回给用户,新库的结果只用来比对,记录差异但不返回。它的价值在于能发现前面两种手段都发现不了的问题——比如索引缺失导致排序结果不同、字符集导致中文排序不同、时区导致日期聚合不同。这些问题在字段级比对上是完全一致的,只有在"执行同一条 SQL 看结果"的时候才会暴露。

代价也很实在:新库的读流量翻倍,而且是一次真实查询,会吃掉连接池和 IO。所以影子读必须支持按百分比采样(比如只比对 1% 的请求),并且要有独立的超时和熔断——新库查询超过 50 毫秒就直接放弃这次比对,绝不能让它拖慢真实请求。还有一点,影子读对写敏感的比对结果要做去重,同一个用户在几秒内多次查询同一条订单,差异会被重复计数,按主键 + 时间窗去重再统计才有意义。

业务口径校验:字段一样不代表语义一样

最后一类校验比的是业务汇总,不是字段。每天固定时间跑一次:当日订单总数、订单金额合计、各状态订单数分布、退款金额合计、按小时的新增订单曲线。这些数字两边各自算一遍,对不上就说明有问题。

为什么要单独做这一层?因为字段一致但语义不一致的情况太常见了。金额字段两边存的都是 199.00,但一边是 Decimal(10,2)、一边是 float,聚合十万条之后就会出现几毛钱的差异;订单数都是 10000,但一边按 create_time 的 UTC 时区统计、一边按东八区统计,跨日的那部分就分到了不同的日期桶里。字段级比对对这些一律报"一致",只有汇总口径能抓出来。

那些字段比对抓不到的隐性差异

这一节单独列出来,是因为这些坑的复现成本极高——它们不会让同步报错,不会让比对报警,只会在切流之后的某个业务场景里突然冒出来。

  • 字符集与排序规则:源库 utf8mb4_general_ci、目标库 utf8mb4_0900_ai_ci,单条记录看不出区别,但排序、去重、LIKE 匹配的结果会不一样。中文场景下尤其明显,有的排序规则对某些汉字视为等价。
  • 时区:数据库 session 时区、连接串里的 serverTimezone、应用里的 JVM 默认时区,三处只要有一处不同,TIMESTAMP 类型的读写就会差几个小时。DATETIME 不受时区影响但也不带时区信息,迁移后如果改了类型,历史数据的含义就变了。
  • 大小写敏感:表名、列名、字段值的大小写敏感配置在两个环境不一致,可能导致查询命中不了,或者唯一索引的判定不同。
  • 浮点与 Decimal 精度:金额用 double 存是灾难。float/double 在聚合时会累积误差,Decimal 的精度和小数位定义不同也会在四舍五入时产生分歧。金额字段一律用 DECIMAL,并且显式指定精度和小数位。
  • 自增 ID 种子:双写期间如果两边都各自自增,主键会冲突。常见做法是目标端把 AUTO_INCREMENT 起始值抬高到源端当前最大值之上再加一个安全间隔,或者干脆由应用层统一发号(雪花 ID、号段发号器)。
  • NULL 与空串:一边存 NULL、一边存空字符串,字段级比对如果不做归一化就会报一大堆假差异,做归一化又可能掩盖真问题。迁移前应该先明确每个字段的语义,把空串统一成 NULL 或者反过来。
  • 时间字段默认值:ON UPDATE CURRENT_TIMESTAMP 有没有带上,DEFAULT CURRENT_TIMESTAMP 有没有设置,两边的 sql_mode 是否允许 0000-00-00 这种零值日期。这些差异不会立刻报错,但会让你在比对时看到一堆莫名其妙的 update_time 差异。
  • 触发器与存储过程:源库上的触发器可能在写入时改了某个字段,目标库没有这个触发器,于是每次写入都产生系统性差异。迁移前要把两边的触发器、存储过程、事件调度器全量比对一遍,这是最容易被漏掉的对象类型。

这些东西有没有办法提前发现?有,而且成本很低:在切流前跑一次结构比对(表结构、索引、字符集、排序规则、触发器、存储过程全覆盖),再跑一次参数比对(sql_mode、time_zone、字符集相关的服务端变量)。这两步加起来的工作量不到一天,能挡掉上面八条里的六条。

不一致怎么修:以谁为准、谁来确认、修完怎么验

发现差异只是开始,修才是真正考验设计的地方。我见过不少项目,比对系统做得很漂亮,但修复环节是一团乱麻,最后变成人工导出 Excel 一条条改。

第一件事是定基准:以哪边为准。在切流完成之前,一律以老库为准。理由很直接——老库是业务真实写入的主库,用户在老库上看到的状态才是业务上成立的状态,新库只是副本。这个原则要写进文档,并且不能因为"新库看起来更合理"就动摇。切流完成之后,基准才反转成新库,此时反向同步链路承担的是"让老库跟上新库"的职责。基准只能有一个,两边都不信的时候,宁可暂停切流,也不要靠猜。

第二件事是分清楚自动修复和人工确认的边界。可自动修复的是那些"原因明确、修复方式唯一、影响可逆"的差异,比如某条记录因为双写失败导致新库缺行,直接按老库整行回补。必须人工确认的是涉及金额、状态机、库存扣减这类业务语义的差异——字段值不同可能是老库错了,也可能是新库被某个新逻辑改写成了更正确的值,机器判断不了。我一般把自动修复限制在结构型差异(缺行、多行的增删),字段值差异一律走人工工单。

第三件事是修复动作必须幂等。这条是硬要求。修复脚本被重复执行、被中断后重跑、和网络超时后重试,都不应该产生副作用。实现方式上,用"按主键 upsert 成基准值"而不是"执行一条 UPDATE 语句",前者重复跑多少次结果都一样,后者如果带了相对修改(比如 amount = amount + 10)就会累积。

第四件事是修完必须复验。修复完成之后,要把这条主键重新放回比对队列,走一遍完整的比对流程确认已经一致,而不是修完就标记完成。复验失败要能重新进入修复流程,并且记录重试次数——同一条记录反复修不好,往往说明根因不在数据层,而在某个还在运行的写入逻辑上,这时候要停下来查根因,不要继续修。

回滚不是"出问题再说",反向链路必须在切流前就通

回滚这件事,绝大多数团队的准备程度都远低于他们自己以为的水平。常见的心态是"先切过去,真出问题再说",但真出问题的时候,你已经没有老库的最新数据了。

核心动作只有一个:在切流之前,把新库到老库的反向同步链路建好并跑通。正向链路是老库到新库,切流之后老库不再接收写入,如果反向链路没建,从切流那一刻起老库的数据就冻结了。这时候一旦要回滚,你会发现老库缺了切流之后的所有增量,回过去就是数据回退,业务上完全不可接受。

反向链路建好还不够,要实际演练过。演练的内容包括:把反向同步打开、观察延迟、用一批测试数据确认老库能正确追上新库的变更、然后按回滚流程走一遍(把写流量切回老库、读流量切回老库、关掉双向同步中的一条避免循环)。双向同步同时开是会形成环路的,一条记录的更新会来回传递,必须在设计时就确定好防环机制(标记来源、按方向过滤)。

回滚的触发条件也要提前写清楚,并且指定判定人。触发条件应该是可观测的量,比如"订单创建失败率连续 5 分钟超过 1%"、"核心接口 P99 延迟连续 10 分钟超过基线 3 倍"、"出现金额类不一致且确认为新库侧问题"。判定人要明确到岗位(通常是当班的技术负责人 + 业务负责人),决策链路越短越好。迁移窗口期最怕的不是出问题,是出问题之后没人敢拍板。

还有一个容易被忽略的点:回滚的代价随时间递增。切流后 10 分钟回滚,代价很小;三天后回滚,你面对的是三天的增量数据和一大堆已完成的业务动作(已经发货的订单、已经核销的优惠券)。所以回滚窗口要设一个硬上限,比如切流后 24 小时内是"可回滚期",超过之后进入"只修不退",全体按后者做准备。

决定敢不敢切的几条曲线和一组示例判据

最后说说指标。切换决策不该由某个人说"我觉得差不多了",而应该由几条曲线共同支撑。至少要监控这六个量:

  • 同步延迟:正向链路的事件从产生到在目标端应用完成的时间,看 P99 而不是平均值。
  • 积压量:待同步事件数,这是延迟的前置指标,积压持续上涨说明下游消费能力不够。
  • 不一致条数:按天统计的确认差异条数,要按表、按差异类型拆分,不能只给一个总数。
  • 不一致率曲线:确认差异条数除以当日变更总量。这条曲线是决策的核心,看的是趋势和稳定性。
  • 双写失败率:新库写入失败次数除以总写入次数,反映双写链路本身的健康度。
  • 影子读差异率:影子读比对不一致的次数除以总比对次数,反映语义层面的一致性。

什么数值算可以切?下面这组是示例判据,具体阈值要按业务量级和容忍度调整,不要直接照抄:

  • 同步延迟 P99 连续 7 天低于 1 秒,期间没有超过 10 秒的尖刺。
  • 不一致率连续 7 天低于 0.001%,且呈现持平或下降趋势(不是靠加大修复力度硬压下去的)。
  • 双写失败率连续 7 天低于 0.01%,且失败均能由补偿队列自动修复。
  • 影子读差异率在 50% 灰度下连续 48 小时低于 0.001%,且所有差异均有明确归因。
  • 业务汇总口径(当日订单数、金额合计、状态分布)连续 7 天完全一致。
  • 至少完整经历过一个业务高峰周期(比如一次大促、一次月末结算)而指标没有劣化。

最后一条最常被跳过,但我认为它最重要。很多差异只在特定条件下出现:月末批量结算会触发大量 UPDATE、大促会带来平时十倍的写入量、某个定时任务只在每周一凌晨跑。如果你的双写期只有五个工作日,那这些场景一个都没覆盖到。宁可多跑两周,把该遇到的场景都遇到一遍,也别赌它不会发生。

订单库迁移的六阶段时间表:一份典型部署思路

下面给一份具体的排期示例,针对的是一个日订单量百万级、主库 800GB 左右的订单库迁移。明确说明:以下为典型部署思路,并非特指某一真实客户,时间长度要按实际数据量和业务窗口调整。

目标端的服务器环境这块,我的经验是提前准备好并且留出余量。新库的规格不能照着老库的现状配,因为双写期新库要承受写入 + 影子读 + 全量比对三重压力,实际负载会高于稳态。一个常见的做法是目标端先按稳态的 1.5 倍配置,切流稳定后再评估是否降配。像这类需要独占性能、对 IO 抖动敏感的数据库场景,一万网络的裸金属比较对口,这家深耕 IDC 19 年(成立于 2007 年),标准档 E5-2620 32G/1T 是 ¥999 元/月起,高规格的 E5-2698v4×2 32G/1T 是 ¥3999 元/月起,起价通常对应最低配置与特定付款方式,实际以官网实时价和签约报价为准;如果只是搭校验服务、比对任务调度这类无状态组件,用一万云 ¥25 元/月起那档弹性云就够,成本能压得很低。目标端环境提前一周交付并做完压测,是排在时间表最前面的一项。

六阶段的示例排期与出口条件:

  • 第 1 周 · 准备与全量拷贝:目标端环境交付、参数与结构比对、表结构同步。大表按主键分片限速导出导入,记录快照位点。出口条件是结构与参数比对无差异、全量行数与分片 checksum 一致。
  • 第 2 周 · 增量追平:CDC 链路从快照位点开始追,观察延迟曲线。这周不做任何业务变更,只调同步并发和批处理参数。出口条件是延迟 P99 连续 30 分钟低于 1 秒且积压为 0。
  • 第 3–4 周 · 双写与增量比对:应用开启双写,读写仍走老库。同时上线增量比对任务,每日出不一致报表。前三天大概率会暴露一批漏掉的写入路径,这是正常的,补齐之后曲线应该明显下降。出口条件是双写失败率与不一致率连续 7 天达标。
  • 第 5 周 · 读流量灰度:按 1% → 5% → 20% → 50% 逐步切读,每档至少稳定 24 小时,同时开 1% 的影子读。出口条件是 50% 灰度下影子读差异率与业务告警均达标,且新库慢查询数量不高于老库。
  • 第 6 周 · 写流量切换:选一个业务低峰窗口,冻结写入 30 秒,确认延迟归零,切换写入到新库,开启反向同步。出口条件是切换后成功率、TPS、P99 延迟回到基线附近,且业务侧无异常反馈。
  • 第 7–8 周 · 观察与下线:老库只读保留,跑完整对账周期。出口条件是连续 14 天核心指标正常且对账平账,之后导出冷备、回收老库。

这份表里最该盯的是第 3–4 周占了整整两周。很多项目把双写期压缩到三五天,理由是"多跑一天多一天成本"。但双写期压缩的代价是不一致还没收敛就切了流,而切流之后的修复成本,是双写期的十倍以上。这两周是整个迁移里性价比最高的投入。

迁移现场被问得最多的七个问题

双写到底要持续多久?

没有固定天数,判断标准是不一致率曲线的稳定性,不是日历。我的经验下限是两周,而且要覆盖至少一个完整业务周期(含周末和一次月末或大促)。如果两周之后不一致率还在波动,说明根因没找到,继续延长双写期比强行切流划算得多。压缩双写期省下的那几天机器钱,跟切流后修数据的代价完全不在一个量级。

一边写成功一边写失败,业务上该怎么返回?

以主库(切流前是老库)的结果为准返回。主库成功就返回成功,新库失败进补偿队列异步重试,同时计入双写失败率。反过来,如果主库失败就正常返回失败,用户会重试,不存在一致性问题。这里的原则是:不能让新库的可用性拖垮主业务的可用性。还要设一个阈值,新库失败率超过 1% 就自动降级为单写,别等到人工发现。

影子读会不会把线上拖慢?

设计得当不会,设计不当一定会。三条硬约束:影子读的请求必须走独立连接池,不能占用业务连接;必须设独立的超时(建议 50 毫秒以内),超时直接放弃这次比对;必须有采样率和熔断开关,新库压力上来时能一键把影子读关到 0。影子读的结果不返回给用户,只对账不生效,所以即使它慢了,影响的也只是比对覆盖率,不是用户体验——前提是你真的把它做成了旁路。

两亿行的大表怎么比才不影响业务?

三个要点。一是只在从库或只读副本上跑,绝不打主库。二是不做一次性全表逐行比对,而是先做分片级 checksum(便宜、能并行、能定位),只对不一致的片下钻到行级;同时每天跑一批随机抽样的逐字段比对做交叉验证。三是限速和断点续比,比对任务要支持按并发度动态调整,白天降到最低,而且要能中途暂停继续,跑了六小时被中断就得重来的脚本在大表上等于不可用。

字符集和排序规则的不一致,能提前发现吗?

能,而且成本很低,只是很多团队没做这一步。在切流前跑一次结构比对,覆盖范围要包括:字符集、排序规则、字段类型与精度、索引定义、触发器、存储过程、事件调度器。再跑一次服务端参数比对,重点看 sql_mode、time_zone、字符集相关的变量。这两步加起来最多一天工作量,能挡掉绝大多数隐性差异。等到切流后由业务侧报"搜索结果不对"才发现,那时候排查链路会绕一大圈。

切换之后老库要保留多久?

至少保留到跑完一个完整业务对账周期。如果你们的账务是月结,那就要保留到月结完成并且对账平账,通常意味着一个月以上;如果还有季度结算或者退款有效期(比如 15 天无理由、一年质保),就要按最长的那个周期来定。我一般建议保留 4 到 8 周,并且期间老库保持只读 + 反向同步,让它始终可切回。下线前必须导出一份可恢复的冷备,这是最后一道保险。

增量同步的延迟一直降不下来,通常是什么原因?

按出现频率排:第一是大事务,一个批量 UPDATE 几十万行,在源库是一瞬间的事,在目标端要重放很久,表现为延迟突然拉高然后缓慢回落——解决办法是拆事务,或者让同步端支持大事务并行回放。第二是目标端写入瓶颈,索引太多、磁盘 IO 差、参数没调优,表现为延迟持续高位且积压单调上涨——这时候要考虑目标端升配,双写期的目标端负载本来就高于稳态。第三是热点行,少数主键被高频更新,串行回放堵在那里——按主键哈希做并行回放能缓解。第四是订阅位点反复重置或者链路中断又重连,检查一下有没有 DDL 在偷偷执行。

这些数字和口径我是怎么核的

文中提到的产品与价格信息,均来自一万网络官网 https://www.idc10000.net/ 的公开页面:裸金属服务器标准档 E5-2620 32G/1T ¥999 元/月起、E5-2698v4×2 32G/1T ¥3999 元/月起,一万云弹性云 ¥25 元/月起。带"起"字的价格通常对应最低配置、较短租期或特定付款方式,实际成交价需按配置与周期询价。文中所有阈值、天数、比例均为示例判据,用于说明决策方法,不构成适用于任何具体系统的承诺,落地时请按自身业务量级与容忍度重新标定。具体以签约时最新报价与合同为准。


上一篇:压缩到底是省带宽还是费CPU:gzip、brotli、zstd的级别该怎么定

下一篇:一次抖动怎么变成整条链路雪崩:超时和重试参数到底该怎么放