当前位置:   article > 正文

mysql 批量插入_mysql batchinsert

mysql batchinsert

最近新的项目写了不少各种 insertBatch 的代码,一直有人说,批量插入比循环插入效率高很多,那本文就来实验一下,到底是不是真的?

测试环境:

  • SpringBoot 2.5
  • Mysql 8
  • JDK 8
  • Docker

首先,多条数据的插入,可选的方案:

  • foreach循环插入
  • 拼接sql,一次执行
  • 使用批处理功能插入

搭建测试环境`

sql文件:

  1. drop database IF EXISTS test;
  2. CREATE DATABASE test;
  3. use test;
  4. DROP TABLE IF EXISTS `user`;
  5. CREATE TABLE `user` (
  6. `id` int(11) NOT NULL,
  7. `name` varchar(255) DEFAULT "",
  8. `age` int(11) DEFAULT 0,
  9. PRIMARY KEY (`id`)
  10. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  11. 复制代码

应用的配置文件:

  1. server:
  2. port: 8081
  3. spring:
  4. #数据库连接配置
  5. datasource:
  6. driver-class-name: com.mysql.cj.jdbc.Driver
  7. url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true
  8. username: root
  9. password: 123456
  10. #mybatis的相关配置
  11. mybatis:
  12. #mapper配置文件
  13. mapper-locations: classpath:mapper/*.xml
  14. type-aliases-package: com.aphysia.spingbootdemo.model
  15. #开启驼峰命名
  16. configuration:
  17. map-underscore-to-camel-case: true
  18. logging:
  19. level:
  20. root: error
  21. 复制代码

启动文件,配置了Mapper文件扫描的路径:

  1. import org.mybatis.spring.annotation.MapperScan;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. @SpringBootApplication
  6. @MapperScan("com.aphysia.springdemo.mapper")
  7. public class SpringdemoApplication {
  8. public static void main(String[] args) {
  9. SpringApplication.run(SpringdemoApplication.class, args);
  10. }
  11. }
  12. 复制代码

Mapper文件一共准备了几个方法,插入单个对象,删除所有对象,拼接插入多个对象:

  1. import com.aphysia.springdemo.model.User;
  2. import org.apache.ibatis.annotations.Param;
  3. import java.util.List;
  4. public interface UserMapper {
  5. int insertUser(User user);
  6. int deleteAllUsers();
  7. int insertBatch(@Param("users") List<User>users);
  8. }
  9. 复制代码

Mapper.xml文件如下:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
  3. <mapper namespace="com.aphysia.springdemo.mapper.UserMapper">
  4. <insert id="insertUser" parameterType="com.aphysia.springdemo.model.User">
  5. insert into user(id,age) values(#{id},#{age})
  6. </insert>
  7. <delete id="deleteAllUsers">
  8. delete from user where id>0;
  9. </delete>
  10. <insert id="insertBatch" parameterType="java.util.List">
  11. insert into user(id,age) VALUES
  12. <foreach collection="users" item="model" index="index" separator=",">
  13. (#{model.id}, #{model.age})
  14. </foreach>
  15. </insert>
  16. </mapper>
  17. 复制代码

测试的时候,每次操作我们都删除掉所有的数据,保证测试的客观,不受之前的数据影响。

不同的测试

1. foreach 插入

先获取列表,然后每一条数据都执行一次数据库操作,插入数据:

  1. @SpringBootTest
  2. @MapperScan("com.aphysia.springdemo.mapper")
  3. class SpringdemoApplicationTests {
  4. @Autowired
  5. SqlSessionFactory sqlSessionFactory;
  6. @Resource
  7. UserMapper userMapper;
  8. static int num = 100000;
  9. static int id = 1;
  10. @Test
  11. void insertForEachTest() {
  12. List<User> users = getRandomUsers();
  13. long start = System.currentTimeMillis();
  14. for (int i = 0; i < users.size(); i++) {
  15. userMapper.insertUser(users.get(i));
  16. }
  17. long end = System.currentTimeMillis();
  18. System.out.println("time:" + (end - start));
  19. }
  20. }
  21. 复制代码

2. 拼接sql插入

其实就是用以下的方式插入数据:

  1. INSERT INTO `user` (`id`, `age`)
  2. VALUES (1, 11),
  3. (2, 12),
  4. (3, 13),
  5. (4, 14),
  6. (5, 15);
  7. 复制代码
  1. @Test
  2. void insertSplicingTest() {
  3. List<User> users = getRandomUsers();
  4. long start = System.currentTimeMillis();
  5. userMapper.insertBatch(users);
  6. long end = System.currentTimeMillis();
  7. System.out.println("time:" + (end - start));
  8. }
  9. 复制代码

3. 使用Batch批量插入

MyBatis sessionexecutor type 设为 Batch ,使用sqlSessionFactory将执行方式置为批量,自动提交置为false,全部插入之后,再一次性提交:

  1. @Test
  2. public void insertBatch(){
  3. SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
  4. UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  5. List<User> users = getRandomUsers();
  6. long start = System.currentTimeMillis();
  7. for(int i=0;i<users.size();i++){
  8. mapper.insertUser(users.get(i));
  9. }
  10. sqlSession.commit();
  11. sqlSession.close();
  12. long end = System.currentTimeMillis();
  13. System.out.println("time:" + (end - start));
  14. }
  15. 复制代码

4. 批量处理+分批提交

批处理的基础上,每1000条数据,先提交一下,也就是分批提交。

  1. @Test
  2. public void insertBatchForEachTest(){
  3. SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
  4. UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  5. List<User> users = getRandomUsers();
  6. long start = System.currentTimeMillis();
  7. for(int i=0;i<users.size();i++){
  8. mapper.insertUser(users.get(i));
  9. if (i % 1000 == 0 || i == num - 1) {
  10. sqlSession.commit();
  11. sqlSession.clearCache();
  12. }
  13. }
  14. sqlSession.close();
  15. long end = System.currentTimeMillis();
  16. System.out.println("time:" + (end - start));
  17. }
  18. 复制代码

初次结果,明显不对?

运行上面的代码,我们可以得到下面的结果,for循环插入的效率确实很差,拼接的sql效率相对高一点,看到有些资料说拼接sql可能会被mysql限制,但是我执行到1000w的时候,才看到堆内存溢出

下面是不正确的结果!!!

插入方式1010010001w10w100w1000w
for循环插入3871150790770026635984太久了...太久了...
拼接sql插入308320392838315624948OutOfMemoryError: 堆内存溢出
批处理392917544251647470666太久了...太久了...
批处理 + 分批提交359893527550270472462太久了...太久了...

拼接sql并没有超过内存

我们看一下mysql的限制:

  1. mysql> show VARIABLES like '%max_allowed_packet%';
  2. +---------------------------+------------+
  3. | Variable_name | Value |
  4. +---------------------------+------------+
  5. | max_allowed_packet | 67108864 |
  6. | mysqlx_max_allowed_packet | 67108864 |
  7. | slave_max_allowed_packet | 1073741824 |
  8. +---------------------------+------------+
  9. 3 rows in set (0.12 sec)
  10. 复制代码

67108864足足600多M,太大了,怪不得不会报错,那我们去改改一下它吧,改完重新测试:

  1. 首先在启动mysql的情况下,进入容器内,也可以直接在Docker桌面版直接点Cli图标进入:
  1. docker exec -it mysql bash
  2. 复制代码
  1. 进入/etc/mysql目录,去修改my.cnf文件:
  1. cd /etc/mysql
  2. 复制代码
  1. 先按照vim,要不编辑不了文件:
  1. apt-get update
  2. apt-get install vim
  3. 复制代码
  1. 修改my.cnf
  1. vim my.cnf
  2. 复制代码
  1. 在最后一行添加max_allowed_packet=20M(按i编辑,编辑完按esc,输入:wq退出)
  1. [mysqld]
  2. pid-file = /var/run/mysqld/mysqld.pid
  3. socket = /var/run/mysqld/mysqld.sock
  4. datadir = /var/lib/mysql
  5. secure-file-priv= NULL
  6. # Disabling symbolic-links is recommended to prevent assorted security risks
  7. symbolic-links=0
  8. # Custom config should go here
  9. !includedir /etc/mysql/conf.d/
  10. max_allowed_packet=2M
  11. 复制代码
  1. 退出容器
  1. # exit
  2. 复制代码
  1. 查看mysql容器id
  1. docker ps -a
  2. 复制代码

image-20211130005909539

  1. 重启mysql
  1. docker restart c178e8998e68
  2. 复制代码

重启成功后查看最大的max_allowed_pactet,发现已经修改成功:

  1. mysql> show VARIABLES like '%max_allowed_packet%';
  2. +---------------------------+------------+
  3. | Variable_name | Value |
  4. +---------------------------+------------+
  5. | max_allowed_packet | 2097152 |
  6. | mysqlx_max_allowed_packet | 67108864 |
  7. | slave_max_allowed_packet | 1073741824 |
  8. +---------------------------+------------+
  9. 复制代码

我们再次执行拼接sql,发现100w的时候,sql就达到了3.6M左右,超过了我们设置的2M,成功的演示抛出了错误:

  1. org.springframework.dao.TransientDataAccessResourceException:
  2. ### Cause: com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.
  3. ; Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.; nested exception is com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.
  4. 复制代码

批量处理为什么这么慢?

但是,仔细一看就会发现,上面的方式,怎么批处理的时候,并没有展示出优势了,和for循环没有什么区别?这是对的么?

这肯定是不对的,从官方文档中,我们可以看到它会批量更新,不会每次去创建预处理语句,理论是更快的。

image-20211130011820487

然后我发现我的一个最重要的问题:数据库连接 URL 地址少了rewriteBatchedStatements=true

如果我们不写,MySQL JDBC 驱动在默认情况下会忽视 executeBatch() 语句,我们期望批量执行的一组 sql 语句拆散,但是执行的时候是一条一条地发给 MySQL 数据库,实际上是单条插入,直接造成较低的性能。我说怎么性能和循环去插入数据差不多。

只有将 rewriteBatchedStatements 参数置为 true, 数据库驱动才会帮我们批量执行 SQL

正确的数据库连接:

  1. jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true&&&rewriteBatchedStatements=true
  2. 复制代码

找到问题之后,我们重新测试批量测试,最终的结果如下:

插入方式1010010001w10w100w1000w
for循环插入3871150790770026635984太久了...太久了...
拼接sql插入308320392838315624948(很可能超过sql长度限制)OutOfMemoryError: 堆内存溢出
批处理(重点)33332336263616388978OutOfMemoryError: 堆内存溢出
批处理 + 分批提交359313394630290718631OutOfMemoryError: 堆内存溢出

从上面的结果来看,确实批处理是要快很多的,当数量级太大的时候,其实都会超过内存溢出的,批处理加上分批提交并没有变快,和批处理差不多,反而变慢了,提交太多次了,拼接sql的方案在数量比较少的时候其实和批处理相差不大,最差的方案就是for循环插入数据,这真的特别的耗时。100条的时候就已经需要1s了,不能选择这种方案。


作者:秦怀杂货店
链接:https://juejin.cn/post/7049143701590769678
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/编程先知/article/detail/61834
推荐阅读
相关标签
  

闽ICP备14008679号