零停机数据库迁移:双写策略与校验回滚实战记录
数据库迁移这件事,最怕的不是数据量大,而是业务不能停。我们这次要把核心交易库从自建 MySQL 迁到云上 RDS,还要顺带做分库分表改造,留给停机的时间窗口几乎为零。于是“双写 + 校验 + 回滚”成了整个方案的三件套,听起来像标准答案,真正落地时每一步都有坑。
双写不是“两边都写”那么简单
一开始团队讨论双写,很多人第一反应是:业务代码里同时写旧库和新库,不就完了?但两个库不在同一个事务里,任何一边失败都会留下不一致。我们最终选了“旧库为主、新库异步”的模式:业务事务只保证旧库提交,提交后通过本地消息表发一条变更事件,由消费者写新库。这样旧库永远是兜底,新库写失败可以重试,不会阻塞主流程。
为了保证重试安全,所有写新库的操作都必须幂等。我们用业务唯一键 + 版本号做冲突控制,更新时带上旧值版本,避免异步延迟导致旧数据覆盖新数据。自增主键也换成了雪花 ID,否则新旧库的 ID 规则不一致,迁移后会非常难受。软删字段、状态字段、JSON 字段的默认值,全部提前在预发环境跑过 DDL 和回填。
读切换要灰度,别一把梭
双写稳定后,下一步是切读。我们没有直接全量切,而是按租户 ID 哈希做灰度:1%、5%、20%、50%、100%。配置中心里放一个读开关,每个阶段观察核心接口的错误率、P99 和慢查询。灰度期间还开了“双读比对”,同一请求同时读旧库和新库,把结果做 diff 后写日志。大部分差异是延迟导致,但确实抓到过几笔新库漏写的数据。
这里的关键是:读切换和写切换要分开。读可以先切,因为读新库不影响旧库写入;写切换则要等校验和回滚预案都就绪。
校验:别等切完才发现数据不对
数据校验我们分了两层。第一层是全量 checksum,按主键分片,每片算行数和关键字段摘要,新旧库对比。第二层是增量对账,消费 binlog 或双写消息,记录每张表的变更流水,再和两端实际数据比对。对于账户余额、订单金额这类关键表,直接逐笔对账,不接受抽样。
校验工具最怕影响线上。我们一开始把校验任务跑在主库上,结果大表 checksum 把 IO 打满。后来改成从库 + 低峰期 + 限流,才稳住。差异处理也提前分了类:延迟差异等重试,丢失差异用补偿工具重放,覆盖差异则要人工确认。所有补偿操作都留审计日志,方便回滚。
回滚要有一键路径
回滚不是“把新库删了”这么简单。我们保留了旧库的完整写入能力,双写期间旧库始终是主。回滚动作只有三步:关掉新库读开关,停掉新库写入消费者,把流量切回旧库。因为旧库数据一直是最新的,回滚窗口可以控制在分钟级。
但回滚也有代价:停写新库后,新库会落后,重新开启双写需要先做增量追平。所以每次灰度切写前,我们都会确认回滚窗口和追平耗时。监控看板盯四个指标:双写失败率、消费延迟、校验差异数、新旧库读耗时差。任何一项超阈值,自动告警并暂停切流。
实战踩过的坑
第一个坑是唯一键冲突:旧库软删的数据在新库是硬删,重新插入时撞了唯一索引,导致双写失败。第二个坑是时间精度,旧库 DATETIME 和新库 TIMESTAMP 在时区处理上不一致,对账时差了几秒。第三个坑最危险:有个批量任务绕过 DAL 直接更新旧库,漏发了双写消息,导致新库少了三千多行。后来所有写入口强制收口到统一数据访问层,才堵住这个口子。
零停机迁移没有银弹,双写、校验、回滚每一步都要当作线上功能来建设。先保旧库,再切读,再切写,最后下线旧库;监控、演练、补偿工具一个都不能少。迁移完成后回头看,真正省时间的不是写代码,而是把不一致场景提前想清楚。
转载请注明出处,版权归原作者所有。
管理员



