百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

坑已踩好,MyBatis 几种批量插入性能比较

ccwgpt 2025-02-06 14:12 47 浏览 0 评论

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

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

一、前期准备

基于Spring Boot + Mysql,同时为了省略get/set,使用了lombok,详见pom.xml。

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://47.111.118.152:3306/mybatis?rewriteBatchedStatements=true
    username: mybatis
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: cn.van.mybatis.batch.entity

1.3 实体类

@Data
@Accessors(chain = true)
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;
}

1.4 UserInfoBatchMapper

public interface UserInfoBatchMapper {


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


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

1.5 UserInfoBatchMapper.xml






  
    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 into user_info_batch (user_name, pass_word, nick_name, mobile, email, gmt_create, gmt_update)
    values
    
      (#{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})
    
  

1.6 预备数据

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

   
private List list = new ArrayList<>();
    private List lessList = new ArrayList<>();
    private List lageList = new ArrayList<>();
    private List warmList = new ArrayList<>();
    // 计数工具
    private StopWatch sw = new StopWatch();

为了方便组装数据,抽出了一个公共方法。

private List assemblyData(int count){
        List list = new ArrayList<>();
        UserInfoBatchDO userInfoDO;
        for (int i = 0;i < count;i++){
            userInfoDO = new UserInfoBatchDO()
                    .setUserName("Van")
                    .setNickName("风尘博客")
                    .setMobile("17098705205")
                    .setPassWord("password")
                    .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() {
    log.info("【程序热身】");
    for (UserInfoBatchDO userInfoBatchDO : warmList) {
        userInfoBatchMapper.insert(userInfoBatchDO);
    }
    log.info("【热身结束】");
    sw.start("反复执行单条插入语句");
    // 这里插入 20w 条太慢了,所以我只插入了 2000 条
    for (UserInfoBatchDO userInfoBatchDO : lessList) {
        userInfoBatchMapper.insert(userInfoBatchDO);
    }
    sw.stop();
    log.info("all cost info:{}",sw.prettyPrint());
}

2.3 执行时间

  • 第一次
-----------------------------------------
ms     %     Task name
-----------------------------------------
59887  100%  反复执行单条插入语句

  • 第二次
-----------------------------------------
ms     %     Task name
-----------------------------------------
64853  100%  反复执行单条插入语句
  • 第三次
-----------------------------------------
ms     %     Task name
-----------------------------------------
58235  100%  反复执行单条插入语句

该方式插入2000 条数据,执行三次的平均时间:60991 ms。

三、foreach 拼接SQL

3.1 对应mapper 接口

int batchInsert(List list);

3.2 测试方法

该方式和下一种方式都采用20w条数据测试。

@Test
public void batchInsert() {
    log.info("【程序热身】");
    for (UserInfoBatchDO userInfoBatchDO : warmList) {
        userInfoBatchMapper.insert(userInfoBatchDO);
    }
    log.info("【热身结束】");
    sw.start("foreach 拼接 sql");
    userInfoBatchMapper.batchInsert(list);
    sw.stop();
    log.info("all cost info:{}",sw.prettyPrint());
}

3.3 执行时间

  • 第一次
-----------------------------------------
ms     %     Task name
-----------------------------------------
18835  100%  foreach 拼接 sql

  • 第二次
-----------------------------------------
ms     %     Task name
-----------------------------------------
17895  100%  foreach 拼接 sql

  • 第三次
-----------------------------------------
ms     %     Task name
-----------------------------------------
19827  100%  foreach 拼接 sql

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

四、批处理

该方式 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() {
    log.info("【程序热身】");
    for (UserInfoBatchDO userInfoBatchDO : warmList) {
        userInfoBatchMapper.insert(userInfoBatchDO);
    }
    log.info("【热身结束】");
    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();
    log.info("all cost info:{}",sw.prettyPrint());
}

4.3 执行时间

  • 第一次
-----------------------------------------
ms     %     Task name
-----------------------------------------
09346  100%  批处理执行 插入
  • 第二次
-----------------------------------------
ms     %     Task name
-----------------------------------------
08890  100%  批处理执行 插入

  • 第三次
-----------------------------------------
ms     %     Task name
-----------------------------------------
09042  100%  批处理执行 插入

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

4.4 如果数据更大

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

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

  • 第一次
-----------------------------------------
ms     %     Task name
-----------------------------------------
32419  100%  批处理执行 插入
  • 第二次
-----------------------------------------
ms     %     Task name
-----------------------------------------
31935  100%  批处理执行 插入
  • 第三次
-----------------------------------------
ms     %     Task name
-----------------------------------------
33048  100%  批处理执行 插入

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

五、总结

批量插入方式

数据量

执行三次的平均时间

循环插入单条数据

2000

60991 ms

foreach 拼接sql

20w

18852 ms

批处理

20w

9092 ms

批处理

100w

32467 ms

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

相关推荐

自己动手写Android数据库框架_android开发数据库搭建

http://blog.csdn.net/feiduclear_up/article/details/50557590推荐理由关于Android数据库操作,由于每次都要自己写数据库操作,每次还得去...

谷歌开源大模型评测工具LMEval,打通谷歌、OpenAI、Anthropic

智东西编译|金碧辉编辑|程茜智东西5月28日消息,据科技媒体TheDecoder5月26日报道,当天,谷歌正式发布开源大模型评测框架LMEval,支持对GPT-4o、Claude3.7...

工信部:着力推动大模型算法、框架等基础性原创性的技术突破

工信部新闻发言人今日在发布会上表示,下一步,我们将坚持突出重点领域,大力推动制造业数字化转型,推动人工智能创新应用。主要从以下四个方面着力。一是夯实人工智能技术底座。通过科技创新重大项目,着力推动大模...

乒乓反复纠结“框架不稳定”的三个小误区

很多球友由于对框架的认知不清晰,往往会把“框架不稳定”当成一种心理负担,从而影响学球进度,其典型状态就是训练中有模有样,一旦进入实战,就像被捆住了手脚。通过训练和学习,结合“基本功打卡群”球友们交流发...

前AMD、英特尔显卡架构师Raja再战GPU,号称要全面重构堆栈

IT之家8月5日消息,知名GPU架构师拉贾科杜里(RajaKoduri)此前曾先后在AMD和英特尔的显卡部门担任要职。而在今日,由Raja创立的GPU软件与IP初创企...

三种必须掌握的嵌入式开发程序架构

前言在嵌入式软件开发,包括单片机开发中,软件架构对于开发人员是一个必须认真考虑的问题。软件架构对于系统整体的稳定性和可靠性是非常重要的,一个合适的软件架构不仅结构清晰,并且便于开发。我相...

怪不得别人3秒就知道软考案例怎么做能50+

软考高级统一合格标准必须三科都达到45分,案例分析也一直是考生头疼的一门,但是掌握到得分点,案例能不能50+还不是你们说了算吗?今天就结合架构案例考点,分享实用的备考攻略~一、吃透考点,搭建知识框架从...

UML统一建模常用图有哪些,各自的作用是什么?一篇文章彻底讲透

10万+爆款解析:9大UML图实战案例,小白也能秒懂!为什么需要UML?UML(统一建模语言)是软件开发的“蓝图”,用图形化语言描述系统结构、行为和交互,让复杂需求一目了然。它能:降低沟通成本避...

勒索软件转向云原生架构,直指备份基础设施

勒索软件组织和其他网络犯罪分子正越来越多地将目标对准基于云的备份系统,对久已确立的灾难恢复方法构成了挑战。谷歌安全研究人员在一份关于云安全威胁演变的报告中警告称,随着攻击者不断改进数据窃取、身份泄露和...

ConceptDraw DIAGRAM:释放创意,绘就高效办公新未来

在当今数字化时代,可视化工具已成为提升工作效率和激发创意的关键。ConceptDrawDIAGRAM,作为一款世界顶级的商业绘图软件,凭借其强大的功能和用户友好的界面,正逐渐成为众多专业人士的首选绘...

APP 制作界面设计教程:一步到位_app界面设计模板一套

想让APP界面设计高效落地,无需繁琐流程,掌握“框架搭建—细节填充—体验优化”三步法,即可一步到位完成专业级设计。黄金框架搭建是基础。采用“三三制布局”:将屏幕横向三等分,纵向保留三...

MCP 的工作原理:关键组件_mcp部件

以下是MCP架构的关键组件:MCP主机:像ClaudeDesktop、GitHubCopilot或旅行助手这样的AI智能体,它们希望通过MCP协议访问工具、资源等。MCP主机会...

软件架构_软件架构师工资一般多少

软件架构师自身需要是程序员,并且必须一直坚持做一线程序员。软件架构应该是能力最强的一群程序员,他们通常会在自身承接编程任务的同时,逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进。软件系统的架...

不知不觉将手机字体调大!老花眼是因为“老了吗”?

现在不管是联系、交友,还是购物,都离不开手机。中老年人使用手机的时间也在逐渐加长,刷抖音、看短视频、发朋友圈……看手机的同时,人们也不得不面对“视力危机”——老花眼,习惯眯眼看、凑近看、瞪眼看,不少人...

8000通用汉字学习系列讲座(第046讲)

[表声母字]加(续)[从声汉字]伽茄泇迦枷痂袈笳嘉驾架咖贺瘸(计14字)嘉[正音]标准音读jiā。[辨形]上下结构,十四画。会意形声字,从壴从加,加也表声。注:从壴,字义与鼓乐有关;从加,字义与...

取消回复欢迎 发表评论: