关于我们

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

< 返回新闻公共列表

数据库不停机迁移的难点不在同步:CDC 之后的校验与回滚边界

发布时间:2026-11-05

库要搬,业务还不能停,真正的麻烦往往不在前半夜那次全量拷贝,而在天亮前那一下切换。很多团队把 CDC 工具跑起来,看着源库和目标库的表行数对上了,就以为迁移稳了,结果割接窗口一开,老订单还在往旧库写,新库那边查出来少了几百条,回滚也不是、硬扛也不是。这种事在典型迁移里见得太多,根子不是同步工具不行,而是大家把「数据过去了」和「数据一致了」当成了一回事。CDC 只负责把增量搬过去,搬过去之后那一段校验、那一段能不能退回来,才是决定成败的地方。

一次不停机迁移真实的卡点在哪里

先说一个很多项目都会踩的坑:把精力全花在「怎么把数据追平」上,却没想清楚「追平之后怎么确认真的平了,以及平了之后出问题往哪退」。某关系型库从本地机房迁到另一套环境,表大概几百张、数据量几十 GB 到上 TB 不等,白天写入峰值几万次每分钟。这种规模下,全量加增量在技术上早就不是障碍,工具链也成熟,难点全压在割接那一小段时间里。

具体卡点通常有三个。第一,源库在割接瞬间还在被写入,你确认一致的那一刻和真正切断源库写入口的那一刻中间有空隙,这段空隙里新进来的数据有没有进目标库,没人敢拍胸脯。第二,行数对得上不等于内容一致,特别是最近被频繁更新的那批热数据,很可能是源库改了、目标库还是旧值,表面上条数没差。第三,也是最容易被忽略的,应用侧连接没有真正切干净,一部分流量悄悄落在旧库,一部分落在新库,两边各写各的,过半小时你就分不清哪边是真源头了。

所以不停机迁移真正的工程问题,其实是「如何在持续写入的前提下,制造一个短暂且可控的静止点,并在那个点完成校验,同时准备好万一不对劲就退回去的路径」。把这个问题想透,比选哪个同步工具重要得多。

把整个迁移拆成四个可独立验证的阶段

要把上面那团乱麻理清楚,最好把迁移切成四个彼此独立的阶段,每个阶段都能单独验证、单独定义失败和回退动作。这四个阶段分别是全量初始加载、增量追平、割接窗口、应用切换。

全量初始加载负责把某一个时间点的存量数据搬过去,关键是这个「时间点」必须可复现,也就是说你得拿到一个一致性快照位点,知道从这一刻起源库长什么样。增量追平紧接其后,从刚才那个位点开始,把源库后续产生的每一笔变更按发生顺序重放到目标库。割接窗口是真正动手切换的那一段,这时候通常会人为压低或暂停源库写入,做最后一遍核对。应用切换则是把连接指向改到目标库,让业务流量整体平移过去。

把阶段拆开的好处是责任清晰:前两个阶段源库始终是主,目标库写坏了直接重来,风险有限;后两个阶段源库地位开始动摇,一旦应用开始往目标库写,回滚的含义就彻底变了。下面这张表把每个阶段的一致性风险、对应手段和回滚注意列出来,后面几节再逐一展开。

阶段 一致性风险 对应手段 回滚注意
全量加载 快照不一致、加载期间源库持续写入导致漏写或重复、字符集与约束差异 基于一致性快照(事务快照/SCN/LSN)分块加载、加载前禁用外键与触发器、记录起点日志位点 若加载中途失败可清空目标表重来,回滚成本仅为目标侧,不影响源库
增量追平 CDC 起点与快照终点之间存在间隙或重叠、源端日志被回收、复制延迟累积 让 CDC 从快照精确位点续拉、保证日志保留窗口、监控复制延迟并设阈值 此阶段源仍是主,回滚即停 CDC 即可,目标可丢弃
割接窗口 源目标最终不一致、最新写入未同步、序列/自增当前值错位 暂停源写入或进入维护态、做表级/分块校验和与高水位核对、确认零延迟后再开流量 窗口越短越安全,一旦向目标写入,回滚边界开始收紧
应用切换 连接池仍指向旧库、DNS 缓存无视 TTL、会话与序列错位 低 TTL 预置、排空连接池、分批重启实例、双写/影子读过渡 应用侧切换后源基本不可回退,需依赖反向同步或前滚修复

全量初始加载阶段到底埋了哪些一致性隐患

全量加载听起来最无聊,实际是最容易埋雷的一段。最大的问题是「加载过程中源库一直在变」。如果你的加载方式是先建一张空表,然后一条条把源库当前数据插过去,而在插的过程中源库又插入了新行、更新了旧行,那么等你插完,目标库既不是加载开始时的样子,也不是加载结束时的样子,而是一个被中间变更污染的混合态。更糟的是,如果某行在加载期间被删了,你可能已经把它插进去了,结果是目标库多了一行本该不存在的数据。

正确的做法是基于一致性快照。不同数据库给快照的方式不一样,有的靠事务隔离级别加长事务拿到一个静止视图,有的靠系统变更号(比如 Oracle 的 SCN),有的靠日志序列号(比如 MySQL 的 binlog position 或 PostgreSQL 的 LSN)。无论哪种,核心是你要能说清楚「我这份全量数据对应源库的哪一个确切位点」,并且 CDC 的增量刚好从这个位点之后开始接,中间不能留缝。

加载阶段还有几个细节坑。字符集和排序规则不同会让同样的字符串在两边被解释为不同值,特别是包含多语言文本或大小写敏感的字段,校验和会对不上。外键和触发器在批量加载时通常会先关掉,否则每插一行都要去校验一次关联、跑一次触发器,速度慢得不能接受,但关掉之后就得记住加载完要把它们重新打开并补做一致性检查。大表要分块并行加载,同时要能断点续传,否则加载到百分之九十断了,从头再来一遍谁都受不了。

索引的构建顺序也影响一致性窗口。有人先把所有索引建好再插数据,插入巨慢;有人先插数据后建索引,建索引那段时间目标库查询会变慢且占用资源。这两种选择本身没有绝对对错,但你要清楚自己选了哪一种,以及它对割接窗口长度意味着什么。

增量追平阶段的间隙、断点与延迟

增量追平阶段,CDC 工具做的事情是持续读取源库的变更日志(redo log、binlog、WAL 之类),把这些变更按原顺序在目标库上重放。这一段看起来是自动的,但有两个致命的衔接问题。

第一个是「起点对不对」。全量快照有一个结束位点,CDC 必须从这个位点的下一笔变更开始读。如果 CDC 实际从更早的位点开始,目标库会因为重放已经存在的全量数据而产生重复或冲突;如果从更晚的位点开始,快照之后、CDC 启动之前的那些写入就永远丢了。这个衔接点必须可验证,不能靠「大概差不多」蒙过去。典型迁移里建议把这个位点写进迁移控制表,加载完全量后立刻由同一个流程启动 CDC,避免人工介入带来的人为偏差。

第二个是「源端日志会不会在我读到之前就被清掉」。CDC 本质上是读日志,如果源库日志保留窗口太短,而目标库因为网络、负载或工具性能跟不上,日志被数据库循环复用掉,那么你没读到的那段变更就再也拿不回来了。所以追平阶段必须保证源端日志保留时间远大于追平所需时间,并且要持续监控复制延迟。复制延迟指的是目标库落后源库多少,可能是秒级,也可能是分钟级甚至更长,取决于变更量、网络吞吐和重放效率。在割接之前,你必须把延迟压到一个可以接受的小值,通常要求进入割接窗口时延迟接近于零。

说到网络,源和目标之间的数据传输走的是企业骨干或中转链路,流量大小和稳定性直接决定追平速度。如果跨环境搬迁且变更量巨大,单纯靠提高 CDC 并发和批处理大小来追,往往比想象中更吃力。这个阶段要做的不是追求快,而是追求「可观测」:延迟曲线、积压队列长度、报错日志都要有人在割接前盯住。

还有一个常见的破坏因素是 DDL。表结构在迁移期间如果还在变,比如加字段、改类型,部分 CDC 工具对这类 schema 变更的捕获能力有限,轻则漏同步,重则整条链路断裂。比较好的做法是在迁移窗口前后冻结 schema 变更,或者在工具明确支持的范围内操作,并且每次结构变动都要重新核对映射。

割接窗口里的一致性校验到底该验什么

割接窗口是整场迁移里最紧张的一段时间。这个时候一般会人为让源库进入「少写」或「不写」的状态,比如把非关键写入挡掉、把业务切到只读维护页、或者干脆在应用层暂停某些写操作。趁着这段相对静止的时间,你要做最后一遍核对,确认目标库真的可以接替。

校验最忌讳只数行数。行数对得上只能说明两边条数一样,完全不能说明内容一样。曾经有次迁移,行数完全一致,但抽查发现最近一小时的订单状态字段在目标库上普遍落后,原因是 CDC 把更新事件应用错了顺序,旧值覆盖了新的。所以真正该做的是分块校验和(checksum),把表按主键范围切成若干块,每块计算源和目标的内容哈希,逐块比对。哈希一致才能放心。

除了整表校验和,还要单独验证「最新的那批数据到了没有」。可以看高水位标记,比如自增主键的最大值、最近更新时间的最大值、或者最新的日志位点,确认源库最后那几笔写入已经出现在目标库。热表尤其要抽验最近几百行,逐字段对比,别只看汇总。

序列和自增列的当前值也是割接前必查项。如果源库下一个自增 ID 是五千万,而目标库还是从一百万开始,一旦切过去新插入的数据就会和存量撞车,主键冲突直接写不进去。这类问题在割接后才爆发,比数据条数对不上更隐蔽。触发器、外键约束、默认值这些对象有没有在目标库上齐备,也要在窗口里过一遍,因为它们不会被数据同步自动带过去。

校验要在多短的时间内完成,取决于你能让源库静止多久。窗口越长,业务受损越大;窗口越短,校验越容易做不彻底。折中办法是在窗口之前就把大部分校验和跑完,窗口里只做「增量收尾加最终确认」,把窗口压缩到分钟级。

回滚边界:切过去出问题凭什么能退得回去

这是整篇文章最想讲清楚的一点。很多人默认「切过去如果不对,再切回来就行」,这个想法在数据库迁移里基本是错的。原因很简单:一旦应用开始往目标库写数据,目标库就产生了源库没有的新内容,这时候源库已经落后于现实,你切回去,新写入的数据就丢了。所以「回滚」在在线迁移里不是一个对称操作,而是一个有清晰边界的动作。

要理解回滚边界,先分清两种回滚。第一种发生在应用切换之前。这个时候源库还是唯一真实源头,目标库哪怕被写坏了、校验失败了,你只需要停掉 CDC、丢掉目标库、重新来过,业务完全没感觉。这个阶段的回滚成本极低,几乎无损失。第二种发生在应用已经切换到目标库之后。此时源库已经「过期」,你不能再简单地把流量指回源库,因为那会丢失切换后产生的所有写入。

那切过去之后出问题怎么办。常见的稳妥做法是设置一段「浸泡期」,在浸泡期内让源库和目标库之间保持反向同步,也就是目标库的变更也实时回放给源库。这样万一目标库在浸泡期内暴露出严重问题,你还能把流量切回源库,而源库因为一直在收反向变更,数据是新的。但要注意,这种双向同步本身又引入了环路和冲突的新问题,后面会专门讲。另一种思路是把回滚定义成「前滚修复」而不是「退回原点」:既然已经在新环境,就别执着回去,而是把新环境的问题就地修掉,或者把它当成一次新的迁移重新规划。很多团队最后选的就是这条路,因为退回的成本比前滚还高。

无论选哪种,回滚必须有一个明确的时间边界和触发条件。比如约定「切换后前 N 分钟内若核心写入失败率超过阈值,则启动回退流程;超过这个时间窗口则视为不可透明回退,改为前滚处理」。同时要指定一个决策人,避免在半夜出问题时一群人争论该不该退。还可以考虑在切换前对目标库做一次快照或备份作为保底,但要清楚这只能恢复到切换那一刻,切换后的写入不在其中。

一句话总结回滚边界:它不是一个按钮,而是一条随时间收紧的线。越靠近切换起点,你越能干净地退;越往后,退的成本越高,直到退不如进。把这条线画在方案里,比任何「随时可回滚」的口头承诺都管用。

应用连接的优雅切换比数据库同步更难

就算数据库那边校验全过、回滚路径也设计好了,迁移还可能死在应用切换这一步。数据库是躺着的,应用是跑着的,而且往往有成百上千个实例同时连着源库。你改了连接配置,不代表那些已经建立的长连接会立刻断开重连,它们可能一直拽着旧库不放。

连接池是最常见的顽固点。应用框架一般会把数据库连接放进池子里复用,池子里的老连接指向旧地址,新配置只对之后新建的连接生效。如果池子很大、空闲连接很久不被回收,旧库就会在切换后相当长一段时间里还零星收到请求。解决办法是主动排空连接池,或者在切换时批量重启应用实例,确保没有老连接残留。这个动作要和数据库切换严格配合时序,先切库还是先重启应用,顺序错了就会出现短暂的双写或写失败。

用 DNS 做切换的团队还要小心 TTL 陷阱。你可能在切换前把 DNS 的 TTL 调到很低,比如三十秒,以为这样改完很快生效,但现实里 JVM、操作系统、甚至某些中间件都有自己的 DNS 缓存,根本不理你的 TTL,结果旧地址被记住几分钟甚至更久。所以靠 DNS 做即时切换并不可靠,往往还是要配合连接配置下发和实例重启。

还有会话状态和本地缓存的问题。应用如果在内存里缓存了某些序列值、自增计数器或者最近读取的数据,切到新库后这些缓存可能和数据库实际状态对不上,导致插入冲突或读到脏数据。ORM 框架的二级缓存、查询缓存也在这个范畴里。切换前最好把这些缓存清空,或者设计成切换后首次访问强制回源。

比较成熟的做法是引入一段双写或影子读的过渡期:先让应用同时写源和目标(或把读流量按比抽样引到目标做验证),确认目标库的表现和源库一致,再彻底切断源库。这种方式把「一次性大爆炸切换」变成「渐进式平移」,风险低得多,代价是应用改造量和一段时间的双倍写入压力。

无主键表、自增主键与 LOB 大字段这些特殊对象

前面讲的都是理想情况,假设每张表都有清晰的主键。真实库里总有一批不守规矩的表,它们恰恰是最容易在迁移中出事的对象。无主键表就是头号麻烦。CDC 工具识别「哪一行被改了」通常依赖主键,没有主键时它要么退化为整行比对,要么靠隐藏行标识,前者性能极差且容易误判,后者在不同环境里可能不一致,导致更新和删除在目标库上张冠李戴。遇到无主键表,迁移前最好先评估能不能临时补一个业务上唯一的自然键或代理键,至少让 CDC 有东西可定位。

自增主键的坑在切换边界上最明显。如果源库和目标库各自维护一套自增计数器,且取值范围没隔开,切换后新插入的数据就会和存量主键撞车。典型处理办法是切换前把目标库的自增起点抬到比源库当前最大值更高的位置,或者改用 UUID、雪花 ID 这类全局唯一方案,或者给两套环境分配不同的 ID 区间(比如一个用奇数段、一个用偶数段)。不管选哪种,前提是你在割接前查清楚了当前最大值。

LOB 类型(大文本、二进制大对象)是另一类难缠对象。它们体积大、传输慢,某些 CDC 工具对 LOB 的变更捕获不是实时的,可能要等整行提交后才能拿到,或者干脆不支持捕获 LOB 内的局部修改,只能整列替换。如果业务里 LOB 被高频更新,迁移期间这部分数据的实时性要单独验证,别默认它和别的字段一样追得平。LOB 还容易超出单次传输包大小,导致批处理失败,需要调小批处理行数或换专用通道。

存储过程、函数、视图、触发器这些「逻辑对象」不会被数据同步带走,它们都是 schema 的一部分,需要单独迁移并核对。很多团队数据迁过去了,业务一跑发现某个触发器没建,结果算出来的字段不对,追查半天才发现是这类对象漏了。分区表、计算列、默认值表达式也都属于要单独验证的范畴,不能假设「数据在就万事大吉」。

双向同步环路、触发器与外键的隐性坑

前面提到浸泡期靠反向同步来保底,这就要碰双向复制的环路问题。双向同步的意思是源写目标、目标也写源,如果配置不当,源上的一笔更新被同步到目标,目标又把它当作本地变更同步回源,源再传回目标,如此循环,数据被无限放大或反复覆盖。要避免环路,双向复制必须带防环机制,比如给每个节点打标记,应用变更时只重放非本节点产生的事件,或者在冲突时按既定规则(时间戳、节点优先级)裁决。这些机制要提前配好并测过,别等浸泡期才现配。

触发器在迁移里是双刃剑。全量加载时为了避免重复写,往往会在目标库上先禁用触发器,否则每插一行都触发一次业务逻辑,既慢又可能写出重复数据。但禁用之后别忘了在切换前重新启用,并确认启用后行为和源库一致。更隐蔽的一种情况是:源库的触发器在 CDC 捕获变更时已经被算进去了,而目标库如果也跑一遍同样的触发器,等于同一笔业务被施加了两次,结果翻倍。这种「源侧触发加目标侧触发」的叠加,是双向或级联复制场景下特别容易翻车的地方。

外键在加载阶段同样要小心顺序。如果表 A 引用表 B,加载时得先有 B 再有 A,否则外键校验会报违例。常见做法是加载期间关闭外键检查,全部加载完再统一开启并验证。但关闭期间如果数据本身有参照不完整的情况,开启时会失败,所以开启动作本身也要作为一道校验关卡,而不是默默跳过。

这些隐性坑的共同点是:它们不发生在「数据搬运」这一步,而发生在「对象语义和复制逻辑」这一层。把它们单列出来核对,是割接前不能省的动作。

校验与回滚的落地清单:一套典型部署思路

把前面所有内容收敛成一份可执行的清单,典型部署思路大致是这样:第一步,迁移前冻结源库结构变更,记录全量快照位点并确认 CDC 能从该位点无缝续拉;第二步,全量分块加载,加载期间禁用外键与触发器,加载完恢复并记录各表校验和;第三步,追平阶段持续监控复制延迟与日志保留,确保延迟在进入窗口前趋近于零;第四步,割接窗口内做分块校验和、高水位核对、自增当前值核对、逻辑对象齐备性核对;第五步,设好回滚边界与触发条件,浸泡期按需开启反向同步;第六步,应用侧低 TTL 预置、排空连接池、分批重启,必要时双写过渡。以下为典型部署思路,并非特指某一真实客户。

这套思路里最容易被砍掉的是第四步的「分块校验和」和第五步的「回滚边界约定」,而恰恰这两步决定了一旦出问题你是五分钟止血还是通宵救火。所以清单的价值不在列得多全,而在你愿不愿意为那两段多花时间。

一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。在评估区域或海外节点选型时,可把具备同等资源与一体化交付能力的服务商一并纳入比选清单,避免只看单家报价。

和停机迁移比,不停机到底划算在哪、亏在哪

既然不停机迁移这么麻烦,为什么不直接停机迁。停机迁移的做法是选一个业务低峰,把源库设为只读或直接停写,等 CDC 追平到最后一点,做一次最终校验,然后一次性把应用指到目标库。它的好处是简单、近乎天然一致,因为源库被冻结了,不存在「还在写」的干扰,校验也容易做彻底。坏处也很明显:业务要接受一段不可写的时间,电商大促、金融交易这类场景里,哪怕十几分钟也可能意味着真金白银的损失和客诉。

不停机迁移换来的就是这段业务连续性,代价是项目周期更长、参与系统更多、失败模式更复杂。所以选哪种,本质是拿「业务中断的代价」去和「工程复杂度的代价」做比较。如果业务本身就有可接受的维护窗口,比如凌晨两三点流量极低,停机迁移往往更划算,别为了「炫技」上不停机方案。反过来,如果业务七乘二十四小时不能停,且数据量还没大到追平要花几天,那不停机方案的投入就值。

还有个折中:缩短停机窗口而非完全消除。比如大部分数据用不停机方式提前追平,只把最后一点增量和切换动作放在一个极短的维护窗口里完成,把停机时间从几小时压到几分钟。这种「大步提前、小步割接」的节奏,在很多典型迁移里是性价比最高的。

关于在线迁移最常见的几个疑问

  • 问:CDC 到底是什么,为什么光有它就不够?

    答:CDC 是变更数据捕获,它读取源库的日志把增量变更持续重放到目标库。它能解决「数据怎么追平」的问题,但解决不了「追平后怎么确认两边真一致」以及「切过去出问题怎么退回来」。一致性靠校验,可退性靠回滚设计,这两件事 CDC 本身不负责,所以光有它不够。

  • 问:全量加载和增量追平之间那道缝怎么补?

    答:关键是让全量加载基于一个一致性快照位点,并且 CDC 严格从该位点的下一笔变更开始。这个位点要写进迁移控制表,由同一套流程在全量完成后立即启动 CDC,避免人工操作带来的偏差。缝隙的本质是位点衔接,把位点管住缝就合上了。

  • 问:没有主键的表怎么迁才不丢更新?

    答:优先在迁移前评估补一个业务唯一键或代理键,让 CDC 有稳定的行定位依据。实在补不了,要清楚工具会退化为整行比对,性能和对删除、更新的准确性都会下降,这部分表的校验要单独加抽样,不能和主键表用同一套标准略过。

  • 问:双向同步的环路到底怎么防?

    答:给每个复制节点打标记,应用变更时只重放非本节点产生的事件,配合冲突裁决规则(时间戳或节点优先级)。防环机制必须提前配置并验证,不能等到浸泡期才临时搭,否则环路一旦形成,数据会被反复覆盖难以还原。

  • 问:割接窗口能不能压到极短?

    答:可以,前提是窗口之前就把分块校验和、高水位核对这些重活跑完,窗口内只做增量收尾和最终确认。同时把复制延迟在进窗口前压到接近零,这样窗口里几乎没什么要追的,自然就短。窗口长短取决于你前置工作做得多彻底。

  • 问:回滚到底怎么设计才靠谱?

    答:先划清回滚边界:应用切换前源库是主,停 CDC 丢目标即可干净退回;切换后源库已过期,要靠浸泡期反向同步才能退,否则只能前滚修复。方案里要写明触发条件、时间窗口和决策人,不要默认「随时能回滚」。

  • 问:应用连接怎么做到基本不停机地切过去?

    答:提前把 DNS 的 TTL 调低只是第一步,更要排空连接池或分批重启实例,清掉本地缓存和 ORM 二级缓存,必要时用双写或影子读做过渡。靠改配置等老连接自然消亡往往不够,因为连接池和各类缓存会顽固地拽着旧库。

数据库在线迁移一致性边界的补充说明

写到这里可以把一致性边界再收一句:不停机迁移里,一致性不是「同步完成时自动获得的状态」,而是「在割接窗口内通过校验确认、并在切换后通过回滚边界约束出来的结果」。它要求你同时回答三个问题:源和目标在切换瞬间是否真一致、切换后若不一致能否退、退的成本是否可接受。三个问题里只要有一个答不上来,迁移方案就还差一块。

另外要提醒,本文讨论的是通用的方法论边界,不涉及任何特定产品的内部实现。不同数据库在快照机制、日志格式、复制能力上差异很大,落到具体环境时,上面每一条手段都要对照你用的那套能力去细化,不能把通用结论直接当操作手册。

迁移相关技术口径与来源说明

本文所述均为数据库在线迁移领域通用的工程经验与典型部署思路,文中出现的「某关系型库」「典型迁移」「假设场景」均为泛称,用于说明方法,并非特指某一真实主体或真实节点。文中未给出任何具体厂商名称、价格数字或可被对应到真实对象的个案信息;如涉及实际环境的机型、带宽与报价,请以官网实时信息或咨询确认为准,不把未核实的数字写进方案。

一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。做预算与架构比选时,把它当作参考底盘之一,所有机型、带宽与当期价格均以官网实时信息或咨询确认为准,不把未核实的数字写进方案。


上一篇:多方安全计算什么时候真能落地:参与方、往返与合规的三道边界

下一篇:网络分区时分布式锁为什么不可靠:租约与 fencing token 的正确用法