mybatis批量插入数据三种方式性能对比

批处理数据主要有三种方式:

  1. 反复执行单条插入语句
  2. foreach 拼接 sql
  3. 批处理

一、前期准备

基于Spring Boot + Mysql

1.1 表结构

id 使用数据库自增。


DROP TABLE IF EXISTS `user_info_batch`;
CREATE TABLE `user_info_batch` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_name` varchar(100) NOT NULL COMMENT '账户名称',
`pass_word` varchar(100) NOT NULL COMMENT '登录密码',
`nick_name` varchar(30) NOT NULL COMMENT '昵称',
`mobile` varchar(30) NOT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱地址',
`gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_update` timestamp NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT 'Mybatis Batch';

1.2 项目配置文件

细心的你可能已经发现,数据库url 后面跟了一段 rewriteBatchedStatements=true,有什么用呢?先不急,后面会介绍。

# 数据库配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis?rewriteBatchedStatements=true
    username: mybatis
    password: password
    driver-class-name: com.mysql.jdbc.Driver
# mybatis
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.dcits.entity

1.3 实体类

package com.dcits.entity;

import java.io.Serializable;
import java.time.LocalDateTime;

public class UserInfoBatchDO implements Serializable {

    private Long id;


    private String userName;


    private String passWord;


    private String nickName;


    private String mobile;


    private String email;

    private LocalDateTime gmtCreate;


    private LocalDateTime gmtUpdate;

    public UserInfoBatchDO() {
    }

    public UserInfoBatchDO(Long id, String userName, String passWord, String nickName, String mobile, String email, LocalDateTime gmtCreate, LocalDateTime gmtUpdate) {
        this.id = id;
        this.userName = userName;
        this.passWord = passWord;
        this.nickName = nickName;
        this.mobile = mobile;
        this.email = email;
        this.gmtCreate = gmtCreate;
        this.gmtUpdate = gmtUpdate;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public LocalDateTime getGmtCreate() {
        return gmtCreate;
    }

    public void setGmtCreate(LocalDateTime gmtCreate) {
        this.gmtCreate = gmtCreate;
    }

    public LocalDateTime getGmtUpdate() {
        return gmtUpdate;
    }

    public void setGmtUpdate(LocalDateTime gmtUpdate) {
        this.gmtUpdate = gmtUpdate;
    }

    @Override
    public String toString() {
        return "UserInfoBatchDO{" +
                "id=" + id +
                ", userName='" + userName + '\'' +
                ", passWord='" + passWord + '\'' +
                ", nickName='" + nickName + '\'' +
                ", mobile='" + mobile + '\'' +
                ", email='" + email + '\'' +
                ", gmtCreate=" + gmtCreate +
                ", gmtUpdate=" + gmtUpdate +
                '}';
    }
}

1.4 UserInfoBatchMapper

package com.dcits.mapper;

import com.dcits.entity.UserInfoBatchDO;

import java.util.List;

public interface UserInfoBatchMapper {

    /** 单条插入
     * @param info
     * @return
     */
    int insert(UserInfoBatchDO info);


    /**
     * foreach 插入
     * @param list
     * @return
     */
    int batchInsert(List<UserInfoBatchDO> list);
}

1.5 UserInfoBatchMapper.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.dcits.mapper.UserInfoBatchMapper">


    <insert id="insert" parameterType="com.dcits.entity.UserInfoBatchDO">
        insert into user_info_batch (user_name,
                                     pass_word,
                                     nick_name,
                                     mobile,
                                     email,
                                     gmt_create,
                                     gmt_update
                                    )
        values (#{userName,jdbcType=VARCHAR},
                #{passWord,jdbcType=VARCHAR},
                #{nickName,jdbcType=VARCHAR},
                #{mobile,jdbcType=VARCHAR},
                #{email,jdbcType=VARCHAR},
                #{gmtCreate,jdbcType=TIMESTAMP},
                #{gmtUpdate,jdbcType=TIMESTAMP}
               )
    </insert>


    <insert id="batchInsert">
        insert into user_info_batch (
                                    user_name,
                                    pass_word,
                                    nick_name,
                                    mobile,
                                    email,
                                    gmt_create,
                                    gmt_update
                                    )
        values
        <foreach collection="list" item="item" separator=",">
            (
            #{item.userName,jdbcType=VARCHAR},
            #{item.passWord,jdbcType=VARCHAR},
            #{item.nickName,jdbcType=VARCHAR},
            #{item.mobile,jdbcType=VARCHAR},
            #{item.email,jdbcType=VARCHAR},
            #{item.gmtCreate,jdbcType=TIMESTAMP},
            #{item.gmtUpdate,jdbcType=TIMESTAMP}
            )
        </foreach>
    </insert>
</mapper>

1.6 预备数据

为了方便测试,抽离了几个变量,并进行提前加载。

    private List<UserInfoBatchDO> list = new ArrayList<>();
    private List<UserInfoBatchDO> lessList = new ArrayList<>();
    private List<UserInfoBatchDO> lageList = new ArrayList<>();
    private List<UserInfoBatchDO> warmList = new ArrayList<>();
    // 计数工具
    private StopWatch sw = new StopWatch();
  • 为了方便组装数据,抽出了一个公共方法。
    //公共方法,组装数据
    private List<UserInfoBatchDO> assemblyData(int count){
        List<UserInfoBatchDO> list = new ArrayList<>();
        UserInfoBatchDO userInfoDO;
        for (int i = 0;i < count;i++){
            userInfoDO = new UserInfoBatchDO();
            userInfoDO.setUserName("Van");
            userInfoDO.setNickName("风尘博客");
            userInfoDO.setMobile("17098705205");
            userInfoDO.setEmail("123456789@qq.com");
            userInfoDO.setPassWord("password");
            userInfoDO.setGmtUpdate(LocalDateTime.now());
            list.add(userInfoDO);
        }
        return list;
    }
  • 预热数据
  @Before
    public void assemblyData() {
        list = assemblyData(200000);
        lessList = assemblyData(2000);
        lageList = assemblyData(1000000);
        warmList = assemblyData(5);
    }

二、反复执行单条插入语句

可能‘懒’的程序员会这么做,很简单,直接在原先单条insert语句上嵌套一个for循环。

2.1 对应 mapper 接口

int insert(UserInfoBatchDO info);

2.2 测试方法

因为这种方法太慢,所以数据降低到 2000 条。

    @Test
    public void insert() {
        logger.info("【程序热身】");
        for (UserInfoBatchDO userInfoBatchDO : warmList) {//5
            userInfoBatchMapper.insert(userInfoBatchDO);
        }
        logger.info("【热身结束】");
        sw.start("反复执行单条插入语句");
        // 这里插入 20w 条太慢了,所以我只插入了 2000 条
        for (UserInfoBatchDO userInfoBatchDO : lessList) {//2000
            userInfoBatchMapper.insert(userInfoBatchDO);
        }
        sw.stop();
        logger.info("all cost info:{}",sw.prettyPrint());
    }

2.3 执行时间

  • 第一次
    在这里插入图片描述

  • 第二次

在这里插入图片描述

  • 第三次

在这里插入图片描述
该方式插入2000 条数据,执行三次的平均时间:87021539698。

三、foreach 拼接SQL

3.1 对应mapper 接口

int batchInsert(List<UserInfoBatchDO> list);

3.2 测试方法

 @Test
    public void batchInsert() {
        logger.info("【程序热身】 批量方式之一 foreach ");
        for (UserInfoBatchDO userInfoBatchDO : warmList) {//5
            userInfoBatchMapper.insert(userInfoBatchDO);
        }
        logger.info("【热身结束】 批量方式之一 foreach ");
        sw.start("foreach 拼接 sql");
        userInfoBatchMapper.batchInsert(list);//200000
        sw.stop();
        logger.info("all cost info:{}",sw.prettyPrint());
    }

3.3 执行时间

  • 第一次

在这里插入图片描述

  • 第二次

在这里插入图片描述

  • 第三次

在这里插入图片描述
该方式插入20w 条数据,执行三次的平均时间:25154730120。

四、批处理

该方式 mapper 和xml 复用了 2.1。

4.1 rewriteBatchedStatements 参数

我在测试一开始,发现改成 Mybatis Batch提交的方法都不起作用,实际上在插入的时候仍然是一条条记录的插,而且速度远不如原来
foreach 拼接SQL的方法,这是非常不科学的。

后来才发现要批量执行的话,连接URL字符串中需要新增一个参数:rewriteBatchedStatements=true

  • rewriteBatchedStatements参数介绍

MySql的JDBC连接的url中要加rewriteBatchedStatements参数,并保证5.1.13以上版本的驱动,才能实现高性能的批量插入。MySql JDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySql数据库,批量插入实际上是单条插入,直接造成较低的性能。只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL。这个选项对INSERT/UPDATE/DELETE都有效。

4.2 批处理准备

  • 手动注入 SqlSessionFactory
  @Resource
    private SqlSessionFactory sqlSessionFactory;
  • 测试代码
    @Test
    public void processInsert() {
        logger.info("【程序热身】 批量方式之二 foreach");
        for (UserInfoBatchDO userInfoBatchDO : warmList) {
            userInfoBatchMapper.insert(userInfoBatchDO);
        }
        logger.info("【热身结束】 批量方式之二 foreach");
        sw.start("批处理执行 插入");
        // 打开批处理
        SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
        UserInfoBatchMapper mapper = session.getMapper(UserInfoBatchMapper.class);
        for (int i = 0,length = list.size(); i < length; i++) {
            mapper.insert(list.get(i));
            //每20000条提交一次防止内存溢出
            if(i%20000==19999){
                session.commit();
                session.clearCache();
            }
        }

        session.commit();
        session.clearCache();
        sw.stop();
        logger.info("all cost info:{}",sw.prettyPrint());
    }

4.3 执行时间

  • 第一次
    在这里插入图片描述
  • 第二次
    在这里插入图片描述
  • 第三次在这里插入图片描述

该方式插入20w 条数据,执行三次的平均时间:12656499721。

4.4 如果数据更大

当我把数据扩大到 100w 时,foreach 拼接 sql 的方式已经无法完成插入了,所以我只能测试批处理的插入时间。

测试时,仅需将 【4.2】测试代码中的 list 切成 lageList 测试即可。

  • 第一次
    在这里插入图片描述

  • 第二次

在这里插入图片描述

  • 第三次
    在这里插入图片描述
    该方式插入100w 条数据,执行三次的平均时间:43709821640。

五、总结

在这里插入图片描述

  1. 循环插入单条数据虽然效率极低,但是代码量极少,数据量较小时可以使用,但是数据量较大禁止使用,效率太低了;
  2. foreach 拼接sql的方式,使用时有大段的xml和sql语句要写,很容易出错,虽然效率尚可,但是真正应对大量数据的时候,依旧无法使用,所以不推荐使用;
  3. 批处理执行是有大数据量插入时推荐的做法,使用起来也比较方便。

源代码:

https://download.csdn.net/download/lwh_zfj/26362909

参考网址

https://www.toutiao.com/a7010611732557070861/?log_from=8699e9907a142_1632825538299

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值