如何实现数据库的不停服迁移?从双写到灰度切换的完整方案
考点分析迁移全流程是否清楚「全量同步 → 增量同步 → 双写 → 灰度切换 → 下线旧库」的完整闭环而不是只停留在“导数据”。不停服的实现手段能否说清“零停机”靠的是双写、CDC 增量同步、读写分离路由等手段。数据一致性保障全量/增量数据如何校验双写不一致如何处理最终一致还是强一致。灰度切换与回滚如何按比例切流验证切换失败时如何无损回滚旧库什么时候才能安全下线。工程容错能力断点续传、同步延迟、写入失败降级、数据补偿等真实的线上细节。1. 标准回答总结数据库不停服迁移的核心思想是“历史数据全量同步 在线变更增量同步 新旧库双写 校验一致后灰度切流 保留旧库可回滚”。通过引入同步链路与路由层让业务在迁移过程中持续对外提供服务最终无损切换到新库。作用在业务不中断的前提下完成数据库的版本升级、引擎替换、机房搬迁、分库分表或上云迁移避免停机窗口带来的资损和用户不可用。特点平滑对业务透明不会出现长时间不可用。可回滚旧库在切换后保留观察期出问题可快速切回。可验证通过一致性校验和灰度流量提前暴露问题。代价可控相比停机迁移开发与运维成本更高需要同步、路由、监控等多层配合。2. 核心原理不停服迁移本质上要把「存量数据搬过去」和「增量数据追平」两件事同时解决。2.1 全量同步先对旧库做一致性快照把历史数据批量写入新库。常用方式基于主键或时间戳分片导出、分批写入减小单批次压力。记录同步开始时的位点binlog 位点 / LSN / 时间戳作为后续增量同步的起点。支持断点续传以主键区间为单位记录进度失败后从断点继续。2.2 增量同步全量同步期间旧库还会持续产生新数据因此需要增量链路把变更追平。常用手段是CDCChange Data CaptureMySQL 订阅binlogPg 使用逻辑复制槽Oracle 使用LogMiner / OGG。通过 Canal、Debezium、Flink CDC 等工具消费变更转换为新库的 SQL。同步延迟是核心指标切换前要保证延迟趋近于 0。2.3 双写保障单靠同步链路存在延迟和丢失风险因此在业务写路径上开启双写一次事务内同时写新库与旧库或在旧库写成功后异步补写新库保证关键变更不会漏。2.4 一致性校验切流前必须校验新旧库数据一致数量校验比对总行数、分片行数。内容校验对主键做CRC32 / MD5 抽样对比。持续校验校验任务周期运行直到差异收敛到可接受范围。2.5 灰度切换与回滚校验通过后通过配置中心或路由层按用户、比例或白名单逐步把读流量切到新库。观察业务指标与错误率逐步放大比例至 100%。旧库保留观察期出现数据异常时把流量切回旧库实现回滚。整体流程如下否是旧库全量同步新库增量同步(CDC)业务写入双写一致性校验校验通过?灰度切流观察期下线旧库3. 应用场景3.1 日常开发场景场景说明大表结构变更老版本 MySQL 直接改表会锁表通过新表 同步 切换规避锁表单库拆分为多库按业务域拆分减少单库压力索引优化/重建避免在线 DDL 对业务的影响数据归档把冷数据迁到归档库减轻主库负载3.2 企业真实场景MySQL 同构迁移版本升级、参数变更、硬件换代。MySQL 迁移到 TiDB / PolarDB业务增长后需要分布式能力。Oracle 迁移 MySQL/PostgreSQL去 IOE、上云改造。机房搬迁 / 单元化改造按地域拆分流量数据同步到新机房。上云迁移IDC 自建库迁到 RDS使用云厂商 DTS 完成。4. 使用方式Java 示例下面通过 Java 展示双写与一致性校验的核心实现思路示例代码为演示核心逻辑生产环境需结合事务、监控与配置中心。4.1 双写路由层publicinterfaceUserDao{intsave(Useruser);UserfindById(Longid);}publicclassUserDaoImplimplementsUserDao{privatefinalJdbcTemplateoldDb;privatefinalJdbcTemplatenewDb;privatefinalMigrationSwitchmigrationSwitch;publicUserDaoImpl(JdbcTemplateoldDb,JdbcTemplatenewDb,MigrationSwitchmigrationSwitch){this.oldDboldDb;this.newDbnewDb;this.migrationSwitchmigrationSwitch;}Overridepublicintsave(Useruser){// 1. 旧库始终写入保证主链路稳定intresultoldDb.update(INSERT INTO t_user(id, name, email) VALUES(?,?,?),user.getId(),user.getName(),user.getEmail());// 2. 灰度开关开启后才双写新库if(migrationSwitch.isDoubleWriteEnabled()){try{newDb.update(INSERT INTO t_user(id, name, email) VALUES(?,?,?) ON DUPLICATE KEY UPDATE name?, email?,user.getId(),user.getName(),user.getEmail(),user.getName(),user.getEmail());}catch(DataAccessExceptione){// 3. 新库写失败不能影响主流程落补偿日志异步重试compensationLog.record(user,e);}}returnresult;}OverridepublicUserfindById(Longid){// 读流量按灰度比例路由if(migrationSwitch.shouldReadFromNewDb(id)){ListUserusersnewDb.query(SELECT * FROM t_user WHERE id ?,(rs,rowNum)-mapUser(rs),id);if(!users.isEmpty()){returnusers.get(0);}}returnoldDb.queryForObject(SELECT * FROM t_user WHERE id ?,(rs,rowNum)-mapUser(rs),id);}}4.2 一致性校验publicclassConsistencyChecker{privatefinalJdbcTemplateoldDb;privatefinalJdbcTemplatenewDb;publicConsistencyChecker(JdbcTemplateoldDb,JdbcTemplatenewDb){this.oldDboldDb;this.newDbnewDb;}/** * 按主键区间分批比对返回不一致的记录主键列表 */publicListLongcheckRange(longstartId,longendId){ListLonginconsistentIdsnewArrayList();ListUseroldListqueryByRange(oldDb,startId,endId);ListUsernewListqueryByRange(newDb,startId,endId);MapLong,LongnewChecksumMapnewList.stream().collect(Collectors.toMap(User::getId,user-crc32(user)));for(UseroldUser:oldList){LongnewChecksumnewChecksumMap.get(oldUser.getId());if(newChecksumnull||newChecksum!crc32(oldUser)){inconsistentIds.add(oldUser.getId());}}returninconsistentIds;}privateListUserqueryByRange(JdbcTemplatedb,longstartId,longendId){returndb.query(SELECT id, name, email FROM t_user WHERE id BETWEEN ? AND ?,(rs,rowNum)-newUser(rs.getLong(id),rs.getString(name),rs.getString(email)),startId,endId);}privatelongcrc32(Useruser){Stringvalueuser.getId()|user.getName()|user.getEmail();returnjava.util.zip.CRC32.class.cast(newjava.util.zip.CRC32()).hashCode()0?0L:computeCrc(value);}privatelongcomputeCrc(Stringvalue){java.util.zip.CRC32crc32newjava.util.zip.CRC32();crc32.update(value.getBytes(StandardCharsets.UTF_8));returncrc32.getValue();}}4.3 执行流程与注意事项执行流程部署同步链路完成全量同步并记录增量位点。开启增量同步待延迟稳定在秒级。开启双写持续做一致性校验。校验一致后灰度切换读流量观察业务指标。灰度比例逐级扩大至 100%保留旧库观察 715 天。确认无异常后下线旧库与同步链路。注意事项双写失败不能阻断主库写入新库异常必须降级为只写旧库并通过补偿任务追数据。同步延迟要监控切换前确保延迟接近 0否则会读到旧数据。唯一键冲突新库写入使用INSERT ... ON DUPLICATE KEY UPDATE等幂等方式避免重复插入报错。大事务/大字段全量同步分批执行避免长事务与新库压力过大。回滚预案切流后必须保留旧库可写能力一旦新库出现严重问题可瞬间切回。校验要持续切换前、切换后都要持续校验灰度期发现问题及时修复。5. 扩展延伸5.1 技术方案对比方案原理优点缺点适用场景停机迁移停止服务后导出导入简单、一致性强业务中断低频、可接受停机的内部系统双写迁移业务层同时写新旧库实时性好、可控性强需改造代码、双写不一致风险核心业务、自定义要求高基于 binlog 同步订阅日志回放对业务侵入小有延迟、DDL 同步受限同构/兼容数据库迁移云厂商 DTS托管迁移服务稳定省心、支持异构可能产生费用上云、跨厂商迁移5.2 优缺点分析优点业务全程可用用户体验无感知。灰度切换可提前暴露性能、兼容性问题。保留旧库观察期风险可控。缺点系统复杂度显著上升需要同步链路、路由层、监控告警等多套设施。双写可能引入数据不一致需要补偿与校验兜底。迁移周期变长团队需要有较强的工程与运维能力。5.3 实际开发注意事项DDL 兼容性异构迁移要提前评估新库对 SQL、索引、字符集、事务隔离级别的兼容性。自增主键与序列双写时注意 ID 生成策略避免主键冲突建议沿用统一发号器。时区与字符集迁移前统一utf8mb4与Asia/Shanghai避免隐性数据差异。历史大表按时间或主键分片避开业务高峰执行全量同步。压测与演练切换前做一次全链路压测和回滚演练验证预案有效性。文档与 SOP明确每个阶段的操作手册、负责人和回滚命令避免慌乱。6. 面试追问追问 1全量同步期间的新数据怎么保证不丢回答思路强调“快照 位点 增量同步”。标准答案全量同步开始前先记录数据库的增量位点例如 MySQL 的 binlog 位点或 Pg 的逻辑复制槽。全量同步完成后从记录的位点开始做增量同步把期间产生的变更追平。在切换前持续观察延迟确保增量同步追平后再切流就不会丢数据。追问 2双写时新库写失败怎么办会不会拖垮主流程回答思路突出“主库优先 降级 补偿”。标准答案新库写失败绝不能让主流程失败要捕获异常并降级为“只写旧库”同时记录一条补偿日志。后台补偿任务根据日志重试写入新库直到成功。这样既保证业务稳定又能最终追平数据。追问 3如何验证新旧库数据真正一致回答思路从“数量 内容 持续校验”三层说明。标准答案首先比对总行数和分片行数然后按主键区间抽样对比内容校验值比如 CRC32 或 MD5。考虑到变更持续发生校验任务要周期性运行直到差异数量收敛到 0 或可忽略范围。对发现差异的记录定位到具体主键进行修复。追问 4灰度切换后发现新库读性能变差怎么处理回答思路先回滚保稳定再定位优化。标准答案第一步是立即把读流量切回旧库保证业务不受影响这也体现了保留观察期和回滚能力的重要性。然后分析是索引缺失、SQL 不兼容还是新库资源配置不足针对性地补充索引、优化 SQL 或扩容。修复并重新校验后再重新灰度放量。追问 5旧库什么时候才能安全下线回答思路强调“观察期 数据追平 业务指标 应急预案”。标准答案必须满足几个条件灰度已 100% 切到新库且业务指标稳定增量同步延迟持续为 0校验无差异观察期如 715 天内无异常。下线旧库前还要保留可回滚的备份或快照确保极端情况下仍有兜底手段。追问 6迁移过程中业务代码需要做哪些改造回答思路围绕“路由层隔离 双写开关 灰度策略”。标准答案业务代码尽量通过数据访问层或中间件统一改造不散落在各处。核心改造包括写路径增加双写开关读路径增加灰度路由策略同时接入配置中心让开关和比例可以动态调整。这样切换、回滚都能通过配置完成避免发版和重新部署。