引言:一场没有硝烟的“数据大迁徙”
想象一下,你正在为一家电商公司优化数据库架构,需要将 MySQL 迁移到分布式数据库 TiDB。但问题来了:如何在业务高峰期不停止服务,同时确保数据零丢失?
这不仅是技术挑战,更是一场精密的“数据芭蕾舞”。今天,我们就从理论到实战,手把手教你完成这场“不可能的任务”!
一、迁移前的“战前沙盘推演”
1.1 数据摸底:绘制“数据地图”
- 数据规模:统计表大小、索引、分区信息(示例:
SELECT table_name, table_rows FROM information_schema.tables
)。 - 业务流量:监控 QPS、TPS、热点表(工具:Prometheus + Grafana)。
- 依赖分析:确认外键、存储过程、定时任务等关联组件。
1.2 迁移目标:明确“作战目标”
- 目标架构:单机 → 分布式(如 MySQL → TiDB)或云厂商迁移(AWS RDS → 阿里云 PolarDB)。
- SLA要求:RPO(恢复点目标)≤ 1分钟,RTO(恢复时间目标)≤ 5分钟。
二、迁移方案设计:选择你的“武器库”
2.1 方案1:双活架构 + 增量同步
- 原理:新旧数据库并行运行,通过日志同步增量数据。
- 工具:
- MySQL:
pt-online-schema-change
(表结构变更) - 通用方案:Canal(解析 binlog) + Flink(实时同步)
- 云服务:AWS DMS、阿里云 DTS
- MySQL:
- Java双写示例:
public void writeData(DataSource oldDS, DataSource newDS, Data data) { try (Connection conn1 = oldDS.getConnection(); Connection conn2 = newDS.getConnection()) { conn1.setAutoCommit(false); conn2.setAutoCommit(false); // 同步写入新旧数据库 insertIntoDB(conn1, data); insertIntoDB(conn2, data); conn1.commit(); conn2.commit(); } catch (SQLException e) { // 异常处理:记录日志并回滚 rollback(conn1); rollback(conn2); throw new RuntimeException("双写失败", e); } }
2.2 方案2:分阶段迁移(Phased Migration)
- 步骤:
- 冷数据迁移:先迁移历史数据(如备份恢复)。
- 热数据同步:通过 CDC(Change Data Capture)实时同步增量数据。
- 流量切换:灰度发布,逐步将请求切到新库。
三、迁移实施:执行“精密手术”
3.1 冷数据迁移:备份与恢复
- 工具:
mysqldump
(全量导出)pg_dump
(PostgreSQL)- Java批量导入:
// 使用JDBC批量插入提升效率 try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("INSERT INTO user VALUES (?, ?, ?)")) { for (User user : userList) { ps.setLong(1, user.getId()); ps.setString(2, user.getName()); ps.setString(3, user.getEmail()); ps.addBatch(); } ps.executeBatch(); }
3.2 增量同步:让数据“永不停跳”
- 关键点:
- 日志位点记录:保存 binlog 文件名和位置(
SHOW MASTER STATUS
)。 - 冲突解决:处理新旧库同时写入的冲突(如选择新值覆盖旧值)。
- 日志位点记录:保存 binlog 文件名和位置(
- 工具示例:
# 使用Debezium进行实时同步 bin/connect-standalone.properties \ debezium-connector-mysql.properties \ debezium-sink-kafka.properties
四、数据一致性校验:揪出“潜伏的BUG”
4.1 全量校验:逐行对比
- 工具:
pt-table-checksum
(Percona工具)- Java自定义脚本:
public void verifyData(DataSource source, DataSource target, String tableName) { try (Connection srcConn = source.getConnection(); Connection tgtConn = target.getConnection()) { // 获取源表数据哈希值 String srcHash = getTableHash(srcConn, tableName); String tgtHash = getTableHash(tgtConn, tableName); if (!srcHash.equals(tgtHash)) { throw new DataMismatchException("表 " + tableName + " 数据不一致"); } } }
4.2 持续监控:防止“数据漂移”
- 指标监控:
- 新旧库 QPS、TPS 对比
- 同步延迟(如 Kafka 消息堆积)
- Java监控示例:
// 使用Micrometer监控同步延迟 MeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); registry.gauge("cdc_delay_seconds", () -> calculateDelay(sourceDS, targetDS));
五、回滚策略:准备“逃生舱”
5.1 快速回滚:一键切换流量
- 方案:
- DNS切换:修改数据库域名指向旧库。
- 代理层路由:通过 Mycat 或 ProxySQL 动态切换后端库。
- Java配置热更新:
// 使用Apollo配置中心动态修改数据源 @Value("${db.url}") private String dbUrl; @Scheduled(fixedRate = 60000) public void refreshDataSource() { dataSource.setUrl(dbUrl); }
5.2 数据修复:修补“时空裂缝”
- 工具:
pt-table-sync
(Percona工具)- Java修复脚本:根据校验结果批量修复差异数据。
六、实战案例:从 MySQL 到 TiDB 的“真刀真枪”
6.1 场景:电商大促前的数据库升级
- 挑战:日均 500 万订单,RPO ≤ 1 秒。
- 方案:
- 双写阶段:通过 Canal 同步数据到 TiDB。
- 流量切换:在凌晨低峰期,通过 DNS 切换流量。
- 验证:使用 JMeter 模拟高并发读写,对比新旧库结果。
结语:迁移不是“终点”,而是“新起点”
- 核心原则:
- 最小化停机:通过双活和灰度发布逐步迁移。
- 数据校验至上:宁可慢一点,不可错一点。
- 工具赋能:善用 CDC、监控和自动化工具。
留言区互动:
“你经历过哪些惊心动魄的数据库迁移?遇到过什么奇葩问题?”
“在迁移过程中,你最担心的是什么?”
点击“评论”,分享你的故事!下一期我们将揭秘“分布式事务的21种死法”——不见不散!