找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1750

积分

0

好友

236

主题
发表于 15 小时前 | 查看: 1| 回复: 0

本文将通过一个具体的性能测试案例,对比在 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.

MyBatis批量插入数据包超限异常截图

原因是单次 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 使用率持续处于高位。
循环插入时高磁盘IO监控图

插入过程非常缓慢,日志显示在频繁地提交事务。
循环逐条插入数据库日志截图

最终,完成30万条数据插入总共耗时 14909367 毫秒,约合 4小时8分钟
循环插入30万条完成日志截图
查询验证30万条数据结果截图

这个性能显然无法接受,必须进行优化。

方案三: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 和磁盘会出现短暂的利用率峰值。
13秒插入完成时高CPU磁盘监控图
13秒快速插入代码运行截图

方案四:使用原生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() 一次性发送到数据库执行,配合手动事务提交,能极大提升效率。
JDBC批处理插入过程日志截图
JDBC批处理插入完成日志截图

总结与优化策略

通过以上对比实验,我们可以得出高效大数据量插入的核心策略:

  1. 批处理是关键:无论是 MyBatis 的 foreach 标签,还是 JDBC 的 addBatch/executeBatch,其核心都是减少与数据库的网络交互和事务开销。这是性能提升最有效的手段。

    • 批次大小:建议设置在 1000 到 5000 之间。太小则优化效果不明显,太大可能导致内存压力或数据库数据包超限。
    • 平滑插入:在生产环境,如果担心瞬时负载过高,可以在批次间添加短暂休眠(如 Thread.sleep)。
  2. 索引优化:如果目标表存在大量索引,在插入前临时禁用非关键索引,插入完成后再重建,可以显著提升写入速度。

  3. 连接与配置

    • 务必使用数据库连接池(如 HikariCP),避免频繁创建销毁连接的开销。
    • 根据实际情况调整数据库参数,例如适当增加 max_allowed_packetinnodb_buffer_pool_size 等。
  4. 事务控制:一定要关闭自动提交,采用手动方式在批次操作后统一提交,这能避免每一条数据都产生事务日志的巨大开销。

总之,处理海量数据插入时,切忌使用简单的循环逐条插入。合理的分批次批处理,结合适当的数据库调优,才是保证性能的“正确姿势”。希望本文的实战对比能为你带来启发。如果你想探讨更多关于后端性能优化的话题,欢迎在云栈社区交流。




上一篇:SpringBoot整合ShardingSphere实现MySQL分库分表实战(含配置详解)
下一篇:SkillsBench基准测试详解:Agent技能如何提升性能12.66%、设计原则与真实崩溃分析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-25 19:43 , Processed in 0.400451 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表