MySQL单表有10亿数据如何做迁移

📅 2026/7/23 2:30:03
MySQL单表有10亿数据如何做迁移
整体思路是先全量后增量双写兜底灰度切换可回滚。迁移核心三原则面试官最看重的底线✅ 数据零丢失所有变更必须可追溯最终一致性保证✅ 最小停机时间业务无感知或秒级停机✅ 可快速回滚任何阶段出问题都能 1 分钟切回旧库整体迁移流程图image分阶段详细执行核心技术点3.1 迁移前准备与评估决定成败的阶段 ⚠️1.数据体检统计行数、平均行长度、大字段 (TEXT/BLOB) 占比、索引总大小分析热点数据分布、主键连续性、是否有唯一键冲突风险2.目标库预配置硬件CPU / 内存 / 磁盘 IO 不低于源库预留 1.5 倍源库空间参数临时调优innodb_flush_log_at_trx_commit2、sync_binlog0关键操作预建表结构只建主键索引全量完成后再建二级索引速度提升 5-10 倍3.监控与预案监控大盘迁移速度、延迟、QPS、磁盘使用率、连接数回滚方案保留旧库 7 天数据双写机制兜底3.2 迁移中全量 增量双阶段执行全量迁移工具选型对比表工具 适用场景 优点 缺点 10 亿数据推荐指数mysqldump 小表 (1000 万) 简单易用 锁表、单线程、极慢 ⭐xtrabackup 同版本同引擎物理迁移 速度快、不锁表 不支持异构、不能过滤数据 ⭐⭐⭐DataX 异构数据源全量迁移 多线程、支持过滤、可切分任务 无增量能力 ⭐⭐⭐⭐⭐Canal MySQL 增量同步 阿里开源、成熟稳定、低延迟 仅支持 MySQL ⭐⭐⭐⭐⭐Debezium 多数据源 CDC 生态好、支持 Kafka 部署复杂 ⭐⭐⭐⭐推荐方案用 DataX 按主键范围切分 100-200 个并行任务做全量迁移速度可达 10 万行 / 秒以上10 亿数据约 2-3 小时完成。增量同步与双写基于Binlog ROW 格式做 CDC必须提前确认 binlog_formatROW用 Canal 监听源库 Binlog实时同步到目标库延迟控制在 1 秒内全量完成后开启业务双写先写旧库再异步写新库失败重试 3 次保证最终一致3.3 迁移后验证与灰度切换数据一致性校验三层校验粗校验SELECT TABLE_ROWS FROM information_schema.TABLES对比行数细校验用pt-table-checksum批量校验或按主键范围抽样 1% 数据逐行对比业务校验核心接口跑回归测试验证读写逻辑正确性灰度流量切换第 1 步切 1% 流量到新库观察 1 小时第 2 步切 10% 流量观察 4 小时第 3 步切 50% 流量观察 1 天第 4 步切 100% 流量稳定运行 3 天最终下线确认无问题后关闭双写下线旧库高频踩坑与优化技巧 踩坑点 后果 解决方案提前建二级索引 全量迁移速度慢 10 倍以上 全量完成后用ALTER TABLE ALGORITHMINPLACE批量建索引Binlog 格式为 STATEMENT 增量同步数据不一致 提前修改binlog_formatROW重启 MySQL 生效大字段导致单条记录过大 迁移速度骤降、OOM 大字段单独拆表或迁移时过滤非必要历史数据主键不连续导致数据倾斜 部分 DataX 任务跑不完 手动指定主键切分范围均匀分配任务目标库磁盘空间不足 迁移到一半失败 提前计算空间源库数据量 ×1.5含索引加分项延伸思考如果迁移同时需要分库分表可以在 DataX 全量迁移时直接按分片规则写入目标分表增量同步时也按分片规则路由如果允许凌晨停机可以用 xtrabackup 物理备份恢复停机时间 备份时间 恢复时间约 1-2 小时云环境优化优先使用云厂商 DTS 工具阿里云 DTS、腾讯云 CDB 迁移支持自动全量 增量 切换省心省力核心代码实现带技术亮点标注6.1 DataX 全量迁移配置按主键切分 多线程并行技术亮点主键范围手动切分解决数据倾斜、多通道并行提速、只迁移必要字段过滤大字段{“job”: {“setting”: {“speed”: {“channel”: 16, // 按CPU核心数设置16核16通道10万行/秒“byte”: 104857600 // 单通道每秒100MB},“errorLimit”: {“record”: 0, // 零错误容忍有一条失败就终止“percentage”: 0.02}},“content”: [{“reader”: {“name”: “mysqlreader”,“parameter”: {“username”: “root”,“password”: “xxx”,“column”: [“id”, “user_id”, “order_no”, “amount”, “create_time”], // 过滤TEXT/BLOB大字段“connection”: [{“jdbcUrl”: [“jdbc:mysql://old-db:3306/order_db?useSSLfalse”],// 手动切分主键范围解决数据倾斜核心优化“querySql”: [“SELECT id,user_id,order_no,amount,create_time FROM t_order WHERE id 1 AND id 50000000”,“SELECT id,user_id,order_no,amount,create_time FROM t_order WHERE id 50000000 AND id 100000000”,// … 共切分200个范围每个范围500万行]}]}},“writer”: {“name”: “mysqlwriter”,“parameter”: {“username”: “root”,“password”: “xxx”,“column”: [“id”, “user_id”, “order_no”, “amount”, “create_time”],“connection”: [{“jdbcUrl”: “jdbc:mysql://new-db:3306/order_db?useSSLfalserewriteBatchedStatementstrue”,“table”: [“t_order”]}],“batchSize”: 10000, // 批量提交1万行速度提升3倍“preSql”: [“SET FOREIGN_KEY_CHECKS0”] // 临时关闭外键约束}}}]}}6.2 Canal 增量同步客户端批量处理 幂等性保证技术亮点批量消费 Binlog、INSERT … ON DUPLICATE KEY UPDATE 幂等、异常重试机制Componentpublic class CanalIncrementSyncService {Autowiredprivate JdbcTemplate jdbcTemplate;// 批量处理大小平衡延迟与吞吐量 private static final int BATCH_SIZE 5000; // 幂等性SQL模板核心避免重复消费导致数据错误 private static final String UPSERT_SQL INSERT INTO t_order (id,user_id,order_no,amount,create_time,update_time) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE user_idVALUES(user_id), order_noVALUES(order_no), amountVALUES(amount), update_timeVALUES(update_time); PostConstruct public void startCanalConsumer() { // 连接Canal Server CanalConnector connector CanalConnectors.newSingleConnector( new InetSocketAddress(canal-server, 11111), example, , ); connector.connect(); connector.subscribe(order_db.t_order); // 只订阅目标表 while (true) { Message message connector.getWithoutAck(10000); // 批量获取1万条 long batchId message.getId(); ListCanalEntry.Entry entries message.getEntries(); if (batchId -1 || entries.isEmpty()) { try { Thread.sleep(100); } catch (InterruptedException e) {} continue; } try { processEntries(entries); connector.ack(batchId); // 处理成功才ACK } catch (Exception e) { log.error(增量同步失败batchId:{}, batchId, e); connector.rollback(batchId); // 失败回滚下次重新消费 } } } private void processEntries(ListCanalEntry.Entry entries) { ListObject[] batchArgs new ArrayList(BATCH_SIZE); for (CanalEntry.Entry entry : entries) { if (entry.getEntryType() CanalEntry.EntryType.ROWDATA) { CanalEntry.RowChange rowChange CanalEntry.RowChange.parseFrom(entry.getStoreValue()); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { // 处理INSERT/UPDATE/DELETE统一用UPSERT保证幂等 ListCanalEntry.Column columns rowData.getAfterColumnsList(); Object[] args new Object[6]; args[0] Long.parseLong(getColumnValue(columns, id)); args[1] Long.parseLong(getColumnValue(columns, user_id)); args[2] getColumnValue(columns, order_no); args[3] new BigDecimal(getColumnValue(columns, amount)); args[4] Timestamp.valueOf(getColumnValue(columns, create_time)); args[5] Timestamp.valueOf(getColumnValue(columns, update_time)); batchArgs.add(args); // 批量提交 if (batchArgs.size() BATCH_SIZE) { jdbcTemplate.batchUpdate(UPSERT_SQL, batchArgs); batchArgs.clear(); } } } } // 提交剩余数据 if (!batchArgs.isEmpty()) { jdbcTemplate.batchUpdate(UPSERT_SQL, batchArgs); } } private String getColumnValue(ListCanalEntry.Column columns, String columnName) { for (CanalEntry.Column column : columns) { if (column.getName().equals(columnName)) { return column.getValue(); } } return null; }}6.3 业务双写实现异步 重试 兜底技术亮点异步线程池隔离、失败重试 3 次、Canal 兜底最终一致Servicepublic class OrderService {Autowiredprivate OrderMapper oldOrderMapper;Autowiredprivate OrderMapper newOrderMapper;// 独立线程池隔离双写逻辑不影响主业务Qualifier(“doubleWriteExecutor”)private ThreadPoolTaskExecutor doubleWriteExecutor;Transactional(rollbackFor Exception.class) public void createOrder(Order order) { // 1. 先写旧库主库事务保证 oldOrderMapper.insert(order); // 2. 异步写新库失败重试3次 doubleWriteExecutor.execute(() - { try { newOrderMapper.insert(order); } catch (Exception e) { log.error(双写新库失败orderId:{}, order.getId(), e); // 重试3次仍失败则记录日志由Canal兜底同步 RetryTemplate.builder() .maxAttempts(3) .backoffOptions(new FixedBackOffOptions(1000, 2)) .build() .execute(context - newOrderMapper.insert(order)); } }); }}6.4 数据一致性校验代码MD5 抽样对比技术亮点按主键范围分页抽样、MD5 行级对比、高效发现不一致Servicepublic class DataConsistencyChecker {Autowiredprivate JdbcTemplate oldJdbcTemplate;Autowiredprivate JdbcTemplate newJdbcTemplate;// 抽样校验1%数据按主键范围分1000批 public boolean checkConsistency(long startId, long endId) { long batchSize (endId - startId) / 1000; AtomicBoolean isConsistent new AtomicBoolean(true); IntStream.range(0, 1000).parallel().forEach(i - { long batchStart startId i * batchSize; long batchEnd (i 999) ? endId : batchStart batchSize; // 计算源库和目标库该范围的MD5总和 String oldMd5 calculateMd5Sum(oldJdbcTemplate, batchStart, batchEnd); String newMd5 calculateMd5Sum(newJdbcTemplate, batchStart, batchEnd); if (!oldMd5.equals(newMd5)) { log.error(数据不一致范围:[{},{}], batchStart, batchEnd); isConsistent.set(false); // 进一步逐行对比找出具体不一致的行 findInconsistentRows(batchStart, batchEnd); } }); return isConsistent.get(); } private String calculateMd5Sum(JdbcTemplate jdbcTemplate, long startId, long endId) { String sql SELECT MD5(CONCAT(id,user_id,order_no,amount,create_time)) AS row_md5 FROM t_order WHERE id ? AND id ?; ListString md5List jdbcTemplate.queryForList(sql, String.class, startId, endId); return DigestUtils.md5Hex(String.join(, md5List)); }}核心技术难点与解决方案面试官必问技术难点 问题本质 解决方案 优化效果⚠️ 10 亿数据全量迁移速度慢 数据倾斜 主键分布不均匀如 UUID、自增断档导致部分任务数据量过大单条插入效率低 1. 手动按主键范围切分任务保证每个任务 500 万行2. 开启 rewriteBatchedStatements 批量提交3. 全量迁移时只建主键索引二级索引后建 迁移速度从 1 万行 / 秒提升至 10-15 万行 / 秒10 亿数据 2-3 小时完成⚠️ 增量同步延迟过高秒级以上 单条消费 Binlog目标库写入压力大Canal 参数不合理 1. 批量消费 批量提交5000 行 / 批2. 目标库临时调优innodb_flush_log_at_trx_commit23. 调整 Canal 参数canal.instance.batchSize10000 延迟稳定控制在100ms 以内业务无感知⚠️ 双写一致性与性能平衡 同步双写会增加接口响应时间异步双写可能丢失数据 1. 异步双写 独立线程池隔离2. 失败重试 3 次 日志记录3. Canal 增量同步作为兜底保证最终一致 接口响应时间增加 1ms数据一致性 100%⚠️ 大字段 (TEXT/BLOB) 迁移瓶颈 单条记录过大导致网络 IO 和磁盘 IO 瓶颈内存溢出 1. 迁移时过滤非必要大字段2. 大字段单独拆表单独迁移3. 开启 MySQL 压缩协议 迁移速度提升 5-10 倍避免 OOM⚠️ 迁移过程中源库性能影响 全量迁移会占用源库大量 CPU 和 IO影响线上业务 1. 迁移时间选在业务低峰期凌晨 2-6 点2. 限制 DataX 单通道速度100MB / 秒3. 源库开启只读账号避免写操作 源库 CPU 使用率 30%业务 QPS 无明显下降⚠️ 灰度切换时的流量控制与回滚 流量突增导致新库雪崩新库有问题无法快速回滚 1. 按比例灰度切换1%→10%→50%→100%2. 网关层配置流量路由支持一键切回3. 保留旧库 7 天数据双写机制持续到切换完成 回滚时间 1 秒业务无感知⚠️ 分库分表场景下的路由一致性 迁移同时需要分库分表数据路由错误导致数据丢失 1. DataX 全量迁移时直接按分片规则写入目标分表2. Canal 增量同步时也按相同分片规则路由3. 校验时按分片维度分别校验 分库分表迁移零数据丢失路由准确率 100%面试加分金句一句话体现实战经验“我之前在做电商订单表 12 亿数据迁移时就是用这套方案最终实现了零停机、零数据丢失整个迁移过程业务完全无感知唯一的影响是凌晨低峰期源库 CPU 短暂升到 28%。”真实现场模拟面试真实面试对话场景面试官 ‍“咱们有个 MySQL 单表线上数据马上就要到 10 亿了现在打算把它迁到一个新实例或者换成新表结构。要求不能长时间停服也不能把线上业务拖垮。如果是你你会怎么设计这个迁移方案”候选人 “10 亿级别的大表迁移最忌讳的就是想‘一把梭’绝对不能直接 mysqldump 然后锁表导。我的总体思路就是九个字化整为零、在线无锁、全量增量。核心原则就三条利用自增主键或时间字段把大表切成无数小段先搬全量再通过 binlog 增量追赶实时变化全程不加锁、不产生大事务绝不让线上业务感知到。”面试官 “嗯思路很清晰。那你按步骤详细说说第一步会做什么”候选人 ‍“第一步当然是评估 准备。先摸清表的‘脾气’有没有自增 ID有没有时间索引确认好迁移窗口和回滚方案。然后在新库里把表结构建好字符集、引擎这些必须和源库一致。而且这是做表分区的绝佳机会比如直接在新表上按时间建分区一步到位。最关键一点源表必须有能索引的切分条件ID 最好create_time 也可以否则得想办法加一列。”面试官 “好那具体怎么搬全量数据10 亿行啊总不能一条条 select 吧”候选人 “全量迁移我绝对不会手写脚本直接用 Percona Toolkit 里的 pt-archiver这个工具专干在线归档。命令大概长这样pt-archiver–source hold_host,Ddb,thuge_table–dest hnew_host,Ddb,thuge_table–where “id 1 AND id 50000000”–limit 1000 --txn-size 1000 --no-delete–progress 50000按 ID 范围 多进程并行 跑每个进程认领一个区间比如 5000 万一段–txn-size 控制事务大小避免长事务把 undo log 撑爆或者主从延迟飞涨。原理是 WHERE id BETWEEN … 走主键索引RC/RR 隔离级别下都是一致性快照读几乎不锁表。我还会同时画一个流程图方便理解image面试官 “这个图很直观。那全量跑完中间产生的增量数据怎么追”候选人 “全量完成后我会立刻在源库执行 SHOW MASTER STATUS;记下 File 和 Position这就是增量同步的起点。然后有三种主流方式追增量云厂商 DTS最简单设置源和目标指定起始位点自动完成全量增量自建 Canal伪装成 MySQL 从库拉取 binlog自己写 client 写入新库临时从库法把新库配成源库的 MySQL 从库CHANGE MASTER TO 从记录的位点开始追START SLAVE。等增量延迟稳定到毫秒级才敢谈下一步。”面试官 ⚠️“那如果这张表没有自增 ID也没有时间字段呢你怎么办”候选人 ️“这确实是个大坑。如果没有自增 ID 也没时间索引我会分情况业务允许加字段那就立刻加一个自增 ID 或者 modified_time 索引利用低峰期 Online DDL 工具gh-ost 或 pt-osc不锁表完成实在加不了只能靠 LIMIT offset, size但深分页会越来越慢10 亿数据根本走不通。这时候就得跟业务方争取一个稍长的停机窗口用 SELECT INTO OUTFILE LOAD DATA 的方式暴力迁但那不是‘在线’方案了。所以我在前期评估时就会逼着大家想办法造出合适的切分键不然在线平滑迁移没戏。”面试官 “数据搬过去了你怎么确认新旧库数据完全一致”候选人 ✅“不能靠感觉必须上工具首选 pt-table-checksum在主库上生成校验值再在新库从库上对比连行级差异都能抓到或者按分片对比 CRC32 / COUNT业务侧再抽验核心用户或订单三重保险。只有校验全部通过我心里才敢想切换。”面试官 “最后切换环节怎么尽可能平滑”候选人 “所有准备就绪后选一个凌晨低峰期先短暂暂停源库写入挂一个维护状态等增量完全追平延迟归零修改应用配置或数据库中间件把流量切到新库验证核心接口没问题后旧表保留 N 天作为回滚备份同步链路也可以保留一段时间万一出问题能快速切回。这整个过程写操作暂停可能只有几秒钟基本无感。”面试官 “很好整体方案很完整。最后用一句话总结下”候选人 “一句话全量分批 binlog 增量 数据校验 秒级停写切换。工具有 pt-archiver、pt-table-checksum配合 DTS/Canal只要抓住有索引的切分键10 亿数据在线迁移也能稳如老狗。”面试官 ‍“前面方案聊得挺细了。你能不能展示一下实际会用到的核心代码片段比如你刚才说的并行分批导出、增量同步、数据校验有什么技术亮点另外再帮我总结一下这个场景的技术难点以及你的解决方案。”候选人 “好的我直接把代码思路和亮点亮出来然后给您梳理一下难点矩阵。” 1. 核心代码 技术亮点 亮点1Java 多线程并行分批迁移无锁断点续传下面是一个简化但生产可用的迁移 Worker亮点在于✅ 分片并发按 ID 范围切段线程池控制并发度✅ 断点续传记录每个分片的最大 ID重启不丢进度✅ 流控保护LIMIT 控制单次查询大小BETWEEN 走索引✅ 批量写入rewriteBatchedStatementstrue减少网络往返// 分段任务class SegmentTask {long startId;long endId;// getter/setter…}// 迁移主流程伪代码ExecutorService pool Executors.newFixedThreadPool(parallelism);CompletionService completion new ExecutorCompletionService(pool);// 分段每 50 万一个区间long batchSize 500_000;for (long start 1; start maxId; start batchSize) {long end Math.min(start batchSize - 1, maxId);SegmentTask task new SegmentTask(start, end);completion.submit(() - {migrateSegment(task);return null;});}// 收集结果处理异常重试for (int i 0; i segments; i) {Future future completion.take();future.get(); // 检查异常可重试}pool.shutdown();// 核心迁移方法一次处理一个 ID 范围void migrateSegment(SegmentTask task) {long lastId task.startId;while (lastId task.endId) {// 从源库流式读取游标查询避免内存溢出List batch sourceRepo.findByRange(lastId,Math.min(lastId LIMIT, task.endId));if (batch.isEmpty()) break;// 批量写入目标库开启 rewriteBatchedStatements targetRepo.batchInsert(batch); lastId batch.get(batch.size() - 1).getId(); // 记录进度到 Redis/文件实现断点续传 progressRecorder.update(task.startId, lastId); }} 技术亮点findByRange 使用 SELECT … WHERE id ? AND id ? ORDER BY id ASC利用主键索引一次网络往返只拉 1000 行避免客户端 OOM。每个分片记录进度重启时从 progressRecorder 取出最后的 lastId 继续10 亿数据迁移不怕中断。通过控制 parallelism 和 LIMIT精确调整对源库的负载压力。 亮点2基于 Binlog 的增量同步Canal 客户端示例Canal 可以伪装成 MySQL 从库实时捞取 binlog我们只关心需要的表和事件类型。// Canal 客户端核心处理逻辑简化版EventHandlerpublic void onEvent(CanalEntry.Entry entry) {if (entry.getEntryType() ! EntryType.ROWDATA) return;RowChange rowChange RowChange.parseFrom(entry.getStoreValue()); if (rowChange.getIsDdl()) return; // 忽略DDL for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { switch (rowChange.getEventType()) { case INSERT: ListColumn afterColumns rowData.getAfterColumnsList(); targetRepo.insert(buildEntity(afterColumns)); break; case UPDATE: // 按主键更新 targetRepo.updateByPk(buildEntity(rowData.getAfterColumnsList())); break; case DELETE: // 按主键删除 targetRepo.deleteByPk(getPkValue(rowData.getBeforeColumnsList())); break; } }} 技术亮点顺序消费单线程处理保证与源库执行顺序一致避免乱序导致数据不一致。幂等设计INSERT 使用 INSERT … ON DUPLICATE KEY UPDATE即使重复消费也不出错。位点持久化处理完一个 batch 后将 binlog 位点写入 Redis/DB重启从上次位点继续增量不丢不重。 亮点3数据一致性快速校验CRC32 分片比对避免全表 count(*)采用分片 CRC32 对比高效且准确。– 在源库和新库同时执行分片比较 CRC32 值SELECT(id DIV 100000) AS chunk, – 每 10 万行一个块COUNT(*) AS cnt,CRC32(GROUP_CONCAT(CRC32(CONCAT_WS(‘#’, id, col1, col2, update_time)))) AS checksumFROM huge_tableWHERE id BETWEEN 1 AND 10000000GROUP BY chunkORDER BY chunk; 技术亮点利用 CONCAT_WS 和嵌套 CRC32 对整个块的行生成数字指纹分片对比秒级发现差异。对比双方结果集checksum 不同则缩小分片范围进一步定位比 pt-table-checksum 更轻量且不依赖 Percona 工具集。 2. 技术难点 vs 解决方案矩阵我用一个图表把核心痛点和应对策略串起来更直观。image难点逐条拆解 技术难点 具体问题 解决方案 使用工具 / 技术点海量数据迁移性能 单线程、深分页会导致源库压力大且越跑越慢 按索引分段多线程并行BETWEEN 走主键游标式读取 pt-archiver、自定义Java线程池、LIMIT 控制无合适切分键 表没有自增ID或时间索引 评估阶段必须加字段利用 gh-ost 等在线DDL工具实在不行则与业务协商停机窗口 gh-ost、pt-osc增量同步延迟 全量增量期间业务写入量大新库一直追不平 多线程并行消费binlog合并微小事务控制目标库写入速度必要时暂停非核心业务写入 Canal、DTS、临时从库数据一致性校验 10亿行数据逐行比对不现实 分块计算CRC32指纹对比或使用 pt-table-checksum 自定义CRC32脚本、pt-table-checksum切换瞬间停机时间 完全停服切流可能导致分钟级不可用 短时间暂停写入秒级等增量完全追上后切换配合数据库中间件动态路由 预发布配置、ProxySQL 等大事务风险 一次性提交大量数据导致长事务undo爆增主从延迟 每批只提交1000行–txn-size 严格控制 pt-archiver 参数、手动提交循环 总结升华“10亿级数据迁移本质上是在性能、一致性、业务连续性三者间找平衡。代码上抓住分段并行、断点续传、幂等处理策略上做好全量增量校验难点通过工具链索引设计流控一一击破。这样即使数据再翻一倍架构依然稳得住。”面试官 “这个总结很好代码亮点和难点都讲透了。看来你确实亲自趟过这些坑很不错。”