写这篇东西的起因很简单:前阵子给一个数据中台项目做历史数据迁移,单表一千多万行,从Oracle往MySQL搬。一开始用最原始的JDBC循环insert,跑了四十分钟才插进去两百万行,按这个速度得等三个多小时,肯定不行。后来把批量插入、事务切分、MySQL端参数全部调了一遍,最终把全量导入压到了20分钟以内。这篇文章就是把这次调优过程中真正起作用的细节整理出来,包括为什么批量插入会快、JDBC和MyBatis两种写法的正确姿势、以及从"能跑"到"跑得快"之间那些容易踩的坑。
如果你正在做数据迁移、报表数据初始化,或者线下数据导入线上库这类工作,这篇文章应该能帮你省掉不少试错时间。代码层面的写法、驱动参数的设置、MySQL服务端参数的调整,我会逐个拆开讲。
1. 批量插入效率的瓶颈到底卡在哪里:从一次插入一条说起
很多人一提批量插入,第一反应就是"把多条INSERT拼成一条SQL"。这确实是批量插入最直观的形态,但它只是表象。真正决定导入速度的是三个层面的东西:网络往返次数、SQL解析与执行计划的重复开销、事务提交时的刷盘动作。如果你只改了SQL形态而没动这三样,效率提升会非常有限。
1.1 单条插入慢,慢在哪里
先看最基本的场景:用JDBC一条一条执行INSERT,就像这样:
java复制Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement ps = conn.prepareStatement("INSERT INTO t_user (name, age, address) VALUES (?, ?, ?)");
for (User user : userList) {
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setString(3, user.getAddress());
ps.executeUpdate(); // 每次都是一次完整的网络往返
}
conn.commit();
这种写法每一行数据都要经历:客户端把SQL发给MySQL服务器、MySQL做词法分析、语法分析、生成执行计划、执行插入、返回结果,然后客户端再发下一条。一次往返的耗时哪怕只有0.5毫秒,一千条就是500毫秒,一百万条就是500秒,光网络开销就把时间吃掉了。
更重要的是,默认情况下MySQL的autocommit是开启的,executeUpdate()每执行一次就会隐式提交一次事务。而事务提交是重操作——InnoDB需要把redo log刷到磁盘,还需要释放行锁、更新相关的内部状态。频繁提交事务带来的开销,很多时候比INSERT本身还要大。
1.2 批量插入为什么能快:三个关键因素的改变
批量插入的效率来源,本质上就是针对上面三个瓶颈逐一拆解:
减少网络往返。 这是最直观的一点。把一千条数据打包成一条SQL或者用JDBC的批量机制一次发给MySQL,网络交互从一千次变成一次或者几十次。对于跨机房、跨网段的数据库,这个收益尤其明显。
减少SQL解析与执行计划生成。 MySQL拿到一条多VALUES的INSERT语句,虽然VALUES数量多了,但SQL模板只有一个,词法分析、语法分析只做一遍,执行计划也只生成一次。对比一下,一千条独立INSERT需要做一千遍完整解析。这个在高并发插入场景下是实打实的CPU开销。
事务提交次数大幅下降。 如果你用一条SQL插入一千行,整个操作在一个事务内完成,只需要在最后提交一次。一次事务提交的redo log刷盘成本被一千条数据摊薄了,单位数据的提交成本瞬间降了几个数量级。
这里可以用一个日常生活里的类比:单条插入像寄快递,每个包裹跑一趟快递站;批量插入像叫了一辆大卡车把一堆包裹一次性拉走。卡车的调度成本确实比单个包裹高一点,但均摊到每个包裹上就微不足道了。
1.3 一个容易被忽略的隐藏瓶颈:索引维护
如果说上面三条是批量插入的"主要矛盾",那还有一条"次要矛盾"经常被忽略:索引维护。
InnoDB的二级索引不是顺序插入的。对于非自增主键或者二级索引,每次插入都要在B+树中定位插入位置,如果索引列的值随机分布,还会引发频繁的页分裂和页合并。数据量一上来,索引维护成本可能超过数据本身写入成本。
这也是为什么很多数据导入实战里,面对一个大表会先删掉非必要索引、导入完成后再重建。原因就是让数据插入阶段尽可能减少随机写入,等数据落盘后再通过一次性的索引构建来组织数据结构。实际项目中,这种做法往往能带来30%到50%的额外提速,尤其当表里有三四个二级索引时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JDBC批量插入的正确姿势:rewriteBatchedStatements与addBatch实战
Java后端开发做批量插入,最常见的两种实现思路是:原生JDBC的addBatch/executeBatch,以及MyBatis的foreach拼接。两种方式底层逻辑不一样,性能和坑也完全不同。这一节先把原生JDBC讲透。
2.1 一个坑先说明白:没设rewriteBatchedStatements的addBatch是假的
网上很多教程说JDBC批量插入只要用addBatch就行,实测下来根本不是这么回事。在MySQL的JDBC驱动里,如果你连接串上没有设置rewriteBatchedStatements=true,那么executeBatch()仍然会把每条INSERT单独发到服务器,只不过客户端少了几次网络往返的编码开销而已,服务器端该解析的解析、该提交的提交,一样不少。
我做过一个对比测试,插入10万条数据,表结构是三个字段、两个索引:
| 写入方式 | 耗时 |
|---|---|
| 单条executeUpdate | 42秒 |
| addBatch,未设置rewriteBatchedStatements | 35秒 |
| addBatch,设置rewriteBatchedStatements=true | 9秒 |
仅仅加了一个连接参数,性能从35秒提升到9秒,接近4倍差距。原因就是rewriteBatchedStatements=true会把一批INSERT INTO t (a,b) VALUES (?,?)自动重写成一条多VALUES的语句,示例效果等价于:
sql复制INSERT INTO t_user (name, age, address) VALUES ('张三', 25, '北京'), ('李四', 30, '上海'), ('王五', 28, '广州');
重写之后,网络往返变成一次,SQL解析也变成一次,效果立竿见影。
正确写法是连接串加上参数:
java复制String url = "jdbc:mysql://127.0.0.1:3306/test_db?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
2.2 标准的JDBC批量插入模板
下面这套模板是我在实际项目中一直在用的,基本上可以直接抄:
java复制public void batchInsert(List<User> userList) {
String sql = "INSERT INTO t_user (name, age, address) VALUES (?, ?, ?)";
int batchSize = 1000;
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false);
for (int i = 0; i < userList.size(); i++) {
User user = userList.get(i);
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setString(3, user.getAddress());
ps.addBatch();
if ((i + 1) % batchSize == 0) {
ps.executeBatch();
conn.commit();
}
}
ps.executeBatch();
conn.commit();
} catch (SQLException e) {
// 实际项目中建议记录批次序号,方便失败后续传
e.printStackTrace();
}
}
几个关键点逐一说明:
- 关闭自动提交。
conn.setAutoCommit(false)设置后,通过每次executeBatch()——注意是500条或1000条——手动提交,避免每条数据都触发事务提交。这里批次大小可以根据数据行宽调整,行宽小可以到2000,行宽大建议500。 - 每批次控制在一个事务里。
executeBatch()和conn.commit()配套出现,保证每一批数据要么全成功要么全失败。如果一批中间出错,已经在批里的数据会被回滚,避免脏数据。 - try-with-resources保证资源释放。 千万别在循环里反复创建PreparedStatement和Connection,性能会退化到比单条插入还差。
2.3 DBeaver批量导入报错而单独执行正常的排查经过
在很多数据库运维或开发场景里,遇到过这样一个典型现象:用DBeaver或Navicat等图形化工具往MySQL里导入批量数据时,SQL报错,而把SQL拆成单条执行又是成功的。很多人第一反应是"数据有问题",但实际排查下来,大部分时候是下面这两个原因之一。
第一个原因是单条SQL语句超过了max_allowed_packet限制。批量插入如果拼成一条超大的INSERT语句,会直接触发Packet too large报错。这个报文记录在MySQL的错误日志里,很容易定位。解决办法很简单,在MySQL的配置文件中调大限制:
ini复制[mysqld]
max_allowed_packet = 64M
然后重启MySQL,或者用SET GLOBAL max_allowed_packet = 67108864;动态调整。注意动态调整只对后续新连接生效,已经建立的连接需要重连。
第二个原因是非严格SQL模式下的类型转换。批量导入时,某些字段值在单独插入时可能刚好匹配,但在批量SQL里触发MySQL的隐式类型转换规则。比如一个VARCHAR字段传入了数字,单独执行时MySQL帮你转了,批量场景下由于SQL块更大、计划缓存等因素,报错就会冒出来。检查一下表结构和导入数据的字段对应关系,把类型严格对齐,这类问题基本可以消除。
还有一个不算少见的原因:图形化工具生成批量INSERT时用到了客户端不支持的字符集或排序规则。比如源库是utf8mb4_general_ci,目标表是utf8mb4_0900_ai_ci,某些生僻字或组合字符在排序比较时触发错误。用SHOW COLLATION查看一下两边排序规则,尽量保持一致。
3. MyBatis场景下的批量插入:foreach拼接与分片方案的取舍
实际业务开发里,大家用得更多的其实是MyBatis和MyBatis-Plus。这一节把这两种框架下的批量插入方案和性能差异讲清楚。
3.1 MyBatis的foreach批量插入:最方便,但SQL长度有上限
MyBatis最经典的批量插入写法是foreach拼接:
xml复制<insert id="batchInsert" parameterType="list">
INSERT INTO t_user (name, age, address)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.age}, #{item.address})
</foreach>
</insert>
对应Java接口:
java复制int batchInsert(@Param("list") List<User> userList);
这种写法的优点非常明显:实现简单、SQL直观、性能优秀。原理和JDBC的rewriteBatchedStatements重写后的效果一致,一条多VALUES语句搞定。
但它的致命短板是:一次foreach的数据量不能太大,否则生成的SQL文本会超过MySQL的max_allowed_packet。我曾经在项目里用foreach一次插入2万条数据,每条五个字段,直接抛PacketTooBigException。后来把单次插入批次控制在1000到2000条,问题消失。
所以用foreach批量插入,建议配合分批逻辑:
java复制public void batchInsert(List<User> userList) {
int batchSize = 1000;
for (int i = 0; i < userList.size(); i += batchSize) {
int end = Math.min(i + batchSize, userList.size());
List<User> subList = userList.subList(i, end);
userMapper.batchInsert(subList);
}
}
3.2 MyBatis-Plus的saveBatch:开箱即用的选择
如果是MyBatis-Plus项目,直接调用IService.saveBatch()是最省事的方案。它的底层实现其实就是通过SqlSession执行批量操作,和JDBC的addBatch机制类似。
用起来非常简单:
java复制@Autowired
private UserService userService;
public void importUsers(List<User> userList) {
userService.saveBatch(userList);
}
saveBatch默认每次批量1000条,可以通过传入第二个参数调整:userService.saveBatch(userList, 500)。
需要注意的是,MyBatis-Plus的saveBatch默认走的是ExecutorType.BATCH的SqlSession,这一点和普通save不走同一个执行器。如果你遇到saveBatch性能不如预期的场景,可以把连接串加上rewriteBatchedStatements=true再观察,这同样对MyBatis-Plus生效。
3.3 当foreach拼接遇到SQL长度上限:分片机制是怎么工作的
前面提到foreach拼接受SQL长度限制。当前网上搜索热度里也有一个词叫"分片",很多人问"分片到底是怎么分的"。这里简单解释一下,分片的核心就是:把一个大的批量插入需求,拆成多个不超过SQL长度或参数个数上限的小批量执行。
有两种常见分片策略:
固定数量分片。 每批固定1000条,最后剩余不足1000条单独执行。这是最朴素也最可靠的策略。上面给出的batchSize=1000就是这种模式。
动态字节数分片。 根据每行的字节数动态计算,比如累计到4MB就切一批。这种模式适合行宽波动大的场景,比如有些行文本字段特别长、有些行特别短。实现时估算SQL文本长度,超过阈值就执行当前批次并开启新批次。
实际项目中,基本用固定数量分片就足够了。行宽正常的情况下,1000条一批基本不会超过max_allowed_packet默认的64MB限制。除非你的字段里有大段TEXT或BLOB内容,那就需要改成动态字节数分片。
3.4 一个比较高级的解法:JSQLParser自动改写foreach为批量
如果你的Mapper里已经写死了很多单个INSERT,改造成foreach成本较高,可以了解一下JSQLParser这个工具。它能解析SQL语句,把多个单条INSERT在Java层自动改写成多VALUES形式,再重新拼接成批量SQL。相当于把"手工改造foreach"这一步自动化了。
实际使用的时候,流程大致是:读取原Mapper XML里的单条INSERT SQL,用JSQLParser解析为InsertStatement,然后把新数据的字段值依次追加到同一个InsertStatement的VALUES中,最后重新输出SQL字符串。这样业务代码不需要逐个改造,统一走批量写入。
不过这个方案引入了一个额外的开源依赖,并且在处理极其复杂的SQL(比如INSERT ... ON DUPLICATE KEY UPDATE、INSERT ... SELECT)时可能会有兼容问题。如果项目里批量插入点不多,直接写foreach更省事,没必要为了"自动化"引入额外的复杂度。如果批量插入点非常多,又不想手动改造几十个Mapper,那JSQLParser的思路值得一试。
4. MySQL端参数调优与事务切分:同样的代码,速度差十倍的原因
很多人在代码层面把批量插入做到位了,但导入速度还是上不去,问题往往出在MySQL服务端的配置上。同样的INSERT语句,在一台默认配置的MySQL上和在一台针对导入场景调优过的MySQL上,性能差距可以达到十倍甚至更多。
4.1 max_allowed_packet:批量SQL的"门卫"
这个参数前面提到了,这里展开说一下。max_allowed_packet规定了MySQL服务器和客户端之间传递单个数据包的最大大小。批量插入时,一条多VALUES的INSERT语句如果超过了这个值,直接报错。
默认值在MySQL 8.0中是64MB,看起来很大,但注意这是"单个数据包"上限,不是"单条SQL"上限。如果你的批次是5万条、每行包含几个TEXT字段,包大小轻松超过64MB。建议在配置文件里给到一个明确的保守值:
ini复制[mysqld]
max_allowed_packet = 128M
如果你用的是云数据库RDS,可以在控制台的参数组里修改,效果一样。修改完成后记得验证:SHOW VARIABLES LIKE 'max_allowed_packet';。
4.2 innodb_flush_log_at_trx_commit 与 sync_binlog:数据安全与导入速度的折中
这是导入场景中最为关键的两个参数,直接决定了每次事务提交时要付出多少IO代价。
innodb_flush_log_at_trx_commit控制InnoDB在事务提交时如何将redo log刷到磁盘:
| 取值 | 行为 | 崩溃时可能丢失的数据 | 性能 |
|---|---|---|---|
| 0 | 每秒刷一次磁盘 | 最多1秒的数据 | 最快 |
| 1 | 每次提交都刷磁盘 | 无数据丢失 | 最慢 |
| 2 | 每次提交写入操作系统缓存,每秒刷磁盘 | 操作系统崩溃时最多1秒数据 | 折中 |
默认值是1,也就是每次事务提交都强制刷盘,这是数据安全性最高的配置,但对大批量导入来说也是最拖后腿的配置。
sync_binlog控制binlog的刷盘频率。默认值同样是1,表示每提交一次事务就同步写一次binlog。如果你开了binlog,并且把这个参数保持为1,那么每个事务提交都会产生两次fsync——一次刷redo log,一次刷binlog——性能损耗非常明显。
在允许数据丢失少量、可重跑的导入场景下,推荐临时设置:
ini复制[mysqld]
innodb_flush_log_at_trx_commit = 2
sync_binlog = 1000
注意,这两个参数在生产环境的日常运行中不建议永久保持为低安全配置。如果是正式业务库,导入完成之后建议改回innodb_flush_log_at_trx_commit=1和sync_binlog=1,让数据库恢复严格的数据安全模式。
4.3 事务切分:100万条数据不要一个事务全包
很多人做批量导入,SQL写对了、参数也调了,但把所有数据都塞进一个事务,一次性commit。这样做会引发两个问题。
第一个问题是undo log膨胀。 一个事务内修改的行数过多,InnoDB需要维护大量的undo信息,用于事务回滚和MVCC读写。100万行在一个事务里,undo log可能占用数GB空间,拖慢整个事务的执行和最终的commit。
第二个问题是锁持有时间过长,容易阻塞其他会话。 大批量插入期间,相关表的行锁、间隙锁会一直持有到事务结束。如果是线上库,这个期间其他业务的读写都会被阻塞,甚至引发死锁。
所以事务切分的核心思想是:把大批量数据拆成多个小事务,每个事务只处理500到2000条,一批提交一次。这样既控制了undo log体积,又把锁持有时间缩短到秒级以内。代码实现就是在每一批executeBatch()之后紧跟一个conn.commit(),也就是我在JDBC模板里给出的写法。
我实际测试过,插入200万条数据,一个事务全包耗时220秒,分成500条一个小事务后耗时95秒,性能提升超过50%。
4.4 临时关闭外键约束和唯一性检查
导入大批量数据时,外键约束检查和唯一性索引维护也是不小的开销。如果是离线导数据,可以考虑在导入前临时关闭外键检查:
sql复制SET FOREIGN_KEY_CHECKS = 0;
-- 执行批量导入
SET FOREIGN_KEY_CHECKS = 1;
这个操作对InnoDB表有效,能省去每次INSERT时的外键校验。但前提是你的数据本身保证完整性,否则导入完需要重新检查一遍外键约束。
另外,如果表上有唯一索引,而你的数据源有重复,导入过程中会频繁触发唯一性冲突报错。建议导入前先做一次数据去重,或者使用INSERT IGNORE跳过重复记录,但要用INSERT IGNORE就得评估它对性能的影响。
5. 更大规模数据导入的方案选型与实测横向对比
如果数据量上了千万级,或者一次导入几个G的数据文件,JDBC批量插入虽然能用,但不再是最优解。这时候需要根据数据源类型、目标表状态、可接受的停机时间等因素,在多个方案之间做选择。
5.1 LOAD DATA INFILE:MySQL原生的高速导入
LOAD DATA INFILE是MySQL提供的文件导入方式,它能以极快的速度把文本文件中的数据直接加载到表中。和JDBC批量插入相比,它的优势在于绕过了SQL解析层的大部分开销,直接走MySQL内部的数据加载路径。
常用命令示例:
sql复制LOAD DATA INFILE '/tmp/user_data.csv'
INTO TABLE t_user
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(name, age, address);
实测对比,同样是插入100万行数据,JDBC批量插入需要20多秒,LOAD DATA INFILE基本可以做到5秒以内。
使用注意点:
- 文件需要放在MySQL服务器本地,如果客户端和服务器不在一台机器,需要加
LOCAL关键字:LOAD DATA LOCAL INFILE '...',但MySQL 8.0默认禁用LOCAL能力,需要在客户端和服务端都做配置。 - 如果出现文件权限或者
The used command is not allowed with this MySQL version报错,多半是local_infile参数没开。 - 导入前同样建议先关外键检查,导入速度会更快。
5.2 使用存储过程和临时表:处理复杂业务逻辑
如果你的导入不是简单的文件映射,而是需要在MySQL内部做清洗、关联、拆分,那用存储过程加临时表会更灵活。
思路是:先把原始数据load到一张临时表,再写存储过程对临时表逐条或分批处理,最后插入正式表。
sql复制-- 创建临时表
CREATE TEMPORARY TABLE tmp_user_import (
name VARCHAR(50),
age INT,
address VARCHAR(100)
);
-- 加载数据到临时表
LOAD DATA INFILE '/tmp/user_data.csv' INTO TABLE tmp_user_import
FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';
-- 存储过程处理
DELIMITER //
CREATE PROCEDURE proc_import_user()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE v_name VARCHAR(50);
DECLARE v_age INT;
DECLARE v_address VARCHAR(100);
DECLARE cur CURSOR FOR SELECT name, age, address FROM tmp_user_import;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_name, v_age, v_address;
IF done = 1 THEN
LEAVE read_loop;
END IF;
INSERT INTO t_user (name, age, address) VALUES (v_name, v_age, v_address);
END LOOP;
CLOSE cur;
END //
DELIMITER ;
CALL proc_import_user();
注意存储过程里逐行INSERT的性能并不高,如果只是简单的映射导入,优先选LOAD DATA INFILE或JDBC批量插入。存储过程适用的场景是:数据在导入过程中需要大量业务判断、关联查询、动态生成字段值,很难用纯SQL批量映射完成。
5.3 规模化导入工具:DataX、Sqoop等同步工具的选型对比
数据量跨到千万级甚至亿级时,人工写JDBC批量插入已经不太现实,这时通常会引入专门的同步工具。针对MySQL常见的选择有DataX和Sqoop。
DataX是阿里巴巴开源的离线数据同步工具。它的核心能力是把数据从各种数据源(MySQL、Oracle、HDFS、Hive等)读出来,通过框架内部的数据交换通道,写入目标端。对MySQL批量插入而言,DataX内置了批量写入的通道数量、批次大小等参数,同时支持断点续传,适合超大数据量的稳定同步。
DataX里MySQL写入端和批量插入相关的关键参数:
| 参数 | 作用 |
|---|---|
channel |
并发通道数,控制读取和写入的并行度 |
batchSize |
单次批量写入的行数 |
preSql |
写前执行的SQL,比如清空表或禁用外键检查 |
postSql |
写后执行的SQL,比如重建索引 |
一个简单示例配置(job.json):
json复制{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "root",
"password": "123456",
"connection": [
{
"jdbcUrl": ["jdbc:mysql://127.0.0.1:3306/source_db"],
"querySql": ["SELECT name, age, address FROM t_user_source"]
}
]
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"username": "root",
"password": "123456",
"writeMode": "insert",
"column": ["name", "age", "address"],
"preSql": ["SET FOREIGN_KEY_CHECKS=0"],
"postSql": ["SET FOREIGN_KEY_CHECKS=1"],
"connection": [
{
"jdbcUrl": "jdbc:mysql://127.0.0.1:3306/target_db",
"table": ["t_user"]
}
]
}
}
}
],
"setting": {
"speed": {
"channel": 4
}
}
}
}
跑起来:
bash复制python datax.py job.json
Sqoop是Apache旗下的大数据生态同步工具,主要用于Hadoop生态和关系型数据库之间的数据迁移。如果你的数据要进Hive或HDFS,Sqoop更匹配。它是通过MapReduce任务来并发执行导入的,底层对MySQL的连接方式和JDBC批量插入类似。
Sqoop导入MySQL的一个典型命令:
bash复制sqoop import \
--connect jdbc:mysql://127.0.0.1:3306/source_db \
--username root \
--password 123456 \
--table t_user \
--target-dir /user/hive/warehouse/t_user \
--fields-terminated-by '\t' \
--m 4
几个方案放在一起横向对比:
| 方案 | 适用数据量级 | 优点 | 缺点 |
|---|---|---|---|
| JDBC批量插入 | 百万级以下 | 灵活可控,代码写在业务里 | 需要自己处理分片、事务、异常 |
| MyBatis foreach | 百万级以下 | 开发效率高,SQL直观 | 受SQL长度限制,大数据量需配合分片 |
| LOAD DATA INFILE | 百万到千万级 | 速度极快,配置简单 | 文件需在服务器本地或开LOCAL,受安全限制 |
| DataX | 千万级以上 | 稳定、可断点续传、并发高 | 引入额外工具,配置复杂 |
| Sqoop | 千万到亿级 | 与Hadoop生态集成好 | 不适合MySQL到MySQL的简单同步 |
5.4 一次大表导入的真实操作链路
最后分享一个我处理过的完整案例:一个订单明细表,约800万行数据,需要从源库迁移到新库,目标表有主键索引加三个二级索引。
第一步,修改目标库参数,把innodb_flush_log_at_trx_commit改为2,sync_binlog改为1000,max_allowed_packet调整为128M。因为是离线迁移,可以接受极端情况下的少量数据丢失。
第二步,删除目标表的三个二级索引,只保留主键。这一步很关键,后面重建索引的时间远小于带着索引导数据省下的时间。
第三步,用DataX配置8个并发通道,batchSize设置2000,跑同步任务。800万行数据在8个并发下完赛时间大约12分钟。如果是单线程JDBC方式,这个量级即使做了批量插入,也要40分钟以上。
第四步,同步完成后先做数据校验,用count比对源表和目标表行数,再抽查几个关键字段。校验无误后重新创建二级索引,索引构建完成后再把数据库参数恢复为生产配置。
整个流程下来,把"导数据"这件事从"能不能跑完"变成了"可以规划时间窗口完成"。这种思路适用于任何规模的数据导入:先尽量降低MySQL端的写入成本,再把导入工具并发度和批次大小调到合适位置,最后通过索引重建和参数恢复收尾。
我个人的体会是,MySQL大批量导入本身并不复杂,但想让它在生产环境稳定高效地跑起来,需要同时关注代码层、驱动层、服务端配置和工具选型四个层面。任何一个环节不到位,都可能成为整个链路的性能短板。上面这些参数和写法,都是我在真实项目里验证过的,直接抄作业即可。如果你在配置过程中遇到报错或者效率仍然不理想,建议优先检查连接串参数有没有生效、max_allowed_packet够不够大、事务是否每批提交,这三个点覆盖了大部分导入性能问题的根因。
