6

MyBatis批量插入数据优化,那叫一个优雅!

 8 months ago
source link: https://www.51cto.com/article/778107.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

MyBatis批量插入数据优化,那叫一个优雅!

作者:springboot葵花宝典 2023-12-30 20:04:51
我们使用了 mybatis-plus 框架,并采用其中的 saveBatch 方法进行批量数据插入。然而,通过深入研究源码,我发现这个方法并没有如我期望的那样高效。

在项目开发中,我们经常需要进行大量数据的批量插入操作。然而,在实际应用中,插入大量数据时性能常常成为一个瓶颈。在我最近的项目中,我发现了一些能够显著提升批量插入性能的方法,并进行了一系列实验来验证它们的有效性。

今日内容介绍,大约花费15分钟

图片

图片

我们使用了 mybatis-plus 框架,并采用其中的 saveBatch 方法进行批量数据插入。然而,通过深入研究源码,我发现这个方法并没有如我期望的那样高效

图片

图片

这是因为最终在执行的时候还是通过for循环一条条执行insert,然后再一批的进行flush ,默认批的消息为1000

图片

图片

为了找到更优秀的解决方案,我展开了一场性能优化的探索之旅。好了我们现在开始探索

  • 创建一张表tb_student
create table springboot_mp.tb_student
(
    id      bigint auto_increment comment '主键ID'
        primary key,
    stuid   varchar(40)   not null comment '学号',
    name    varchar(30)   null comment '姓名',
    age     tinyint       null comment '年龄',
    sex     tinyint(1)    null comment '性别 0 男 1 女',
    dept    varchar(2000) null comment '院系',
    address varchar(400)  null comment '家庭地址',
    constraint stuid
        unique (stuid)
);
  • 创建spring-boot-mybatis-demo项目并在pom.xml中添加依赖
图片

图片

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>


    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3</version>
    </dependency>


    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
  • application.yml配置
server:
  port: 8890

spring:
  application:
    name: mybatis-demo #指定服务名
  datasource:
    username: root
    password: root
#    url: jdbc:mysql://localhost:3306/springboot_mp?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
    url: jdbc:mysql://localhost:3306/springboot_mp?useUnicode=true&characterEncoding=utf8
    driver-class-name: com.mysql.cj.jdbc.Driver
#
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:/mapper/**/*.xml
  • 使用mybatisX生成代码
图片

图片

图片

图片

图片

图片

每次都是插入100000条数据

注意:因为我的电脑性能比较好,所以才插入这么多数据,大家可以插入1000进行实验对比

  1. 单条循环插入:传统方法的基准

首先,我采用了传统的单条循环插入方法,将每条数据逐一插入数据库,作为性能对比的基准。

/**
 * @author springboot葵花宝典
 * @description: TODO
 */
@SpringBootTest
public class MybatisTest {

    @Autowired
    private StudentService studentService;

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Test
    public void MybatisBatchSaveOneByOne(){


        SqlSession sqlSession = sqlSessionFactory.openSession();

        try {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start("mybatis plus save one");
            for (int i = 0; i < 100000; i++) {
                Student student = new Student();
                student.setStuid("6840"+i);
                student.setName("zhangsan"+i);

                student.setAge((i%100));


                if(i%2==0){
                    student.setSex(0);
                }else {
                    student.setSex(1);
                }

                student.setDept("计算机学院");
                student.setAddress("广东省广州市番禺"+i+"号");
                //一条一条插入
                studentService.save(student);
            }
            sqlSession.commit();
            stopWatch.stop();
            System.out.println("mybatis plus save one:" + stopWatch.getTotalTimeMillis());

        } finally {
            sqlSession.close();
        }

    }

}

发现花费了195569毫秒

图片图片

  1. mybatis-plus 的 saveBatch 方法

现在尝试 mybatis-plus 提供的 saveBatch 方法,期望它能够提高性能。

@Test
    public void MybatissaveBatch(){


        SqlSession sqlSession = sqlSessionFactory.openSession();

        try {

            List<Student> students = new ArrayList<>();
            StopWatch stopWatch = new StopWatch();

            stopWatch.start("mybatis plus save batch");
            for (int i = 0; i < 100000; i++) {
                Student student = new Student();
                student.setStuid("6840"+i);
                student.setName("zhangsan"+i);

                student.setAge((i%100));


                if(i%2==0){
                    student.setSex(0);
                }else {
                    student.setSex(1);
                }

                student.setDept("计算机学院");
                student.setAddress("广东省广州市番禺"+i+"号");
                //一条一条插入
                students.add(student);
            }

            studentService.saveBatch(students);
            sqlSession.commit();
            stopWatch.stop();
            System.out.println("mybatis plus save batch:" + stopWatch.getTotalTimeMillis());

        } finally {
            sqlSession.close();
        }

    }

发现花费9204毫秒,比一条条插入数据性能提高十几倍

图片

3.手动拼接 SQL:挑战传统的方式

<insert id="saveBatch2">
    insert into springboot_mp.tb_student ( stuid, name, age, sex, dept, address)
    values
    <foreach collection="students" item="stu" index="index" separator=",">
          ( #{stu.stuid}, #{stu.name}, #{stu.age}, #{stu.sex}, #{stu.dept}, #{stu.address})
    </foreach>

</insert>

发现花费10958毫秒,比一条条插入数据性能提高十几倍,但是和saveBatch性能相差不大

既然都验证都这了,我就在想,要不要使用JDBC批量插入进行验证一下,看会不会出现原始的才是最好的结果

4.JDBC 的 executeBatch 方法

尝试直接使用 JDBC 提供的 executeBatch 方法,看是否有意外的性能提升。

@Test
    public void JDBCSaveBatch() throws SQLException {


        SqlSession sqlSession = sqlSessionFactory.openSession();
        Connection connection = sqlSession.getConnection();
        connection.setAutoCommit(false);



        String sql ="insert into springboot_mp.tb_student ( stuid, name, age, sex, dept, address) values (?, ?, ?, ?, ?, ?);";
        try (PreparedStatement statement = connection.prepareStatement(sql)) {

            List<Student> students = new ArrayList<>();
            StopWatch stopWatch = new StopWatch();
            stopWatch.start("mybatis plus JDBCSaveBatch");
            for (int i = 0; i < 100000; i++) {
                statement.setString(1,"6840"+i);
                statement.setString(2,"zhangsan"+i);
                statement.setInt(3,(i%100));
                if(i%2==0){
                    statement.setInt(4,0);
                }else {
                    statement.setInt(4,1);
                }
                statement.setString(5,"计算机学院");
                statement.setString(6,"广东省广州市番禺"+i+"号");


                statement.addBatch();
            }

            statement.executeBatch();
            connection.commit();
            stopWatch.stop();
            System.out.println("mybatis plus JDBCSaveBatch:" + stopWatch.getTotalTimeMillis());

        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
        finally {

            sqlSession.close();
        }

JDBC executeBatch 的性能会好点,耗费6667毫秒

图片

但是感觉到这里以后,觉得时候还是比较长,有没有可以再进行优化的方式,然后我就在ClientPreparedStatement类中发现有一个叫做rewriteBatchedStatements 的属性,从名字来看是要重写批操作的 Statement,前面batchHasPlainStatements 已经是 false,取反肯定是 true,所以只要这参数是 true 就会进行一波操作。rewriteBatchedStatements默认是 false。

图片

图片

图片
图片

图片

大家也可以自行网上搜索一下这个神奇的属性然后我在url添加上这个属性

图片

图片

然后继续跑了下 mybatis-plus 自带的 saveBatch,果然性能大大提高直接由原来的9204毫秒,提升到现在的3903毫秒

图片图片

再来跑一下JDBC 的 executeBatch ,果然也提高了。

直接由原来的6667毫秒,提升到了3794毫秒

图片

批量保存方式

数据量(条)

耗时(ms)

单条循环插入

100000

195569

mybatis-plus saveBatch

100000

mybatis-plus saveBatch(添加 rewrite 参数)

100000

手动拼接 SQL

100000

JDBC executeBatch

100000

10958

JDBC executeBatch(添加 rewrite 参数)

100000

通过实验结果,我们可以得出以下结论:

  • mybatis-plus 的 saveBatch 方法相比单条循环插入在性能上有所提升,但仍然不够理想。
  • JDBC 的 executeBatch 方法在默认情况下性能与 mybatis-plus 的 saveBatch 类似,但通过设置 rewriteBatchedStatements 参数为 true 可显著提高性能。
  • rewriteBatchedStatements 参数的作用是将一批插入拼接成 insert into xxx values (a),(b),(c)... 这样的一条语句形式,提高了性能。

如果您在项目中需要进行批量插入操作,我建议考虑以下优化方案:

  • 如果使用 mybatis-plus,可以尝试将 JDBC 连接字符串中的 rewriteBatchedStatements 参数设置为 true,以提高 saveBatch 方法的性能。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK