SpringBoot整合Mybatis批量插入方式对比

一、前言

Mybatis批量插入的正确姿势到底是什么?在网上浏览了非常多的帖子,很多都是复制粘贴来的,内容基本都是在误导别人,几乎没有测试验证,如果照做的话,性能反而相对于单条几乎没有任何提升,实践才是检验真理的唯一标准。参差不齐的网络文章真的很让人生气,完全是对技术的不负责任。


二、正文

1.准备项目和测试数据表

SpringBoot项目的基本目录如下:
不再详细的介绍搭建项目了,项目的搭建要求是能够通过Mybatis对mysql数据库进行CRUD操作即可,其他功能不需要具备。主要重心放在批量插入方式的讨论与插入性能的对比测试。
在这里插入图片描述
application.yml:

server:
  port: 20100

spring:
  application:
    name: demo
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/test?useSSL=false&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root

mybatis:
  mybatis.type-aliases-package: com.example.demo.entity
  mapper-locations: classpath:/mapper/**/*.xml
#  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

User实体类:

package com.example.demo.entity;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * @Author: zongshaofeng
 * @Description:
 * @Date:Create:in 2021/9/12 11:52
 * @Modified By:
 */
@Data
public class User implements Serializable {
	
	private Integer id;
	private Date time;
	private String name;
	private String content;
}

UserMapper:

package com.example.demo.dao;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @Author: zongshaofeng
 * @Description:
 * @Date:Create:in 2021/9/12 11:55
 * @Modified By:
 */
@Mapper
@Repository
public interface UserMapper {
	List<User> listUser();
	
	Boolean insertUser(@Param("user")User user);
	
	Boolean insertUserList(@Param("userList")List<User> userList);
	
}

UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserMapper">
    <insert id="insertUser">
        insert into user (time,name,content)
        values
        (
            #{user.time},#{user.name},#{user.content}
        )
    </insert>
    <insert id="insertUserList">
        insert into user (time,name,content)
        values
        <foreach collection="userList" item="user" index="index" separator=",">
            (#{user.time},#{user.name},#{user.content})
        </foreach>
    </insert>

    <select id="listUser" resultType="com.example.demo.entity.User">
        select * from user
    </select>
</mapper>

数据表建表语句:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `time` datetime DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `content` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8mb4;

2. 普通for循环,单条插入user表

这个是使用for循环,循环单条插入数据库,每一次循环伴随SqlSession的打开和关闭都是一个单独的事务提交,性能最差,这里使用这种方式来作为对比标准,看看批量的方式能够提高多少性能。

在测试类中编写测试方法如下:

@Test
	void insertForUser() {
		Instant start=Instant.now();
		try {
			int count = 10000;
			for (int i = 0; i < count; i++) {
				User user = new User();
				user.setTime(new Date());
				user.setName("单条插入" + i);
				user.setContent("这是通过for循环单条插入的一条记录");
				userMapper.insertUser(user);
			}
		} catch (Exception exception) {
			exception.printStackTrace();
		}
		System.out.println("***********************for循环单条写入总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
	}

3. foreach,批量插入user表

这个是使用mapper.xml文件中foreach循环,批量插入数据库,其本质就是将所有的数据组成一条sql语句,在一次操作中发送给数据库进行执行。但是你以为这种方式就最佳实践了吗?sql语句的大小数据库是有限制的,mysql限制为1M啊,下面测试时会提到,如果一口气插入10万条数据试一下,不用10万,5万都会抛出语句过大异常。

在测试类中编写测试方法如下:

@Test
	void insertForeachUser() {
		Instant start=Instant.now();
		try {
			int count =10000;
			List<User> userList=new ArrayList<>();
			for (int i = 0; i < count; i++) {
				User user = new User();
				user.setTime(new Date());
				user.setName("批量插入" + i);
				user.setContent("这是通过foreach批量插入的一条记录");
				userList.add(user);
			}
			userMapper.insertUserList(userList);
		} catch (Exception exception) {
			exception.printStackTrace();
		}
		System.out.println("***********************foreach批量写入总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
	}

4. ExecutorType.BATCH,批量插入user表

Mybatis内置的ExecutorType有3种,默认的是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优。
上面这段话我复制的,所有帖子都会写这句话,真的性能将更优吗???放开Mybatis日志输出,可以发现batch模式下确实重复使用已经预处理的语句,只是将动态更新parameters,在最终的commit之前数据库中确实没有插入数据,但是性能与for循环一次次插入几乎没有变化,下面我都将进行测试,稍安勿躁。

在测试类中编写测试方法如下:
再次声明,下面的这个测试方法,是网上非常多的帖子给出的大幅度提高插入性能的batch方式采用的方法,我严重怀疑都是复制粘贴的。我类比过来的几乎没改动,等会测一测试试嘛!

@Test
	void insertBatchUserOne() {
		Instant start=Instant.now();
		SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
		UserMapper mapper = sqlSession.getMapper(UserMapper.class);
		try {
			int count = 100000;
			for (int i = 0; i < count; i++) {
				User user = new User();
				user.setTime(new Date());
				user.setName("批量插入" + i);
				user.setContent("这是通过batch批量插入的一条记录");
				mapper.insertUser(user);
				if (i % 1000 == 0 || i == count - 1) {
					sqlSession.commit();
					sqlSession.clearCache();
				}
			}
		} catch (Exception exception) {
			sqlSession.rollback();
		} finally {
			sqlSession.close();
		}
		System.out.println("***********************batch总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
	}

5. ExecutorType.BATCH,再结合foreach方式,批量插入user表

就是将3和4集合起来而已。

在测试类中编写测试方法如下:

@Test
	void insertBatchUserTwo() {
		Instant start=Instant.now();
		SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
		UserMapper mapper = sqlSession.getMapper(UserMapper.class);
		try {
			int count = 10000;
			List<User> userList=new ArrayList<>();
			for (int i = 0; i < count; i++) {
				User user = new User();
				user.setTime(new Date());
				user.setName("批量插入" + i);
				user.setContent("这是通过batch+foreach批量插入的一条记录");
				userList.add(user);
				if (i % 1000 == 0 || i == count - 1) {
					mapper.insertUserList(userList);
					sqlSession.commit();
					sqlSession.clearCache();
					userList.clear();
				}
			}
		} catch (Exception exception) {
			sqlSession.rollback();
		} finally {
			sqlSession.close();
		}
		System.out.println("***********************batch+foreach总共用时:"+ ChronoUnit.MILLIS.between(start,Instant.now()) +"毫秒***********************************");
	}

6. 运行测试,横向对比结果:

为了保证测试结果的相对准确性,同一种方法不同数据量的测试依次执行,比如for循环,先插入1000,记录时间,再次运行插入10000,记录时间,最终走完所有的情况后,数据库表中数据为111000条数据。然后在测试下一种方式时,首先truncate user,将表数据清零。

插入数据量for循环单条插入foreach批量插入单独batch批量插入batch+foreach批量插入
1000(1千)1498毫秒445毫秒976毫秒434毫秒
10000(1万)8390毫秒860毫秒6853毫秒810毫秒
100000(10万)67457毫秒Exception:Packet for query is too large57985毫秒4052毫秒
1000000(100万)652247毫秒测试无意义574642毫秒21676毫秒

三、总结

看到上述的测试结果,不言自明了吧,像网上大多数帖子说的那样,仅仅使用batch,性能比for循环一条条插入快不到哪里去,验证了那一句,不服跑个分?

单纯的foreach拼装sql语句有大小限制。

batch可以解决sql语句大小限制的问题,但就像阴晴圆圈、悲欢离合,一切事物都有两面性,batch也有其自身的缺点,这时候就有了那一句:不存在的完美,适合自己才是最好的。

最后,本文并没有描述任何理论的东西,如果感兴趣,请继续查阅资料学习吧。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是小宗啊?

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值