🚀 Java 实战:如何优雅地同步几十万、上百万条数据到数据库?

在后端开发中,我们经常需要调用第三方接口或从其他系统同步大量数据到本地数据库。面对几十万甚至上百万的数据量,如果处理不当,轻则程序跑得慢如蜗牛,重则直接导致内存溢出(OOM)或数据库超时崩溃。

今天就来系统梳理一套从“基础稳健”到“高阶极速”的数据同步最佳实践,帮你稳稳搞定海量数据落库!

💣 核心雷区:绝对不要把数据一次性全放内存!

无论数据是几十万还是几百万,绝对不能一次性拉取并全部塞进 JVM 堆内存中。这会导致内存迅速被占满,频繁触发 Full GC,最终引发 OutOfMemoryError 导致服务宕机。

正确的核心思路永远是:“少量多次、边拉边存、及时释放”

🛡️ 基础稳健篇:分批拉取 + 真批量插入

面对几十万条数据,我们采用“流式处理”的策略,每次只拉取一小部分(比如 1000 条),入库后释放内存,再拉取下一批。

1. 循环分页拉取(伪代码思路)

int pageNum = 1;
int pageSize = 1000;
boolean hasMoreData = true;

while (hasMoreData) {
    // 1. 调用接口,仅获取当前页的少量数据
    List<UserDTO> currentPageData = fetchPageDataFromApi(pageNum, pageSize);
    
    if (currentPageData == null || currentPageData.isEmpty()) {
        hasMoreData = false; 
        break;
    }
    
    // 2. 将这1000条数据批量插入数据库
    batchInsertToDatabase(currentPageData);
    
    // 3. 循环结束,当前批次数据离开作用域,等待GC回收
    pageNum++;
}

2. 避开“伪批量插入”的陷阱
在循环中落库时,千万不要以为调用了框架的 saveBatch(list) 就万事大吉。很多 ORM 框架的批量方法,底层其实是循环执行单条 INSERT,性能极差。

要实现真批量插入,推荐以下两种方式:

  • MyBatis XML 动态 SQL(推荐):使用 <foreach> 标签拼接 SQL,将 1000 条数据合并为一条 INSERT INTO ... VALUES (...), (...), (...)
  • JDBC 原生 Batch:使用 PreparedStatementaddBatch()executeBatch() 方法。

3. 必开的 MySQL 性能开关
在你的数据库连接 URL 后面,务必加上参数:rewriteBatchedStatements=true。开启后,MySQL 驱动会在底层自动将多条 INSERT 语句合并执行,批量插入性能可提升数倍甚至十几倍。

⚡ 高阶极速篇:百万级数据的并发与游标优化

当数据量飙升到几百万时,简单的 while 循环会暴露两个致命问题:

  1. 越跑越慢:传统的 LIMIT offset, size 分页,随着 offset 增大,数据库需要扫描并跳过前面的海量行,性能呈断崖式下跌。
  2. 耗时过长:单线程按顺序跑几百万条数据,可能需要数小时。

1. 采用“游标分页”代替传统分页
放弃 OFFSET,利用数据中连续且唯一的字段(如自增主键 id创建时间)作为游标。

  • 传统慢查询SELECT * FROM table LIMIT 2000000, 1000
  • 游标极快查询SELECT * FROM table WHERE id > 上一批最后一条的id ORDER BY id LIMIT 1000

每次拉取完数据后,记录下最大的 id,下一批直接通过 WHERE id > last_id 定位。无论拉到第几百万条,数据库都能通过索引瞬间定位,速度始终如一。

2. 引入多线程并发处理
将几百万条数据拆分成多个“小包裹”,交给固定大小的线程池(如 10 个线程)并行处理。

  • 控制线程数:线程不是越多越好,通常 5-20 个线程足矣,避免打满数据库连接池。
  • 事务隔离:每个线程处理自己那一批数据时,使用独立的小事务。线程 A 失败了不要影响线程 B 的成果,避免长事务导致数据库锁表。

📌 总结:海量数据同步的四大黄金法则

  1. 拒绝全量加载:始终保持“拉取 -> 落库 -> 释放”的节奏,保护内存安全。
  2. 拒绝 OFFSET 分页:尽量要求接口支持按 ID 或时间戳拉取;查库务必使用 WHERE id > last_id 的游标分页。
  3. 拒绝伪批量插入:手写 <foreach> SQL 或使用 JDBC Batch,并开启 rewriteBatchedStatements=true
  4. 拒绝大事务:每批次数据作为一个独立的小事务提交,适度引入多线程并发,将单线程的“长途跋涉”变成多线程的“分头行动”。

更多推荐