当业务系统遇到活动高峰、数据迁移或日志采集场景时,单表短时间内可能要写入几十万甚至上百万行数据。如果仍然采用循环执行单条INSERT语句的方式,数据库连接、事务提交和索引维护的开销会被迅速放大,写入速度可能从每秒几千行跌到几十行。

要解决MySQL并发插入大量数据的问题,核心思路是减少每次写入的固定成本,并合理控制并发度。接下来从批量写入语法、事务策略、连接管理和索引调整几个方面展开。
为什么逐条插入会拖慢大量数据写入
单条INSERT语句看似简单,但在MySQL InnoDB存储引擎下,每执行一次都要经历完整的流程:客户端发送SQL、服务端解析、打开事务、写入redo log、更新聚簇索引和二级索引、提交事务、返回确认。如果启用了innodb_flush_log_at_trx_commit=1,每次提交都会触发一次fsync,将redo log持久化到磁盘。这个操作能保证数据不丢失,但代价是磁盘同步延迟。
当循环插入1万行数据时,就会产生1万次网络往返和最多1万次fsync。即使网络延迟只有0.1毫秒,仅往返总耗时就可以达到1秒以上,再加上磁盘随机写入和索引页分裂,整体耗时会成倍增长。更严重的是,如果多个线程同时进行逐条插入,还会在自增主键、唯一键和间隙锁上产生竞争,进一步降低吞吐量。
-- 逐条插入示例:每行都需要单独提交 INSERT INTO user_log (user_id, action, created_at) VALUES (1001, 'login', NOW()); INSERT INTO user_log (user_id, action, created_at) VALUES (1002, 'view', NOW()); INSERT INTO user_log (user_id, action, created_at) VALUES (1003, 'click', NOW()); -- 重复执行上千次,性能极差
因此,优化的方向不是单纯增加并发线程数,而是先降低单次写入的固定开销。下面的批量写入方法可以显著改善这一问题。
多值INSERT与LOAD DATA:从批量到文件导入
多值INSERT是MySQL提供的原生批量语法,一条语句可以携带多行数据。例如:
INSERT INTO user_log (user_id, action, created_at) VALUES (1001, 'login', NOW()), (1002, 'view', NOW()), (1003, 'click', NOW()), (1004, 'submit', NOW());
这种方式把多次网络往返压缩成一次,服务端也只需要执行一次解析和事务提交。对于500到2000行左右的批量,通常可以获得数倍到数十倍的性能提升。批量大小不是越大越好,过大的SQL会占用过多内存,并且可能超过max_allowed_packet参数的限制。该参数默认通常是4MB或64MB,如果批量语句超过限制,连接会被直接断开。可以通过以下命令查看和调整:
-- 查看当前限制 SHOW VARIABLES LIKE 'max_allowed_packet'; -- 调整会话级别为128MB SET SESSION max_allowed_packet = 134217728;
如果数据已经存放在文本文件中,LOAD DATA INFILE是更高效的方案。它绕过SQL解析层,直接将文件内容批量载入表中,速度通常比多值INSERT还要快。典型的用法如下:
LOAD DATA INFILE '/var/lib/mysql-files/user_log.csv' INTO TABLE user_log FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' (user_id, action, created_at);
需要注意的是,使用LOAD DATA要求文件位于MySQL服务器可访问的路径,并且用户需要FILE权限。如果客户端和服务器分离,可以使用LOAD DATA LOCAL INFILE让客户端读取本地文件,但需要在连接串中开启allowLoadLocalInfile。无论选择多值INSERT还是LOAD DATA,目的都是减少事务提交次数和网络传输次数。
并发写入的进阶优化:分批事务、连接池与索引策略
即使使用了多值INSERT,如果每执行一批就提交一次,在高并发下仍会产生大量事务提交。更好的做法是在一个事务内连续执行多个批量INSERT,最后统一提交。例如在Java中使用JDBC时,可以关闭自动提交,手动提交:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO user_log (user_id, action, created_at) VALUES (?, ?, NOW())"
);
for (int i = 0; i < 50000; i++) {
ps.setInt(1, userIds[i]);
ps.setString(2, actions[i]);
ps.addBatch();
if ((i + 1) % 1000 == 0) {
ps.executeBatch();
ps.clearBatch();
}
}
conn.commit();
ps.close();
conn.close();这样可以把数千行数据的提交合并成一次事务,redo log刷盘次数大幅下降。不过要注意事务过大也会带来长事务风险,例如占用undo log、阻塞其他查询。通常建议每500到2000行提交一次,或在业务允许的情况下每次提交的总数据量控制在几MB以内。
连接池的配置同样关键。频繁创建和销毁数据库连接的成本很高,使用HikariCP、Druid等连接池可以让并发写入线程复用已有连接。一般设置最大连接数为CPU核心数的2到4倍即可,太多连接反而会造成锁竞争和上下文切换开销。对于MySQL 8.0,可以配合使用批处理参数rewriteBatchedStatements=true,让JDBC驱动将多条单行INSERT重写成多值INSERT,进一步提升性能。
索引是另一个容易被忽略的因素。每插入一行数据,所有二级索引都需要同步更新。如果目标表上有大量二级索引,写入速度会明显下降。在数据导入或批量写入前,可以考虑先删除非必要的二级索引,写入完成后再重新创建。对于唯一索引,可以使用ALTER TABLE ... DISABLE KEYS(MyISAM)或对于InnoDB在导入前SET unique_checks=0,但要注意这可能导致重复数据被写入,仅适用于确认数据无唯一冲突的场景。
常见参数调优与问题排查
除了应用层策略,MySQL服务器参数也会影响批量写入性能。innodb_buffer_pool_size足够大时,可以缓存更多索引页和数据页,减少磁盘随机读写。innodb_log_file_size和innodb_log_buffer_size关系到redo log的写入效率,适当增大可以在一定程度上缓解刷盘压力。对于纯写入场景,可以设置innodb_flush_log_at_trx_commit=2或0来降低刷盘频率,但会牺牲一定的数据持久性,适合允许少量丢失的日志类数据。
批量写入时如果遇到报错,先查看max_allowed_packet是否足够,再检查是否有唯一键冲突导致部分批次失败。大事务导致的死锁也是常见问题,可以通过SHOW ENGINE INNODB STATUS查看最近死锁信息。如果写入速度突然下降,可以观察磁盘IO util和慢查询日志,确认是磁盘瓶颈还是锁等待。
总结起来,处理MySQL并发插入大量数据应当遵循从批量化到减少事务提交再到优化索引的路径。多值INSERT和LOAD DATA解决单条写入开销过大的问题,分批事务和连接池降低并发下的固定成本,合理的索引与参数配置则保证写入通道顺畅。根据实际数据量和业务可容忍的延迟,可以组合使用这些技巧,将写入吞吐提升一个数量级。