MyBatis-Plus批量保存与多线程保存比较

news2024/12/19 7:37:46

在日常开发中经常会涉及大量数据保存的情况,之前就是使用saveBatch的方式,直接放一个list进去,看到一篇关于MyBatisPlus批量保存saveBatch的文章,里面对saveBatch进行了数据量的保存测试,还有解析rewriteBatchedStatements=true 的作用,但测试的批次和对比比较少,所以又对各种方式的保存性能进行分析,通过逐个插入,多线程插入,批量插入,多线程批量插入的方式,比较具体的差异情况。

1. 测试前的数据准备

为了保证足够的数据量,每次筛选出5000条数据进行插入,每条数据具有11个字段,大约100个字节,总体数据大小约500KB。

逐个保存方案:遍历5000条数据,逐个使用save方式进行保存。

多线程逐个保存方案:新建线程池,其中线程数量为5个,遍历5000条数据时每次新建一个任务扔到线程池中进行处理,线程使用save的方式进行保存。

saveBatch方案:不设置saveBatch的batchSize参数,直接将5000条数据的list放入方法中进行批量保存。

多线程saveBatch方案:新建线程池,其中线程数量为5个,遍历5000条数据时将数据均分成5个list,分别放到线程池中进行执行,线程使用saveBatch的方式直接将1000条数据进行批量保存。

以上多线程的执行方式采用submit有future的返回方式,任务放入线程池后保存future对象,后续手动进行get请求,保证计时内的任务都执行完毕。因为如果使用这种异步方式直接保存,计时器只会统计扔到线程池的时间,大概5ms就能结束,不具备参考的意义。

测试前的代码内容

方案1:逐个保存方案

public String dbDataTest() {
    // 获取能源数据列表
    List<Energy> dataList = getEnergy();
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 遍历数据列表,将每个能源数据转换为能源测试数据并保存到数据库
    for (Energy energy : dataList) {
        // 创建能源测试数据对象
        EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
        .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
        .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
        .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
        // 将能源测试数据保存到数据库
        energyTestService.save(test);
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    // 返回执行时间
    return end - start + "ms";
}

方案2:多线程逐个保存方案

public String dbDataTest2() throws ExecutionException, InterruptedException {
    // 获取能源数据列表
    List<Energy> dataList = getEnergy();
    // 记录开始时间
    long start = System.currentTimeMillis();
    //创建线程池
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    // 创建一个用于存储异步任务执行结果的列表
    List<Future<?>> futures = new ArrayList<>();
    for (Energy energy : dataList) {
        // 创建能源测试数据对象
        EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
        .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
        .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
        .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
        // 将能源测试数据保存到数据库
        futures.add(executorService.submit(() -> {
            energyTestService.save(test);
            return "null";
        }));
    }
    //获取异步任务执行结果
    for (Future<?> future : futures) {
        future.get();
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    // 返回执行时间
    return end - start + "ms";
}

方案3:saveBatch方案

public String dbDataTest3() {
    // 获取能源数据列表
    List<Energy> dataList = getEnergy();
    // 记录开始时间
    long start = System.currentTimeMillis();
    // 创建一个用于存储 EnergyTest 对象的列表
    List<EnergyTest> testList = new ArrayList<>();
    // 遍历数据列表,将每个能源数据转换为能源测试数据并添加到 testList 中
    for (Energy energy : dataList) {
        // 创建能源测试数据对象
        EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
        .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
        .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
        .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
        // 将能源测试数据添加到 testList 中
        testList.add(test);
    }
    // 将 testList 中的所有能源测试数据批量保存到数据库
    energyTestService.saveBatch(testList);
    // 记录结束时间
    long end = System.currentTimeMillis();
    // 返回执行时间
    return end - start + "ms";
}

方案4:多线程saveBatch方案

public String dbDataTest4() throws ExecutionException, InterruptedException {
    // 获取能源数据列表
    List<Energy> dataList = getEnergy();
    // 记录开始时间
    long start = System.currentTimeMillis();
    //创建线程池
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    // 用于存储分批后的能源测试数据
    Map<String, List<EnergyTest>> testListMap = new HashMap<>(8);
    // 标记当前批次
    int saveFlag = 0;
    // 遍历数据列表,将每个能源数据转换为能源测试数据并添加到对应的批次中
    for (Energy energy : dataList) {
        EnergyTest test = EnergyTest.builder().id(energy.getId()).buildId(energy.getBuildId()).buildName(energy.getBuildName())
        .collectionTime(energy.getCollectionTime()).dataType(energy.getDataType()).usageData(energy.getUsageData())
        .meterId(energy.getMeterId()).meterName(energy.getMeterName()).meterType(energy.getMeterType())
        .reading(energy.getReading()).totalSurface(energy.getTotalSurface()).build();
        // 如果当前批次的列表不存在或大小超过1000,则创建新的批次
        if (!testListMap.containsKey(String.valueOf(saveFlag)) || testListMap.get(String.valueOf(saveFlag)).size() >= 1000) {
            saveFlag++;
            testListMap.put(String.valueOf(saveFlag), new ArrayList<>());
        }
        // 将能源测试数据添加到当前批次的列表中
        testListMap.get(String.valueOf(saveFlag)).add(test);
    }
    // 创建一个用于存储异步任务执行结果的列表
    List<Future<?>> futures = new ArrayList<>();
    // 遍历批次列表,将每个批次的能源测试数据批量保存到数据库
    for (Map.Entry<String, List<EnergyTest>> entry : testListMap.entrySet()) {
        List<EnergyTest> testList = entry.getValue();
        // 提交异步任务,将当前批次的数据批量保存到数据库
        futures.add(executorService.submit(() -> {
            energyTestService.saveBatch(testList);
            return "null";
        }));
    }
    // 获取异步任务执行结果
    for (Future<?> future : futures) {
        future.get();
    }
    // 记录结束时间
    long end = System.currentTimeMillis();
    // 返回执行时间
    return end - start + "ms";
}

2. 第一次测试(不设置rewriteBatchedStatements=true)

测试批次/耗时

逐个保存方案

多线程逐个保存方案

saveBatch方案

多线程saveBatch方案

1

1461ms

514ms

432ms

167ms

2

1432ms

544ms

416ms

170ms

3

1347ms

539ms

428ms

163ms

4

1288ms

486ms

413ms

184ms

5

1434ms

560ms

440ms

168ms

6

1460ms

513ms

462ms

188ms

7

1453ms

480ms

466ms

194ms

8

1435ms

477ms

459ms

170ms

9

1508ms

491ms

408ms

160ms

10

1437ms

484ms

417ms

178ms

最大值

1508ms

560ms

466ms

194ms

最小值

1288ms

477ms

408ms

160ms

平均值

1425.5ms

508.8ms

434.1ms

174.2ms

通过十次测试数据,虽然还有偏差,但也具体有些参考的价值,首先是逐个保存的方案效率最低,多线程的方式会提高很多,而saveBatch明显要比多线程的方式更好,saveBatch并没有对多条SQL进行合并,可能saveBatch的线程数量多一些,这里我将多线程逐个保存方案自定义的线程池内线程数量调整为10,耗时基本和saveBatch的相同,甚至还比saveBatch要快一些,而调大线程池的逐个保存方案在300ms左右达到瓶颈,很难再根据线程数量将耗时降低。这里多线程saveBatch的方案明显是最快的,应该是saveBatch还有一些其他方式的优化。

3. 第二次测试(设置rewriteBatchedStatements=true)

测试批次/耗时

逐个保存方案

多线程逐个保存方案

saveBatch方案

多线程saveBatch方案

1

1536ms

505ms

244ms

106ms

2

1591ms

495ms

277ms

89ms

3

1628

510ms

261ms

96ms

4

1618ms

487ms

281ms

100ms

5

1581ms

519ms

258ms

111ms

6

1655ms

515ms

264ms

112ms

7

1618ms

508ms

271ms

103ms

8

1507ms

519ms

282ms

98ms

9

1531ms

509ms

280ms

85ms

10

1651ms

507ms

287ms

96ms

最大值

1655ms

519ms

287ms

112ms

最小值

1507ms

487ms

244ms

85ms

平均值

1591.6ms

507.4ms

270.5ms

99.6ms

通过对比第一次测试的结果可以看出来,逐个保存和多线程逐个保存的原理都是每次执行一条SQL语句,所以在性能上没有任何优化提升,而saveBatch则提升了40~50%。

4. 总结rewriteBatchedStatements=true的作用

4.1 JDBC批处理机制

JDBC批处理机制是一种优化数据库操作性能的技术,允许将多条SQL语句作为一个批次发送到数据库服务器执行,从而减少客户端与数据库之间的交互次数,显著提高性能。通常用于批量插入、批量更新和批量删除等场景。具体的流程如下:

//创建 PreparedStatement 对象,用于定义批处理的 SQL 模板。
PreparedStatement pstmt = conn.prepareStatement(sql);
for (Data data : dataList) {
    // 多次调用 addBatch() 方法,每次调用都会将一条 SQL 加入批处理队列。
    pstmt.addBatch();
}
//执行批处理,调用 executeBatch() 方法,批量发送 SQL 并执行。
pstmt.executeBatch();

4.2 MySQL JDBC 驱动的默认行为对批处理的影响

未开启重写:在默认状态下,MySQL JDBC驱动会逐一条目地发送批处理中的SQL语句,未开启重写功能。

性能瓶颈:频繁的网络交互以及数据库解析操作,使得批量操作的性能提升效果有限,形成了性能瓶颈。

4.3 rewriteBatchedStatements=true

启用批处理重写:启用批处理重写功能后,驱动能够将多条同类型的SQL语句进行合并,进而发送给数据库执行。

减少网络交互:一次发送多条SQL,可有效降低网络延迟,减少网络交互次数。

提高执行效率:当所有数据都通过一条SQL插入时,MySQL只需要解析一次SQL,降低了解析和执行的开销。

减少内存消耗:虽然批量操作时将数据合并到一条SQL中,理论上会增加内存使用(因为需要构建更大的SQL字符串),但相比多次单条插入的网络延迟和处理开销,整体的资源消耗和执行效率是更优的。

未开启参数时的批处理SQL:

INSERTINTO question (exam_id, content) VALUES (?, ?);
INSERT INTO question (exam_id, content) VALUES (?, ?);
INSERT INTO question (exam_id, content) VALUES (?, ?);
开启参数后的批处理 SQL:
INSERT INTO question (exam_id, content) VALUES (?, ?), (?, ?), (?, ?);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2262030.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

centos stream 8下载安装遇到的坑

早在2020年12月。CentOS 官方发文宣称&#xff1a;“CentOS项目的未来是 CentOS Stream 明年我们会将重点从CentOS Linux 转移到CentOS Stream 它紧随当前 RHEL 版本之前。CentOS Linux 8 作为 RHEL 8 的重建&#xff0c;将于 2021 年底结束。CentOS Stream 在该日期之后继续&a…

信息安全实训室网络攻防靶场实战核心平台解决方案

一、引言 网络安全靶场&#xff0c;作为一种融合了虚拟与现实环境的综合性平台&#xff0c;专为基础设施、应用程序及物理系统等目标设计&#xff0c;旨在向系统用户提供全方位的安全服务&#xff0c;涵盖教学、研究、训练及测试等多个维度。随着网络空间对抗态势的日益复杂化…

视频孪生在景区文件场景中的应用

视频孪生技术在景区的应用主要体现在提升景区的智能化管理和游客的沉浸式体验上‌。依托于视频孪生时空承载平台&#xff0c;可在景区实景三维孪生场景中直观展示景区文物资源、建筑景观、自然景观等资源的类型、数量、空间分布等信息&#xff0c;并可详细查询单体景观详细资料…

电脑excel词典(xllex.dll)文件丢失是或损坏是什么原因?“xllex.dll文件缺失“要怎么解决?

Excel词典&#xff08;xllex.dll&#xff09;文件丢失或损坏&#xff1f;别担心&#xff0c;这里有解决之道&#xff01; 在日常的电脑使用和办公软件操作中&#xff0c;我们偶尔会碰到一些让人头疼的问题&#xff0c;比如Excel突然提示“Excel词典&#xff08;xllex.dll&…

【MySQL】优雅的使用MySQL实现分布式锁

MySQL实现分布式锁 引言二、基于唯一索引2.1、实现思路2.2、代码实现2.3、 测试代码2.4、小结 三、基于悲观锁3.1 、实现思路3.2、代码实现3.3、测试代码3.4、小结 四、基于乐观锁4.1 、实现思路4.2 、代码实现4.3 、测试代码4.4、小结 总结 引言 在文章《Redis实现分布式锁详…

Elasticsearch:使用 Open Crawler 和 semantic text 进行语义搜索

作者&#xff1a;来自 Elastic Jeff Vestal 了解如何使用开放爬虫与 semantic text 字段结合来轻松抓取网站并使其可进行语义搜索。 Elastic Open Crawler 演练 我们在这里要做什么&#xff1f; Elastic Open Crawler 是 Elastic 托管爬虫的后继者。 Semantic text 是 Elasti…

健康养生:拥抱生活的艺术

健康养生&#xff1a;拥抱生活的艺术 在快节奏的现代生活中&#xff0c;健康已成为我们最宝贵的财富。健康养生&#xff0c;不仅仅是一种生活方式的选择&#xff0c;更是一种对待生活的态度&#xff0c;它关乎于如何在日常中寻找到平衡&#xff0c;让身心得以滋养&#xff0c;…

零基础开始学习鸿蒙开发-交友软件页面设计

目录 1.找一张网图&#xff0c;确定大致页面设计 2.页面布局代码详细介绍 3.完整的代码如下 4.最终的运行效果如下图所示 5.总结 1.找一张网图&#xff0c;确定大致页面设计 2.页面布局代码详细介绍 2.1 顶部文字与搜索框布局&#xff0c;在顶部采用行Row组件布局&#xf…

大数据之Hbase环境安装

Hbase软件版本下载地址&#xff1a; http://mirror.bit.edu.cn/apache/hbase/ 1. 集群环境 Master 172.16.11.97 Slave1 172.16.11.98 Slave2 172.16.11.99 2. 下载软件包 #Master wget http://archive.apache.org/dist/hbase/0.98.24/hbase-0.98.24-hadoop1-bin.tar.gz…

【Java服务端开发】深入理解Java中的Server 层的详细分析

目录 1. 什么是服务端&#xff08;Server&#xff09;层&#xff1f; 2. 设计 Server 层的基本原则 2.1 单一职责原则 2.2 面向接口编程 2.3 事务管理 3. 基于 Spring 的 Server 层实现 3.1 示例&#xff1a;创建一个简单的订单服务 3.2 编写 OrderService 3.3 编写 O…

JAVA:代理模式(Proxy Pattern)的技术指南

1、简述 代理模式(Proxy Pattern)是一种结构型设计模式,用于为其他对象提供一种代理,以控制对这个对象的访问。通过代理模式,我们可以在不修改目标对象代码的情况下扩展功能,满足特定的需求。 设计模式样例:https://gitee.com/lhdxhl/design-pattern-example.git 2、什…

XXE练习

pikachu-XXE靶场 1.POC:攻击测试 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe "a">]> <foo>&xxe;</foo> 2.EXP:查看文件 <?xml version"1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SY…

Leetcode打卡:形成目标字符串需要的最少字符串数II

执行结果&#xff1a;通过 题目&#xff1a;3292 形成目标字符串需要的最少字符串数II 给你一个字符串数组 words 和一个字符串 target。 如果字符串 x 是 words 中 任意 字符串的 前缀 &#xff0c;则认为 x 是一个 有效 字符串。 现计划通过 连接 有效字符串形成 targ…

【蓝桥杯】49362.《视频相关度计算》

视频相关性计算 问题描述 小蓝作为异世界最大流媒体网站 LanTube 的高级算法工程师&#xff0c;他想要实现更加精准的视频推荐服务来满足用户的喜好。 其中&#xff0c;**“视频的相关性”**是一个重要指标&#xff0c;它代表了两个视频 A 到 B 的关联程度&#xff0c;记作 f…

ASP.NET|日常开发中数据集合详解

ASP.NET&#xff5c;日常开发中数据集合详解 前言一、数组&#xff08;Array&#xff09;1.1 定义和基本概念1.2 数组的操作 二、列表&#xff08;List<T>&#xff09;2.1 特点和优势2.2 常用操作 三、字典&#xff08;Dictionary<K, V>&#xff09;3.1 概念和用途…

如何将多张图片合并为一个pdf?多张图片合并成一个PDF文件的方法

如何将多张图片合并为一个pdf&#xff1f;当我们需要将多张图片合并为一个PDF文件时&#xff0c;通常是因为我们希望将这些图片整理成一个统一的文档&#xff0c;方便查看、分享或打印。无论是工作中需要提交的报告、学生们需要整理的作业&#xff0c;还是个人收藏的照片、旅行…

【html网页页面013】html+css制作节日主题圣诞节网页含视频、留言表单(独创首发-5页面附效果及源码)

节日主题圣诞节网页制作 &#x1f964;1、写在前面&#x1f367;2、涉及知识&#x1f333;3、网页效果完整效果(5页)&#xff1a;代码目录结构&#xff1a;page1、首页page2、庆祝page3、影响page4、起源page5、留言板 &#x1f308;4、网页源码4.1 html4.2 CSS4.3 源码获取圣诞…

直播预告 | 蓝卓生态说,解锁supOS在化工领域的无限可能

生态是蓝卓生命力的体现&#xff0c;为全方位赋能生态伙伴使用supOS并从中获益&#xff0c;蓝卓打造生态说系列栏目&#xff0c;通过生态沙龙、直播对话、案例剖析、产品解读等&#xff0c;持续展现“12N”的智能工厂创新路径&#xff0c;加速推进工业数字化转型。 嘉宾介绍 朱…

java对子网掩码的转换

一般的子网掩码展示为点分十进制形式&#xff0c;如&#xff1a;255.255.255.0&#xff0c;但有时因为业务需要&#xff0c;我们需要转换成对应的数字&#xff0c;以及数字转成点分十进制&#xff0c;所以整理了java的方法可以进行两者的互相转换 1、点分十进制转数字 public…

亚马逊-用表格创建多变体商品

引言 当我们使用“月亮树选品软件”找到一款不错的产品时&#xff0c;我们会延续这个产品的优点&#xff0c;并对其进行改良。改良之后&#xff0c;我们需要将产品上架到亚马逊平台&#xff0c;以测试这个产品的市场表现。然而&#xff0c;许多亚马逊卖家觉得上传多变体商品这…