本文将通过一个具体的性能测试案例,对比在 Java 应用中使用不同方式向 MySQL 数据库插入30万条数据的效率。我们将分析“梭哈式”批量插入、循环单条插入以及分批次批处理插入等多种方案的优劣,并提供经过验证的优化代码。
验证准备
首先,我们创建用于测试的数据库表 t_user,结构如下:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(64) DEFAULT NULL COMMENT '用户名称',
`age` int(4) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';
实体类、Mapper与配置文件定义
为了进行测试,我们需要准备基础的 Java 实体类、MyBatis Mapper 接口及配置文件。
1. User实体类
/**
* <p>用户实体</p>
*
* @Author zjq
*/
@Data
public class User {
private int id;
private String username;
private int age;
}
2. Mapper接口
public interface UserMapper {
/**
* 批量插入用户
* @param userList
*/
void batchInsertUser(@Param("list") List<User> userList);
}
3. Mapper XML 文件
<!-- 批量插入用户信息 -->
<insert id="batchInsertUser" parameterType="java.util.List">
insert into t_user(username,age) values
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.username},
#{item.age}
)
</foreach>
</insert>
4. JDBC 属性文件 jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=root
5. MyBatis 核心配置文件 sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--通过properties标签加载外部properties文件-->
<properties resource="jdbc.properties"></properties>
<!--自定义别名-->
<typeAliases>
<typeAlias type="com.zjq.domain.User" alias="user"></typeAlias>
</typeAliases>
<!--数据源环境-->
<environments default="developement">
<environment id="developement">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--加载映射文件-->
<mappers>
<mapper resource="com/zjq/mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
方案一:不分批次,“梭哈式”插入
首先尝试最直接的方式:一次性构建30万条数据的 List,然后通过 MyBatis 批量插入。
@Test
public void testBatchInsertUser() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession session = sqlSessionFactory.openSession();
System.out.println("===== 开始插入数据 =====");
long startTime = System.currentTimeMillis();
try {
List<User> userList = new ArrayList<>();
for (int i = 1; i <= 300000; i++) {
User user = new User();
user.setId(i);
user.setUsername("共饮一杯无 " + i);
user.setAge((int) (Math.random() * 100));
userList.add(user);
}
session.insert("batchInsertUser", userList); // 最后插入剩余的数据
session.commit();
long spendTime = System.currentTimeMillis()-startTime;
System.out.println("成功插入 30 万条数据,耗时:"+spendTime+"毫秒");
} finally {
session.close();
}
}
执行后程序报错,控制台输出如下信息:
Cause: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (27759038 > 4194304). You can change this value on the server by setting the max_allowed_packet variable.

原因是单次 SQL 语句的数据包大小超过了 MySQL 服务器默认限制(4MB)。虽然可以通过调整 max_allowed_packet 参数解决,但对于30万条数据这种量级,单次传输并不可取,容易导致内存和网络问题。因此,“梭哈”方案行不通。
方案二:循环逐条插入
既然批量不行,那退回到最基础的方法:在循环中逐条插入并立即提交。
首先,在 Mapper 中新增单条插入的方法:
void insertUser(User user);
<!-- 新增用户信息 -->
<insert id="insertUser" parameterType="user">
insert into t_user(username,age) values
(
#{username},
#{age}
)
</insert>
测试代码如下:
@Test
public void testCirculateInsertUser() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession session = sqlSessionFactory.openSession();
System.out.println("===== 开始插入数据 =====");
long startTime = System.currentTimeMillis();
try {
for (int i = 1; i <= 300000; i++) {
User user = new User();
user.setId(i);
user.setUsername("共饮一杯无 " + i);
user.setAge((int) (Math.random() * 100));
// 一条一条新增
session.insert("insertUser", user);
session.commit();
}
long spendTime = System.currentTimeMillis()-startTime;
System.out.println("成功插入 30 万条数据,耗时:"+spendTime+"毫秒");
} finally {
session.close();
}
}
执行期间,磁盘 IO 使用率持续处于高位。

插入过程非常缓慢,日志显示在频繁地提交事务。

最终,完成30万条数据插入总共耗时 14909367 毫秒,约合 4小时8分钟。


这个性能显然无法接受,必须进行优化。
方案三:MyBatis 分批次批处理插入(推荐)
正确的姿势是结合批处理与分批次提交。思路是:每积累一定数量的数据(如1000条),执行一次批量插入并提交事务,以此减少数据库交互次数和事务开销。
清空测试表后,执行以下优化代码:
TRUNCATE table t_user;
/**
* 分批次批量插入
* @throws IOException
*/
@Test
public void testBatchInsertUser() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession session = sqlSessionFactory.openSession();
System.out.println("===== 开始插入数据 =====");
long startTime = System.currentTimeMillis();
int waitTime = 10;
try {
List<User> userList = new ArrayList<>();
for (int i = 1; i <= 300000; i++) {
User user = new User();
user.setId(i);
user.setUsername("共饮一杯无 " + i);
user.setAge((int) (Math.random() * 100));
userList.add(user);
if (i % 1000 == 0) {
session.insert("batchInsertUser", userList);
// 每 1000 条数据提交一次事务
session.commit();
userList.clear();
// 等待一段时间
Thread.sleep(waitTime * 1000);
}
}
// 最后插入剩余的数据
if(!CollectionUtils.isEmpty(userList)) {
session.insert("batchInsertUser", userList);
session.commit();
}
long spendTime = System.currentTimeMillis()-startTime;
System.out.println("成功插入 30 万条数据,耗时:"+spendTime+"毫秒");
} catch (Exception e) {
e.printStackTrace();
} finally {
session.close();
}
}
这段代码每插入1000条数据就提交一次,并且每次提交后等待10秒。这有助于平缓 I/O 压力,避免瞬时资源占用过高。

最终耗时约 50分钟。时间主要消耗在每次提交后的等待上,这是一种对生产环境友好的“温和”插入方式。

如果是在系统低谷期,并且追求极致速度,可以去掉等待时间,并适当增大批次大小:
// ... 前面代码相同
for (int i = 1; i <= 300000; i++) {
User user = new User();
user.setId(i);
user.setUsername("共饮一杯无 " + i);
user.setAge((int) (Math.random() * 100));
userList.add(user);
if (i % 5000 == 0) { // 批次增大到5000
session.insert("batchInsertUser", userList);
session.commit();
userList.clear();
// 移除了 Thread.sleep
}
}
// ... 后续代码相同
将批次大小调整为5000并取消等待后,插入速度大幅提升。

仅用时 13秒 就成功插入了30万条数据!当然,此时 CPU 和磁盘会出现短暂的利用率峰值。


方案四:使用原生JDBC进行批处理
除了 MyBatis,我们也可以直接使用 JDBC 的原生批处理功能来实现高效插入。
/**
* JDBC分批次批量插入
* @throws IOException
*/
@Test
public void testJDBCBatchInsertUser() throws IOException {
Connection connection = null;
PreparedStatement preparedStatement = null;
String databaseURL = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";
try {
connection = DriverManager.getConnection(databaseURL, user, password);
// 关闭自动提交事务,改为手动提交
connection.setAutoCommit(false);
System.out.println("===== 开始插入数据 =====");
long startTime = System.currentTimeMillis();
String sqlInsert = "INSERT INTO t_user ( username, age) VALUES ( ?, ?)";
preparedStatement = connection.prepareStatement(sqlInsert);
Random random = new Random();
for (int i = 1; i <= 300000; i++) {
preparedStatement.setString(1, "共饮一杯无 " + i);
preparedStatement.setInt(2, random.nextInt(100));
// 添加到批处理中
preparedStatement.addBatch();
if (i % 1000 == 0) {
// 每1000条数据提交一次
preparedStatement.executeBatch();
connection.commit();
System.out.println("成功插入第 "+ i+" 条数据");
}
}
// 处理剩余的数据
preparedStatement.executeBatch();
connection.commit();
long spendTime = System.currentTimeMillis()-startTime;
System.out.println("成功插入 30 万条数据,耗时:"+spendTime+"毫秒");
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage());
} finally {
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
JDBC 批处理的原理是,通过 addBatch() 累积 SQL,然后通过 executeBatch() 一次性发送到数据库执行,配合手动事务提交,能极大提升效率。


总结与优化策略
通过以上对比实验,我们可以得出高效大数据量插入的核心策略:
-
批处理是关键:无论是 MyBatis 的 foreach 标签,还是 JDBC 的 addBatch/executeBatch,其核心都是减少与数据库的网络交互和事务开销。这是性能提升最有效的手段。
- 批次大小:建议设置在 1000 到 5000 之间。太小则优化效果不明显,太大可能导致内存压力或数据库数据包超限。
- 平滑插入:在生产环境,如果担心瞬时负载过高,可以在批次间添加短暂休眠(如
Thread.sleep)。
-
索引优化:如果目标表存在大量索引,在插入前临时禁用非关键索引,插入完成后再重建,可以显著提升写入速度。
-
连接与配置:
- 务必使用数据库连接池(如 HikariCP),避免频繁创建销毁连接的开销。
- 根据实际情况调整数据库参数,例如适当增加
max_allowed_packet、innodb_buffer_pool_size 等。
-
事务控制:一定要关闭自动提交,采用手动方式在批次操作后统一提交,这能避免每一条数据都产生事务日志的巨大开销。
总之,处理海量数据插入时,切忌使用简单的循环逐条插入。合理的分批次批处理,结合适当的数据库调优,才是保证性能的“正确姿势”。希望本文的实战对比能为你带来启发。如果你想探讨更多关于后端性能优化的话题,欢迎在云栈社区交流。