数据迁移时最危险的判断,往往是“脚本已经拆成批次,而且安排在凌晨执行,应该不会影响线上”。我见过一类生产事故:迁移任务每批只更新几千行,数据库 CPU 也没有打满,但接口 P99 延迟从 180 毫秒升到 2.6 秒,连接池开始排队,最终根因不是“锁太多”,而是一个未提交的长事务把迁移和线上写入串成了阻塞链。数据迁移怎样减少严重锁等待,核心不在于找一个更晚的执行时间,而在于把迁移设计成可评估、可限速、可暂停、可恢复、可校验和可回滚的工程流程。
很多团队把“分批”当作降低锁风险的全部答案。实际上,批次数量只是表面参数,真正决定阻塞程度的是每个事务从获得锁到提交之间持续了多久,以及这些锁是否恰好覆盖业务最繁忙的记录范围。
同样是每批处理 5000 行,如果迁移条件无法使用索引,数据库可能先扫描数百万行,再修改目标记录;如果批次内还包含复杂函数、触发器或大量二级索引维护,事务持续时间仍然可能很长。相反,按照主键连续范围定位数据,每批只处理几百行,往往比使用不稳定的分页方式更容易控制。
我的判断顺序通常是:执行计划是否稳定,事务边界是否清晰,批次是否可暂停,最后才是每批多少行。如果前面三个问题没有答案,直接讨论“每批 1000 行还是 1 万行”,通常只是精细化地猜错参数。
CPU、内存和磁盘吞吐可以告诉我们数据库是否繁忙,却不能直接回答“谁在阻塞谁”。锁等待严重时,最需要的是一条完整链路:等待事务、阻塞事务、持锁对象、事务开始时间、业务来源、预计回滚成本。
例如,迁移任务可能只是等待一个已经运行 40 分钟的订单事务。此时继续调低迁移并发没有实质帮助,因为真正的瓶颈是长事务没有提交。技术负责人如果只看到迁移线程耗时变长,很容易误判为数据库性能不足,随后增加并发,结果反而让等待队列更长。
没有停止条件的迁移脚本,本质上是一个只能启动、不能治理的生产程序。启动前就应该约定:业务 P99 延迟连续多少个采样周期超过基线时暂停,最长锁等待达到什么数值时限速,复制延迟达到什么水平时停止,磁盘或事务日志剩余空间降到什么阈值时退出。
这些阈值不能从网上复制。电商下单、支付、内部报表和批处理系统的容忍度不同,应该根据过去 7 到 14 天的业务基线、峰值流量和应急能力确定。没有基线,就没有真正可执行的告警阈值。

凌晨两点通常只是用户请求减少,并不代表数据库进入空闲状态。定时结算、数据同步、备份、报表生成、消息补偿、风控计算和批量对账,往往都集中在这个时间段。如果迁移窗口和这些任务重叠,数据库看到的仍然是多类事务同时竞争。
我在迁移排查中会特别关注“非核心业务连接”。不少阻塞并非来自主交易接口,而是来自一个没人记得的运营报表、数据导出任务或重试队列。它们不一定流量大,却可能打开事务后长时间不提交,成为迁移操作的隐形闸门。
一次性更新大量数据的风险,不只是锁持有时间变长。事务越大,产生的重做日志、回滚段或复制日志越多,提交时的刷盘压力也可能突然升高。迁移中途如果失败,数据库还要花较长时间回滚,而回滚期间可能继续影响同一批业务访问。
但把一个大事务机械地拆成很多小事务,也不是无条件正确。批次过小会增加提交次数、索引定位次数和调度开销;如果批次之间没有稳定游标,还可能重复扫描已经处理过的行。合理做法不是追求最小批次,而是寻找“单批锁影响可接受、吞吐仍然稳定”的区间。
批量更新属于数据层面的读写竞争,创建索引、修改列属性、重命名对象等操作,还可能受到元数据锁影响。即使结构变更工具声称支持在线执行,开始阶段或最终切换阶段仍可能需要获得短暂的元数据锁。
线上存在长事务时,短暂的元数据锁也可能等很久。更麻烦的是,后续请求可能排在等待队列中,形成“一个结构操作等待旧事务,新的业务请求又排在结构操作后面”的队列效应。因此,在线 DDL 不是“完全无锁”,而是把锁风险从全程持有,转移到特定阶段和特定前置条件。
锁等待是一个事务暂时拿不到需要的锁;锁竞争则是多个事务频繁争抢同一资源。短暂等待并不一定是事故,真正需要警惕的是等待时间持续上升、阻塞事务数量扩散、业务延迟同步恶化。
| 现象 | 可能原因 | 优先判断 | 不建议立即做的事 |
|---|---|---|---|
| 迁移任务单独变慢 | 执行计划退化、扫描范围过大、磁盘吞吐不足 | 检查执行计划和迁移条件 | 直接提高并发数 |
| 线上写入延迟上升 | 迁移锁与业务写入争用同一批记录 | 查看阻塞链和热点键分布 | 只观察 CPU 使用率 |
| 大量新请求排队 | 长事务或元数据锁形成队列 | 定位最早持锁事务 | 批量终止所有数据库连接 |
| 数据库资源不高但请求超时 | 连接在等待锁,线程并未消耗大量 CPU | 检查锁等待时长和连接池占用 | 盲目扩容计算资源 |

不同迁移动作的风险差异很大,不能统一套用“低峰期加分批”的方案。评审时,我会先把任务分成四类:同表批量更新、历史数据归档或删除、跨库全量与增量迁移、表结构变更。
同表批量更新的主要矛盾是记录锁和事务日志;归档删除除了锁,还要关注外键、级联操作和空间回收;跨库迁移重点是增量追平与切换窗口;结构变更则需要重点核查元数据锁、引擎能力和版本限制。
| 迁移类型 | 主要风险 | 优先控制项 | 常见方案 |
|---|---|---|---|
| 批量更新字段 | 记录锁、日志增长、热点写入冲突 | 切分键、执行计划、事务大小 | 主键范围分批、限速提交 |
| 历史数据归档 | 删除锁、外键约束、空间和日志压力 | 归档顺序、级联关系、可回滚性 | 先复制后删除、分区交换、按时间窗口处理 |
| 跨库全量迁移 | 复制延迟、增量丢失、切换不一致 | 增量捕获、校验、切换回退 | 全量加增量同步、灰度切换 |
| 表结构变更 | 元数据锁、重建索引、切换等待 | 版本兼容、长事务、在线能力 | 在线 DDL、影子表、分阶段发布 |
批量迁移最好基于主键、唯一键或其他稳定且有选择性的索引切分。一个常见错误是使用“跳过多少行、取多少行”的分页方式循环处理。随着已处理数据增加,数据库可能反复扫描前面的记录,导致每批耗时逐渐变长,锁持有时间也随之扩大。
更稳定的方式是记录上一批的最后一个主键值,用范围条件处理下一批。下面的示例只展示思路,实际语法和锁行为需要结合数据库类型、隔离级别、索引结构以及业务字段确认。
— 示例:按递增主键分批处理
UPDATE orders
SET normalized_status = CASE
WHEN status = 'paid' THEN 1
WHEN status = 'cancelled' THEN 2
ELSE 0
END
WHERE id > :last_id
AND id <= :next_id
AND normalized_status IS NULL;
COMMIT;
执行计划检查不能只看“用了索引”四个字。还要看实际扫描行数、预计返回行数、回表次数、排序或临时表、触发器执行时间以及单批日志量。如果估算行数和实际行数差距很大,迁移批次的耗时可能在生产并发下突然失控。
迁移前至少要回答三个问题:当前是否存在持续时间异常的事务,迁移窗口内有哪些定时任务,哪些连接允许执行大范围查询或导出。不能只看当前时刻的快照,因为许多长事务恰好在业务操作后才出现。
我建议把迁移前观察窗口设为一个完整业务周期。对有明显日峰值的系统,至少观察一个工作日和一个非工作日;对月末结算、节假日促销或周期性批处理系统,则要把特殊窗口纳入评估。
很多方案文档写着“失败可回滚”,但没有说明回滚的是哪一层。数据事务回滚、迁移进度回退和业务流量切回,实际上是三个不同问题。
如果只能回滚当前事务,却无法撤销已经提交的业务语义变化,就不能把方案宣传为“完整可回滚”。更准确的说法应该是“支持批次级失败重试,业务切换具备回退路径”。

如果目标数据量不大、写入热点分散、执行计划稳定,可以采用简单的范围分批。重点不是追求极高吞吐,而是保证每批可以在明确时间内完成并提交。
每批之间可以设置极短的间隔,让线上请求获得更多调度机会。但间隔不是越长越好,过长会使迁移窗口明显延长,也可能导致业务数据在批次之间发生变化。比较稳妥的做法是先用小批次建立基线,再根据锁等待和业务延迟动态调整。
按固定数量切分看起来简单,但每批数据的行宽、索引数量和访问热点可能差异很大。按主键范围切分更容易解释进度,也便于发现某一段数据是否特别慢。
例如历史订单表中,早期数据几乎没有更新,最近 30 天却是高频写入区。将全表均匀划分为若干行数相等的批次,可能把最危险的热点集中到业务高峰附近。更合理的方式是把冷数据和热数据分开,冷数据提高批次大小,热数据降低批次大小,甚至安排在独立窗口执行。
迁移前可以根据过去一段时间的访问日志或数据库统计信息,识别写入最频繁的主键范围、租户、分区或业务状态。先处理低频区域,既能验证脚本,也能获得实际吞吐和日志增长数据。
对于热点区域,我更倾向于采用动态限速,而不是固定并发。比如以业务 P95、P99 延迟和锁等待时长作为反馈信号:指标稳定时逐步提高批次,指标接近阈值时降低批次或暂停。这种方式虽然实现复杂一些,但比“每分钟固定处理 10 万行”更能适应线上波动。
跨库迁移通常不会因为复制动作本身就锁住源库所有业务,但全量读取可能增加 I/O,增量捕获、目标端写入和最终切换也会形成新的压力。真正难的是全量和增量之间如何衔接,以及切换时如何证明新旧数据已经追平。
一个完整的流程通常包括:源端全量读取、增量变更捕获、目标端回放、关键表校验、增量延迟收敛、短时流量切换、切换后观察和回退窗口。每个阶段都要有独立的退出条件,不能因为“全量已完成”就默认迁移成功。
在线结构变更工具可以减少长时间表锁,但它不能消除长事务造成的元数据锁等待。执行前应检查是否有长时间未提交的事务、是否有持续占用目标表的查询、目标数据库版本是否支持当前操作,以及最终切换阶段预计需要多久。
如果目标是新增索引,应先确认业务是否能接受索引构建期间的资源消耗;如果是列类型或字段语义变化,应采用兼容式演进,例如先增加新字段、双向写入、完成历史回填、切换读取逻辑,再删除旧字段。结构迁移的安全性,往往来自应用兼容设计,而不是来自某个“在线”参数。

处理严重锁等待时,最容易犯的错误是看到等待数量上升,就直接终止迁移连接,或者批量清理所有慢连接。这样做有时能快速止血,但也可能让一个大事务进入漫长回滚,短时间内产生更大的资源压力。
我通常按以下顺序建立现场判断:
不同数据库的锁模型和诊断接口差异明显。MySQL、PostgreSQL、Oracle 以及分布式数据库对事务、锁类型、等待事件的命名和可见性并不一致。通用流程可以复用,但诊断 SQL 必须以目标数据库的官方文档和实际版本为准。
这种情况常见于迁移脚本正在更新业务近期频繁写入的记录。迁移事务持有目标行锁,线上写入只能等待。若迁移继续推进,等待请求会占用连接池,随后影响更多接口。
优先动作是暂停迁移,确认业务延迟是否恢复。如果暂停后延迟迅速回落,说明迁移与线上写入之间存在直接竞争;后续应调整热点范围、降低批次或改用不直接修改在线记录的方案。
此时迁移只是受害者。常见来源包括手工事务未提交、报表查询持有快照、后台任务异常、连接池归还连接前没有正确结束事务。不能因为迁移任务报错,就默认迁移脚本是根因。
处理这类问题时,需要先判断阻塞事务是否仍有业务价值。如果是核心支付事务,应优先等待或与业务负责人确认;如果是明显异常的后台连接,则可以按照应急预案终止,并记录回滚时间和副作用。
结构变更在等待一个旧事务,新的查询或写入又排在结构变更后面时,表面上看是大量业务请求突然变慢,实际根因可能只有一个元数据锁等待。此类问题要求现场人员查看对象级等待关系,而不是只查看当前执行 SQL 的耗时。
如果一上来就终止连接,后续可能无法还原阻塞链。最低限度应记录事务 ID、连接 ID、开始时间、SQL 摘要、数据库用户、客户端地址、对象名、等待时长、迁移批次和业务接口指标。
这些信息对复盘非常重要。没有现场证据,复盘往往只能写成“迁移期间数据库变慢,后续加强监控”,下次事故仍会重复。技术负责人要推动把现场采集做成自动化,而不是依靠工程师临时复制屏幕内容。

下面这个案例采用情景化数据,目的是还原生产中常见的故障链,不代表某个特定客户或平台。某交易系统需要为历史订单补充标准化状态字段,目标表约有 1.8 亿行,近 30 天数据仍处于高频写入状态,日均新增订单约 260 万行。
初始方案很直接:在凌晨执行一条批量更新语句,按照状态字段判断并回填新字段。为了“提高效率”,任务设置了 4 个并发 worker,每个 worker 处理不同时间范围。测试环境中数据量只有生产环境的 5%,脚本运行 35 分钟,没有出现明显告警,于是团队认为方案可以上线。
生产执行 18 分钟后,订单写入 P99 从 230 毫秒升至 1.9 秒,锁等待数快速增加,连接池使用率从 61% 升至 91%。数据库 CPU 只有 47%,磁盘吞吐约为平时峰值的 64%,这也是现场人员最初误判的地方。
进一步检查发现,迁移条件虽然包含时间字段,但组合索引无法覆盖实际过滤方式,数据库需要扫描大量记录后再回表更新。与此同时,迁移范围包含近 30 天订单,其中一部分正被订单状态更新事务频繁写入。
另一个问题是,4 个 worker 的范围边界按照时间切分,而不是按照主键或分区切分。相邻时间段内存在大量热点订单,多个 worker 仍然会访问相近的索引页和数据页,导致并发收益低于预期。
现场还发现一个对账任务在迁移启动前打开了事务,持续时间已经超过 50 分钟。它并没有持续占用大量 CPU,却让部分结构检查和记录访问出现等待。迁移任务只是把原本隐藏的长事务问题放大了。
| 观察项 | 迁移前 | 迁移中 | 判断 |
|---|---|---|---|
| 订单写入 P99 | 230 毫秒 | 1900 毫秒 | 已明显影响核心业务 |
| 连接池占用率 | 61% | 91% | 等待请求正在消耗连接资源 |
| 数据库 CPU | 39% | 47% | 不能据此排除锁等待 |
| 复制延迟 | 约 2 秒 | 升至 74 秒 | 迁移日志量已影响下游同步 |
| 最长活跃事务 | 约 8 分钟 | 超过 50 分钟 | 存在迁移之外的长事务风险 |
技术负责人没有立即把所有等待连接全部杀掉,而是先暂停 4 个迁移 worker,保护订单写入和支付相关接口。暂停后,新的锁等待不再快速增加,但部分连接仍然需要等待已有事务完成。
随后,团队单独处理异常对账事务,并保留其终止前的现场信息。由于该事务已经运行较长时间,终止后回滚耗时约 11 分钟,期间没有继续启动迁移。这个动作看起来让恢复变慢,却避免了在回滚尚未结束时叠加新的批量更新。
业务 P99 恢复到 360 毫秒附近后,团队重新做了执行计划检查,并将迁移策略改为:按主键范围切分、每批 2000 至 5000 行、单 worker 顺序提交、热数据延后处理、批次间根据 P99 和锁等待动态休眠。
优化后的情景演练中,冷数据批次平均耗时 1.4 秒,热数据批次平均耗时 3.8 秒。虽然总迁移时间从原计划的 3 小时延长到约 5.6 小时,但订单写入 P99 的峰值控制在 620 毫秒,复制延迟最高约 18 秒,未触发业务级超时。
这个结果说明,迁移方案不能只比较“脚本什么时候完成”。如果为了提前 2 小时结束迁移,却让核心写入延迟持续升高,方案的实际成本可能包括超时重试、重复扣款风险、客服介入和数据修复,远高于多占用几个小时的迁移窗口。

测试环境和生产环境最大的差异,往往不是数据量,而是并发访问方式。生产系统可能有持续写入、长连接、触发器、复制链路和后台任务,这些条件在测试环境里很难自然出现。
演练至少要尽可能复现三类输入:接近生产的数据规模、接近生产的索引和统计信息、接近生产的业务并发。若无法完全复制生产,可以明确哪些风险未被验证,并在上线方案中增加更保守的限速和观察窗口。
“每批 1 万行”不是数据库通用安全线。单行大小、索引数量、触发器、外键、网络延迟、日志刷盘速度和热点分布,都会改变单批事务的实际成本。
批次大小应该通过压测得到:先从小批次开始,测量单批耗时、锁等待、日志增长和业务延迟,再逐步增加,找到吞吐提升已经变小而风险开始上升的拐点。
迁移进程显示成功,只能说明脚本没有报错,不能说明业务没有受到影响,也不能说明数据已经一致。必须把迁移状态和业务指标放在同一个观察面板上。
终止连接有时是必要的,但它不是默认动作。一个大型事务被终止后,回滚本身可能持续很久;如果应用端配置了自动重试,还可能立刻创建更多相同请求,形成新的竞争。
更稳妥的判断是:先停止制造新压力的迁移任务,再识别持锁事务的业务价值,最后评估终止、等待或切换流量的成本。
在线迁移工具解决的是部分执行机制问题,例如影子表、增量追踪和最终切换,但它不能替团队决定切分策略、验证数据一致性、处理异常长事务,也不能保证应用已经兼容新旧结构。
工具引入后,风险从“直接修改原表”变成“额外空间、复制压力、增量追平、最终切换和回退复杂度”。技术负责人要重新评估整条链路,而不是把工具名称当成安全证明。
总耗时是效率指标,不是完整的成功标准。更有价值的是观察迁移期间业务增加了多少延迟、产生了多少重试、复制延迟多久恢复、失败批次是否可重跑,以及迁移后校验花了多少人工时间。
如果一个方案总耗时短,但让业务产生大量超时和人工修复,它的真实效率可能远低于一个耗时更长、但过程稳定的方案。

这类任务可以采用主键范围分批和顺序提交,不必一开始就引入复杂的双写或影子表。建议先用较小批次执行,连续观察若干批次后再逐渐增大处理量。
取舍是:开发和运维成本较低,但迁移总时长可能较长。适合内部系统、低频写入表和可接受较长窗口的业务。
这类任务不应只依赖一条批量更新语句。可以采用全量加增量、影子表、在线迁移工具或按分区逐步搬迁的方式,具体取决于数据库能力和应用改造空间。
重点是先把历史数据和实时变更解耦。历史数据可以限速处理,实时写入通过增量捕获或应用层兼容逻辑保持同步,最后再安排一个短切换窗口。
取舍是:线上锁竞争可能降低,但空间占用、链路复杂度、校验成本和切换风险都会提高。适合核心交易表和不能长时间停写的系统。
热点表的风险不在于总行数,而在于迁移是否碰到正在被频繁更新的记录。建议先按访问热度分层,优先处理冷数据,再评估热数据是否可以通过业务状态隔离、读写分离或应用兼容方式处理。
如果必须更新热点记录,建议降低单批量、降低并发、避开关键交易时段,并为核心接口设置更严格的停止条件。不要为了追赶进度,持续扩大 worker 数量。
删除通常比更新更需要谨慎。删除可能触发外键检查、级联删除、索引维护和空间回收,还可能让备库或下游同步产生较大延迟。
较稳妥的做法是先复制或确认归档数据,再按时间范围或主键范围分批删除。删除前要确认业务查询不会依赖这些数据,删除后要验证关联表、统计口径和恢复路径。
先核查数据库版本、存储引擎、在线变更能力和最终切换行为。对高并发表,应安排长事务检查,并明确变更等待多久后自动退出。
如果业务允许,应优先采用向后兼容的两阶段或三阶段发布:先增加新结构,再让应用同时兼容旧字段和新字段,完成回填和校验后切换读取,最后再清理旧结构。
此时不要继续执行原迁移方案,也不要先讨论如何提高吞吐。第一目标是阻止等待队列扩大,第二目标是恢复核心业务,第三目标才是决定迁移是否继续。

建议至少按照影响对象、数据规模、业务写入频率、是否需要停写、是否涉及结构变更和回滚复杂度进行分级。低风险任务可以简化审批,高风险任务必须经过数据库、应用、运维和业务共同评审。
| 风险等级 | 典型任务 | 最低要求 | 负责人关注点 |
|---|---|---|---|
| 低 | 低频表小批量字段修正 | 执行计划、分批、备份或恢复验证 | 是否幂等、是否可重试 |
| 中 | 大表历史回填、归档删除 | 生产演练、实时监控、暂停条件、数据校验 | 日志、复制和业务延迟是否可控 |
| 高 | 核心交易表结构变更、跨库切换 | 灰度、回退、现场指挥、业务确认 | 一致性、切换和恢复时间目标 |
一个合格的迁移程序,不应该只是把 SQL 放进定时任务。至少应具备配置化批次、进度持久化、失败重试、暂停恢复、超时退出、日志记录和校验输出等能力。
进度记录不能只记录“已经处理 30%”。更有价值的是记录最后成功的主键范围、批次开始和结束时间、实际影响行数、异常信息以及校验结果。这样才能在中断后准确续跑,也能快速判断某个范围是否需要人工复核。
阈值不是写完就不变的配置。每次迁移后都应记录单批耗时分布、锁等待最大值、业务延迟峰值、复制追平时间和实际回滚耗时。下一次评审时,这些数据比通用文章里的“建议批次大小”更有参考价值。
如果某类任务连续多次在相同指标上触发暂停,说明问题可能不只是执行参数,而是架构或数据模型需要调整。例如热点记录长期集中在同一张表,说明分区、异步化或业务拆分可能比继续优化迁移脚本更值得投入。

高吞吐执行适合低频写入表、可停机系统、独立历史库或明确拥有较长维护窗口的任务。此时可以提高批次大小,甚至采用并行处理,但仍要设置日志空间、回滚时间和异常退出条件。
它的优势是窗口短、基础设施占用时间少;短板是对执行计划、硬件能力和业务窗口要求高。一旦判断错误,故障扩散速度也最快。
低并发、动态限速和较小事务更适合核心交易系统。它通常会牺牲迁移总时长,但能降低接口延迟、连接池耗尽和复制延迟失控的概率。
这种方案并不意味着绝对安全。迁移时间变长后,跨越业务高峰的概率反而增加,因此必须具备暂停恢复、阶段性校验和多窗口续跑能力。
当目标表规模很大、业务不能停写、结构变更风险高时,在线迁移工具或影子表方案具有价值。它们能够将长时间操作拆解为多个阶段,减少直接阻塞原表的机会。
代价是增加存储、复制、监控和切换复杂度,也要求团队理解工具在目标数据库版本下的限制。若团队没有演练过最终切换和回退,工具可能只是把风险推迟到最难处理的时刻。
如果数据一致性要求极高、系统规模可控、业务能够接受短暂停写,明确的维护窗口有时比复杂的在线迁移更可靠。停写能够减少并发变化,使校验和切换更容易解释。
但停写并不等于可以省略备份、验证和回滚。停写期间仍可能有后台任务、连接未提交事务或管理员操作,窗口前必须清理并确认数据库状态。
| 方案 | 迁移总时长 | 线上锁风险 | 实施复杂度 | 更适合的场景 |
|---|---|---|---|---|
| 大批量直接执行 | 短 | 高 | 低 | 可停机或低频写入系统 |
| 小批次顺序执行 | 中长 | 中低 | 中 | 普通在线业务、历史数据回填 |
| 动态限速执行 | 不稳定 | 较低 | 中高 | 流量波动明显的核心系统 |
| 影子表或在线迁移 | 较长 | 较低但切换有风险 | 高 | 大型核心表、不能停写的系统 |
| 停写维护窗口 | 可控 | 低 | 中 | 一致性要求高且业务允许停写 |

统计目标数据库过去 7 至 14 天的核心接口 P95、P99、错误率、连接池使用率、最长事务、锁等待和复制延迟。不要只记录平均值,至少保留峰值和长尾数据。
同时列出所有计划任务、批处理、报表和数据同步链路,标注它们的启动时间、持续时间、访问对象和责任人。
把待迁移表按数据量、写入频率、热点程度、索引情况、是否涉及结构变更和是否需要停写进行分类。对每一类指定默认方案和升级条件。
在接近生产的数据规模下测试主键范围、时间范围和分页方式的差异。记录每种方式的扫描行数、实际影响行数、单批耗时和日志增长。
迁移程序至少要能安全暂停、从最后成功范围续跑、区分失败批次和成功批次,并输出结构化日志。不要等到生产阻塞后才发现脚本只能通过强制终止进程退出。
演练时主动模拟线上写入、后台报表、长事务和复制链路压力。重点不是让脚本跑完,而是观察达到什么条件时需要降低批次或暂停。
明确谁负责暂停迁移、谁负责判断业务影响、谁负责数据库处置、谁负责数据校验、谁负责对外沟通。终止连接、切换流量和回滚等高风险动作,应提前定义授权范围。
将迁移对象、开始条件、监控面板、暂停阈值、回滚路径、联系人和校验步骤压缩到一页纸。上线当天,执行人员不应在多个文档之间寻找关键动作。

一场迁移是否成功,不由“脚本执行完成”决定,而由业务是否保持可用、数据是否能够证明一致、异常是否能够及时止损共同决定。
如果只能做一项改进,先不要急着换工具,也不要先扩大数据库规格。先让迁移任务具备暂停、断点续跑、结构化进度、阻塞监控和明确停止条件。因为没有这些能力,再先进的迁移工具也可能在故障发生时变成一个无法解释、无法恢复的黑盒。
下一步可以从最近一次迁移或批量修复任务开始,复盘三个数字:单批最长耗时、线上 P99 峰值、异常后恢复所需时间。如果团队说不清这三个数字,说明当前问题还不是“锁等待参数没有调好”,而是迁移流程尚未被真正工程化。

我以前把一次历史订单修正任务安排在凌晨,以为低峰期就足够安全,结果凌晨仍有对账、风控和数据同步任务同时运行。迁移开始后,接口 P99 延迟从 180ms 升到 2.4s,真正让我意识到:低峰期只是时间选择,不是风险控制方案。技术负责人到底应该怎样设计一套可暂停、可观测、可回滚的迁移流程?
减少锁等待,关键不是把脚本放到凌晨执行,而是把迁移任务改造成一项受控的生产变更。我的建议是按“评估,演练,分批,监控,止损,校验”六个环节推进,每个环节都必须有明确的通过标准。迁移前先确认四件事:目标表的数据量、迁移条件是否命中索引、表上是否存在高频写入、当前是否有长事务。
尤其要检查迁移 SQL 的执行计划。一次全表扫描即使最终只修改少量记录,也可能长时间占用资源并放大锁竞争。
阶段必须确认的内容不通过时的动作 评估数据量、索引、长事务、业务峰值调整方案,不直接上线 演练耗时、锁等待、日志量、复制延迟重新压测或拆分任务 执行P99、错误率、连接池、阻塞链限速、暂停或回滚 完成数据一致性和业务稳定性延长观察窗口 执行脚本必须支持断点续跑,不能依赖一个持续数小时的大事务。
每个批次应记录起止主键、处理数量、提交时间和失败原因。这样发生阻塞时可以暂停后续批次,而不是只能等待整个任务结束或强制终止。止损阈值也要提前写进变更单。例如,P99 延迟连续 3 分钟超过基线两倍、最长锁等待超过 10 秒、连接池使用率超过 80%,就自动暂停迁移。
具体数值不能照搬,应该根据业务平时的基线和可接受损失设定。技术负责人真正需要推动的,不是某一条“优化 SQL”的技巧,而是让迁移具备可观测、可暂停、可恢复、可校验和可回滚五种能力。缺少其中任何一种,迁移就仍然是一次不可控的线上赌博。
我曾经把批处理大小固定成每批 1 万行,测试环境看起来很稳定,到了生产环境却因为行记录大小、索引数量和并发写入不同,单批耗时从 300ms 变成 18 秒。后来我发现,批次大小不能靠一个经验数字决定,而要根据单批持锁时间和业务压力动态调整。实际执行时应该怎么选切分方式?
分批的目的不是把一条大 SQL 简单切成很多条,而是缩短单次事务的持锁时间,并让任务可以随时停下来。优先使用主键或稳定的唯一索引做范围切分,例如按 id 大于上次游标且小于本次上限处理,避免使用大 OFFSET 分页。
OFFSET 分页在数据量增大后会反复扫描前面的记录,前几批可能很快,后几批却越来越慢。基于主键游标的方式更适合长时间迁移,因为每一批的定位成本相对稳定,也便于记录进度和断点恢复。
切分方式优点主要风险适用判断 固定行数实现简单不同批次耗时波动大数据分布均匀、负载稳定 主键范围易定位、易续跑主键分布不均时需调节有连续或可比较的键 时间范围符合归档和历史迁移逻辑热点日期可能过大数据按时间自然分布 OFFSET 分页写法直观后段扫描成本上升只适合小数据量任务 批次大小应通过生产近似环境测出来。
可以先从 500 或 1000 行开始,观察单批耗时、锁等待、日志增长和业务 P99;如果连续多个批次都低于目标耗时,再逐步增加。如果单批耗时突然翻倍,应该回退,而不是继续加量。我更关注“每批持锁多久”,而不是“每批处理多少行”。
同样是 5000 行,窄表、少索引可能只需几百毫秒,宽表、多个二级索引和触发器可能需要数秒。可以设置一个目标,例如单批事务尽量控制在 1 秒以内,再根据压测结果反推批次行数。批次之间可以增加极短的自适应间隔,但不要机械地固定休眠。
更合理的做法是:业务延迟和锁等待低于基线时提高速率,指标接近阈值时降低批次或暂停。分批只是降低影响范围,并不等于不会发生阻塞。
线上出现锁等待时,我最初也容易把刚启动的迁移任务当成唯一嫌疑对象,但有一次真正持锁 26 分钟的是一个异常未提交的报表事务,迁移只是把问题暴露出来。现在遇到阻塞,我不会先杀线程,而是先确认等待事务、阻塞事务、持锁对象和业务来源之间的关系。这个判断流程应该怎样做?
排查锁等待时,先画出阻塞链,而不是先看 CPU 或直接终止连接。最基本的关系是“谁在等待,谁在阻塞,阻塞发生在哪个对象,持锁事务为什么没有提交”。迁移任务可能是等待者,也可能是持锁者,不能仅凭任务名称下结论。建议同时采集事务开始时间、最后执行语句、连接来源、应用模块、锁对象和等待时长。
不同数据库的诊断视图和锁类型并不相同,不能把某一种数据库的查询命令直接套到其他产品上,但判断思路是通用的。
现象更可能的原因先做什么 迁移启动后等待数迅速增加迁移批次过大或索引不合适暂停迁移并检查执行计划 迁移未启动就已有长时间阻塞业务长事务或后台任务确认事务来源和持锁对象 只有特定表持续冲突热点写入、更新范围重叠调整批次边界和执行窗口 锁等待伴随复制延迟日志量过大或下游处理不足限速并观察日志堆积 有一次复盘中,迁移 SQL 本身只修改了约 3000 行,但它访问的表同时被一个报表事务持有锁。
报表事务没有及时提交,迁移一旦进入同一范围就排队,业务写入又被进一步拖慢。若当时直接终止迁移连接,表面上能缓解等待,却没有解决长事务的根因。是否终止阻塞连接,要看事务回滚量、业务重要性和重试副作用。一个已经执行了大量修改的长事务,被强制终止后可能需要长时间回滚,回滚期间仍然占用资源甚至继续影响业务。
因此正确顺序通常是先暂停迁移、保护核心请求、确认阻塞源,再决定等待、终止或改方案。技术负责人还应要求保留现场信息,包括阻塞链快照、事务开始时间、SQL 指纹、业务发布记录和监控曲线。没有这些记录,事后只能凭感觉判断,下一次迁移仍会重复踩坑。
我曾经以为“在线”就等于“无锁”,后来在表结构切换阶段遇到过元数据锁,任务在最后几秒卡住,反而影响了正常发布。现在我会先区分数据搬运、结构变更和业务切换,再决定是否使用在线工具、影子表或双写。技术负责人怎样在风险、复杂度和切换窗口之间做选择?
没有一种迁移方案可以脱离数据库版本、存储引擎、表规模和一致性要求单独判断。在线 DDL 通常能减少长时间的表级影响,但开始或结束阶段仍可能需要获取元数据锁;影子表和双写可以缩短最终切换时间,却会增加同步、校验和回滚复杂度。
方案主要优点主要代价更适合的场景 分批原表更新改造成本低、易暂停持续占用原表资源字段修正、历史数据处理 在线结构变更减少长时间阻塞仍可能受元数据锁影响兼容性明确的结构调整 影子表迁移可把大部分工作移出主表需要增量同步和最终切换大表、切换窗口很短 双写加灰度切换业务迁移更平滑应用和一致性治理复杂跨库、架构升级、长期演进 如果只是修正历史字段,通常优先选择带主键游标的分批更新,不必为了“高级方案”引入双写。
若是大表结构变更,且业务无法接受长时间停写,再考虑在线 DDL 或影子表。方案越复杂,真正的故障点往往越多,不能只比较理论上的锁时间。使用在线变更工具前,至少要验证四件事:目标版本是否支持该操作、触发器和外键是否兼容、增量同步是否会造成日志或延迟堆积、最终切换时是否存在长事务。
切换窗口前应主动清理或等待长事务,否则大部分工作完成后仍可能卡在最后一步。双写并不是天然更安全。它会把数据库锁风险转化为应用一致性风险,例如一边写入成功、另一边失败,或者重试导致重复写入。采用双写时必须有幂等键、失败补偿、对账校验和明确的停止双写条件。
我的判断标准是:先选择能满足一致性和窗口要求的最简单方案,再用压测数据证明它不会超过风险阈值。真正成熟的迁移不是追求工具最复杂,而是让每一步都知道如何暂停、如何验证、如何恢复。


读者评论
文章把锁等待和CPU利用率区分开来,这一点很实用。实际排查时,连接池占用率和阻塞链往往比资源曲线更能说明问题。
按主键范围分批、设置超时和暂停条件,比单纯安排在凌晨执行更稳妥。不过具体批次大小仍需结合索引、触发器和业务热点压测确定。
对“可回滚”的拆分比较准确。当前事务回滚、已提交进度回退和业务流量切回不是一回事,迁移方案确实需要分别设计和演练。