历史数据迁移实践:从小规模补录到大规模批量迁移
前言
在实际项目中,历史数据迁移常常被低估其工程复杂度。早期实现通常被简化为“写一个临时脚本完成数据搬运”,但当数据规模扩大或业务规则复杂化之后,这类任务往往会逐渐演变为一个需要具备事务控制、幂等设计和失败恢复能力的工程问题。
在真实的生产环境中,数据迁移通常不是一个单一的执行动作,而是一个具备明确约束与治理目标的数据处理过程。一个相对规范的历史数据迁移工程,通常需要同时满足以下四个核心目标:
- 数据正确性: 确保源数据到目标数据的映射在语义与结构层面保持一致,避免字段错位、状态误转换或关键数据缺失,尤其在涉及资金、用户状态或核心业务实体时必须严格保证一致性。
- 幂等性与可重入能力: 迁移任务在任意阶段中断后,经过修复或重试,可以重新执行而不会产生重复数据或副作用,保证多次执行结果一致。
- 可恢复性: 支持断点续跑能力,即在任务失败后能够基于持久化的进度信息,从最近一次成功状态继续执行,而无需全量重跑。
- 资源可控性: 迁移过程必须对数据库与应用资源消耗进行约束,避免引发长时间锁等待、大事务堆积、慢查询放大或 JVM 内存压力过高等问题。
一、 数据迁移的本质
从软件工程的视角来看,数据迁移的本质绝不是纯粹的数据复制。如果是复制,我们直接用底层存储的备份恢复(如物理复制、闪回)就可以了。
数据迁移的底层逻辑,是在新系统的业务规则下,重新构建旧数据。
一个完整的工业级数据迁移生命周期,必然包含以下四个核心阶段:
┌───────────────────────┐
│ 数据读取 (Extraction) │ --> 从各种异构源头拉取 Raw Data
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 数据转换 (Transform) │ --> 核心业务逻辑清洗与状态重构
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 数据写入 (Loading) │ --> 批量、高并发、幂等的安全写入
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 状态记录 (Checkpoint) │ --> 每一批次的持久化进度锚点
└───────────────────────┘
二、 架构决策:工具 vs 代码,如何进行架构取舍?
1. 工具的优势与局限
代表工具: DataX, Kettle, Cloud Migration Tools, Canal
什么时候用工具?
- 异构源搬运: 比如从 MongoDB 迁到 MySQL,或者从 Oracle 迁到 Postgre。工具自带适配器,省去了写驱动的时间。
- 简单清洗: 只需要改个字段名、过滤掉某些行。
- 极致吞吐: 工具通常在底层做了零拷贝或极速缓冲区优化,纯搬运速度远超 Java 代码。
- 工具的“天花板”:
一旦涉及“跨表聚合”或“上下文关联”,工具就抓瞎了。 - 案例: 旧系统用户信息在
user_t,等级在level_t。新系统要求合成一张表,且等级要根据过去一年的订单量重新计算。这种逻辑在 DataX 这种流式同步工具里极难实现,强行写 UDF(自定义函数)的开发成本远高于写 Java 代码。
2. SQL 导数的风险边界
代表选手: INSERT INTO ... SELECT ...
- 什么时候用 SQL?
- 同库同构: 仅仅是做个备份表,或者表结构完全没变。
- 为什么生产环境慎用?
- 资源掠夺: 大规模
SELECT会瞬间拉高磁盘 I/O,并产生长时间的间隙锁(Gap Locks),直接导致线上正常的业务写入被堆积、超时甚至挂掉。
3. 自定义 Importer:业务重构的“手术刀”
核心定位:它是带有“业务灵魂”的数据重建程序。
我们之所以在很多项目中选择 Importer,是因为它具备工具无法替代的特征:
- 复杂映射逻辑:
旧系统:status = 'A', 'B', 'C'
新系统:status = 1, 2, 3(且需要根据老系统的audit_log历史记录来推断当前的sub_status)。
这种逻辑只有在 Importer 中,通过注入 Spring Service 调用现有的业务代码才能实现。 - 分布式 ID 的重新洗牌:
历史数据迁移往往伴随着主键策略的变更(比如从自增 ID 变成雪花 ID)。Importer 可以在内存中轻松完成新旧 ID 的映射并保持关联表的一致性。 - 三方服务联动:
迁移数据时,你可能需要实时调用一次短信接口、推送一次配置或者在 Redis 里预热缓存。这些“副作用”是 SQL 或搬运工具绝对无法完成的。
4. 总结
可以根据以下维度快速判断不同迁移方案的适用边界:
| 维度 | SQL 直接导入 | 开源迁移工具 | 自定义 Importer |
|---|---|---|---|
| 适用场景 | 同构数据直接搬运 | 标准化数据同步 | 涉及复杂业务规则的数据迁移 |
| 数据转换能力 | 较弱 | 中等 | 强 |
| 跨表/跨库处理能力 | 有限 | 有限 | 灵活 |
| 业务规则表达能力 | 较弱 | 有限 | 强 |
| 异常处理与重试 | 基础依赖数据库事务 | 支持部分失败处理 | 可按业务自定义 |
| 断点恢复能力 | 通常需要自行实现 | 多数工具支持 | 可完全自定义 |
| 资源控制能力 | 较依赖 SQL 本身质量 | 支持限流、限速 | 可按代码精细控制 |
| 开发与维护成本 | 低 | 中等 | 较高 |
| 可观测性与扩展性 | 一般 | 中等 | 高 |
三、 标准迁移架构模型
为了让系统具备良好的扩展性和职责分离能力,标准的 Importer 架构通常呈现为以下清晰的层级:
┌──────────────┐
│ 数据源层 │ --> 无论是 CSV 文件、老数据库还是三方 API
│ CSV / DB / API│
└──────┬───────┘
↓
┌────────────────────┐
│ Importer 读取层 │ --> 统一抽象的 Extraction 读组件
└────────┬───────────┘
↓
┌────────────────────┐
│ 数据转换 + 清洗层 │ --> 纯内存处理的 Transformation 业务域
└────────┬───────────┘
↓
┌────────────────────┐
│ 批量写入层 │ --> 高效的 Loading 写入组件(带事务与幂等)
└────────┬───────────┘
↓
┌────────────────────┐
│ 正式业务表 │ --> 最终的目标新数据库
└────────────────────┘
四、实现结构简析
在真正的工程实践中,历史数据迁移本质上都离不开四个核心步骤:
数据抽取(Extract)
↓
数据转换(Transform)
↓
数据写入(Load)
↓
进度记录(Checkpoint)
无论是:
- Excel 导入
- 老系统升级
- 数据补录
- 分库分表
- 上云迁移
底层其实都是这一套流程。
一个典型的 Importer 往往长这样:
@Transactional
public void importAll() {
// 1. 数据抽取
List<User> list = queryAll();
// 2. 数据转换 + 批量写入
saveBatch(list);
}
其中:
| 方法 | 本质职责 |
|---|---|
queryAll() |
从老系统、CSV、API 中读取原始数据 |
convert() |
做状态映射、字段清洗、默认值补齐 |
saveBatch() |
批量写入数据库 |
saveCheckpoint() |
记录迁移进度 |
需要特别注意的是:
数据迁移真正复杂的地方,通常不是“读取数据”,而是“如何稳定地写入数据”。
尤其在数据量逐渐增大之后:
- 事务怎么拆?
- 失败怎么恢复?
- 如何避免重复导入?
- 如何控制数据库压力?
这些问题会迅速成为系统核心矛盾。
因此:
不同数据规模下,迁移系统的设计思路会完全不同。
五、小规模数据迁移(一次性脚本模型)
5.1 适用场景
小规模迁移通常出现在:
- 一次性数据补录
- 测试环境数据恢复
- 小型系统初始化
- Excel/CSV 人工导入
- 小型业务系统升级
这一阶段的数据量通常在:
| 数据规模 | 常见情况 |
|---|---|
| 几百 ~ 几千 | 最常见 |
| 1 万左右 | 部分场景仍可接受 |
| 更高 | 需结合单条数据大小评估 |
这里需要特别说明:
“小规模”并不是固定数字,而是事务是否仍然可控。
真正影响是否还能采用单事务的因素包括:
- 单事务执行时间
- 数据库锁持有时间
- JVM 内存占用
- Undo Log 增长速度
- 单条记录大小
例如:
- 普通小字段表,1 万条数据可能仍然可以单事务处理;
- 如果包含 TEXT/BLOB 大字段,几百条数据都可能需要拆批。
因此:
工程上判断“小规模”的核心标准,是事务风险是否仍然可接受。
5.2 小规模迁移的核心特点
这一阶段最大的特点是:
可以接受失败后整体重跑。
整体思路更偏向:
“快速、安全地完成一次性任务”。
即使是小规模迁移,如果存在重复执行风险,仍然建议具备基础幂等能力。
例如:
INSERT IGNORE INTO user ...
或者:
INSERT ... ON DUPLICATE KEY UPDATE
配合唯一索引后,即使脚本重复执行,也不会产生重复数据。
5.3 典型实现
@Transactional
public void importAll() {
// 一次性读取全部数据
List<User> list = queryAll();
// 转换并批量写入
saveBatch(list);
}
这里的实现逻辑非常简单:
queryAll()
负责一次性读取原始数据,例如:
- 老系统数据库
- CSV 文件
- Excel
- 第三方接口
这一阶段通常直接将数据加载到内存。
saveBatch(list)
负责:
- 数据清洗
- 字段转换
- 默认值补齐
- 状态映射
- Batch Insert 批量写入
这里必须强调:
saveBatch的底层绝对不能是循环单条 Insert。
错误写法:
for (User user : list) {
userMapper.insert(user);
}
正确做法应该是 JDBC Batch 或批量 SQL。
例如:
INSERT INTO user(id, name)
VALUES
(1, 'A'),
(2, 'B'),
(3, 'C');
这样可以显著减少:
- SQL 执行次数
- 网络 IO 往返
- 数据库连接消耗
5.4 为什么小规模可以使用单事务?
因为:
- 数据量较小
- 回滚成本可接受
- 长事务风险较低
- 实现复杂度最低
因此:
@Transactional
在小规模场景下是非常合理的方案。
因为它能够保证:
要么全部成功,要么全部失败。
同时:
- 排查简单
- 开发成本低
- 代码最清晰
5.5 小规模阶段最容易犯的错误
(1)过度设计
很多人一开始就引入:
- Checkpoint
- 多线程
- 分布式调度
- 分片任务
结果:
数据只有几千条,迁移系统却比业务系统还复杂。
这是典型的过度设计。
(2)循环单条写入
错误示例:
for (User user : list) {
userMapper.insert(user);
}
这种方式会导致:
- SQL 次数暴涨
- 网络 IO 增加
- 数据库压力明显上升
正确做法应该始终是:
Batch Insert。
5.6 小规模迁移的核心原则
这一阶段最重要的不是复杂架构,而是:
简单、稳定、快速完成。
因为:
能用简单方案解决的问题,不应该提前引入复杂系统。
六、中等规模数据迁移(标准批处理模型)
6.1 适用场景
当数据规模继续增长后:
- 单事务开始不可控
- JVM 内存压力明显增加
- 回滚成本迅速上升
- 长事务风险越来越高
这一阶段通常出现在:
- 历史订单迁移
- 用户数据迁移
- 多年业务归档
- 多租户迁移
- 中型业务系统升级
数据规模通常会进入:
| 数据规模 | 常见范围 |
|---|---|
| 几万 ~ 几十万 | 最典型 |
| 百万以内 | 部分场景仍属于中规模 |
但仍然需要强调:
这不是绝对数字,而是工程经验区间。
6.2 中规模阶段最大的变化
这一阶段最大的变化只有一句话:
“单事务已经不可控。”
继续使用:
queryAll() + saveBatch(allData)
会逐渐出现:
- Full GC
- JVM 内存持续增长
- Undo Log 膨胀
- Binlog 激增
- 长事务锁等待
- 回滚耗时极长
因此:
中规模阶段必须开始拆批。
6.3 中规模迁移的核心设计
这一阶段通常会形成:
分页读取
↓
分批事务
↓
Checkpoint(可选增强)
这里需要特别说明:
中规模阶段,“分批事务”才是真正的核心。
很多人以为:
“分页读取”就已经解决问题了。
其实不是。
如果整个 while 循环仍然共用一个事务:
@Transactional
public void importAll() {
while (true) {
...
}
}
本质上仍然是:
一个超级大事务。
因此:
中规模阶段必须引入“每批独立事务”。
6.4 正确实现模型
while (true) {
List<User> list = queryBatch(lastId);
if (list.isEmpty()) {
break;
}
processBatch(list);
lastId = list.get(list.size() - 1).getId();
saveCheckpoint(taskName, lastId);
}
其中:
queryBatch(lastId)
负责:
- 基于主键游标分页
- 避免 OFFSET 深分页性能问题
- 控制单次内存占用
典型 SQL:
SELECT *
FROM user
WHERE id > ?
ORDER BY id
LIMIT 1000;
processBatch(list)
负责:
- 数据转换
- 批量写入
- 当前批次事务提交
通常会配置:
@Transactional(
propagation = Propagation.REQUIRES_NEW
)
确保:
每一批数据独立提交。
6.5 中规模阶段是否必须做 Checkpoint?
不一定。
这里需要明确区分:
| 能力 | 解决的问题 |
|---|---|
| 幂等 | 重复执行是否安全 |
| Checkpoint | 失败后从哪里继续 |
因此:
情况一:可以接受全量重跑
如果:
- 数据量不算特别大
- 重跑成本仍然能接受
- 导入时间较短
那么:
可以只做幂等,不做 Checkpoint。
例如:
INSERT IGNORE
或者:
ON DUPLICATE KEY UPDATE
即使任务重新执行,也不会产生重复数据。
这种方案在很多中规模场景下已经足够。
情况二:重跑成本已经不可接受
例如:
- 已经跑了几个小时
- 已导入几十万数据
- 失败后重跑代价很高
那么:
就必须引入 Checkpoint。
例如:
saveCheckpoint(taskName, lastId);
用于记录:
- 当前处理位置
- 已完成进度
- 恢复锚点
6.6 中规模阶段的核心目标
这一阶段本质上是在解决:
“如何避免大事务,并让迁移具备基础恢复能力”。
因此:
- 分页读取
- 分批事务
- 基础幂等
基本已经成为标配。
而 Checkpoint 是否必须,则取决于:
“失败后重跑的成本是否还能接受”。
七、大规模数据迁移(高性能状态化系统)
7.1 适用场景
当迁移数据进一步增长后:
- 单线程已经无法接受
- 单机吞吐达到瓶颈
- 数据库压力急剧增加
- 迁移可能持续数小时甚至数天
这一阶段通常包括:
- 百万级订单
- 千万级日志
- 海量用户数据
- 分库分表重构
- 上云迁移
7.2 大规模阶段最大的变化
到了这一阶段:
迁移系统已经不再是“脚本”。
而是:
一个长期运行的后台任务系统。
系统关注点也从:
如何导数据
逐渐变成:
如何长期稳定地导数据
7.3 大规模迁移的核心能力
这一阶段通常必须同时具备:
| 能力 | 作用 |
|---|---|
| 分片并发 | 提升整体吞吐 |
| 分批事务 | 降低事务风险 |
| 幂等写入 | 支持重复执行 |
| Checkpoint | 支持断点恢复 |
| JDBC Batch | 降低数据库交互 |
| 限流降压 | 保护线上数据库 |
7.4 为什么大规模阶段幂等是“必须”?
因为:
大规模迁移中,重复执行几乎一定会发生。
例如:
- 线程失败重试
- 服务重启
- 网络闪断
- 分片重新调度
都会导致:
同一批数据被重复消费。
因此:
INSERT ... ON DUPLICATE KEY UPDATE
会逐渐成为标准配置。
7.5 为什么大规模阶段必须做 Checkpoint?
因为:
已经无法接受失败后全量重跑。
例如:
- 已迁移 3000 万数据
- 已运行 10 小时
- 最后阶段任务失败
如果没有 Checkpoint:
只能重新跑一天。
因此:
- 当前进度
- 分片状态
- 失败批次
- 已完成游标
都必须持久化。
这也是为什么:
大规模迁移本质上已经演变成“状态化任务系统”。
7.6 为什么大规模一定要限流?
很多迁移失败并不是:
“导不进去”。
而是:
“把线上数据库打挂了”。
因此:
- 固定线程池
- Batch Size 控制
- TPS 限流
- 分时迁移
都会成为必须能力。
因为迁移系统最终比拼的已经不是:
“谁写代码快”。
而是:
“谁更懂生产环境资源治理”。
八、总结
历史数据迁移并不是简单的“写个脚本导数据”,而是一个完整的数据处理工程。
随着数据规模不断增长,系统设计重点也会持续演进:
| 数据规模 | 核心方案 | 核心关注点 |
|---|---|---|
| 小规模 | 单事务 + Batch Insert | 快速、安全完成任务 |
| 中规模 | 分页 + 分批事务 + 基础幂等 | 避免大事务、支持恢复 |
| 大规模 | 并发分片 + 幂等 + Checkpoint | 长期稳定运行与资源治理 |
整个演进过程,本质上就是:
一次性脚本
↓
标准批处理
↓
可恢复迁移系统
↓
高性能状态化任务平台
数据量越大,系统关注点就越从“功能实现”转向:
- 事务控制
- 幂等设计
- 断点恢复
- 并发调度
- 数据库资源治理
历史数据迁移真正困难的,从来不是“把数据导进去”,而是如何在保证数据正确性的前提下,让迁移任务稳定、可恢复、可持续地运行。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)