Fork/Join优化mybatis-plus批量插入性能

news2025/1/19 16:57:19

最近在项目开发中,遇到需要一次性保存100万数据到数据库中。想到以下几种实现方式:

第一种方案:在mapper文件中,实现批量插入动态SQL语句,采用insert into table_name values(?,?,?,),(?,?,?)拼接SQL方式。

<insert id="batchSave" parameterType="java.util.Collection" useGeneratedKeys="true" keyProperty="id">
        insert into menu_resource(parent_id,path,resource_key,resource_type,status)
        values
        <foreach collection="list" item="menuResource" separator=",">
            (
            #{menuResource.parentId},
            #{menuResource.path},
            #{menuResource.resourceKey},
            #{menuResource.resourceType},
            #{menuResource.status}
            )
        </foreach>
    </insert>

Service实现如下:

         List<MenuResource> menuResourceList =
                menuResourceAddDtoCollection.stream().map(this::convert2Do).collect(Collectors.toList());
        menuResourceMapper.batchSave(menuResourceList);

如果此时不做任何限制,一次性插入100万条数据,首先会报运行时异常:

        com.mysql.jdbc.PacketTooBigException: Packet for query is too large (16588949 > 4194304).
            You can change this value on the server by setting the max_allowed_packet' variable.

这句报错是,MySQL默认执行query语句的字节长度默认不能超过4M的字节。很显然,100万条数据触发了该限制。

解决方案:修改数据库配置,将限制改为256M:set global max_allowed_packet = 2*1024*1024*128;

修改完毕后,多次执行发现,程序直接异常退出,更有甚者,系统出现死机。

后来发现:出现内存溢出问题。

优化:一次性插入100万条数据,必须要实行按批处理。经过调试发现,每次批量插入1000条数据是一个比较中肯方式。(如果表中字段很多)。

优化后代码如下:

List<MenuResource> menuResourceList =
                menuResourceAddDtoCollection.stream().map(this::convert2Do).collect(Collectors.toList());

        long startTime = System.currentTimeMillis();
        int size = menuResourceList.size();
        if (size <= 1000) {
            menuResourceMapper.batchSave(menuResourceList);
            return Boolean.TRUE;
        }

        int i = size / 1000,remainder = size % 1000,j=0;
        for (; j < i; j++) {
            List<MenuResource> dos = menuResourceList.subList(j * 1000, j * 1000 + 1000);
            menuResourceMapper.batchSave(dos);
        }
        if (remainder != 0) {
            List<MenuResource> dos = menuResourceList.subList(j * 1000, j * 1000 + remainder);
            //自定义批量插入,insert into values(1000条数据)
            menuResourceMapper.batchSave(dos);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("自定义Mapper实现批量插入耗时:" + (endTime - startTime) + " 毫秒");

经过测试发现:采用自定义的mapper文件的方式实现100万条数据批量插入,执行五次耗时分别为:38237、30524、32508、32455、33227【毫秒】。

第二种方式:利用Mybatis-Plus的saveBatch方式:

List<MenuResource> menuResourceList =
                menuResourceAddDtoCollection.stream().map(this::convert2Do).collect(Collectors.toList());

        long startTime = System.currentTimeMillis();
        int size = menuResourceList.size();
        if (size <= 1000) {
            menuResourceMapper.batchSave(menuResourceList);
            return Boolean.TRUE;
        }

        int i = size / 1000,remainder = size % 1000,j=0;
        for (; j < i; j++) {
            List<MenuResource> dos = menuResourceList.subList(j * 1000, j * 1000 + 1000);
            menuResourceMapper.batchSave(dos);
        }
        if (remainder != 0) {
            List<MenuResource> dos = menuResourceList.subList(j * 1000, j * 1000 + remainder);
            //调用Mybatis-Plus的saveBatch方法
            saveBatch(dos);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("借助Mybatis-Plus实现批量插入耗时:" + (endTime - startTime) + " 毫秒");

经过测试发现:平均耗时为:34219 毫秒,比着自定义的批量插入,耗时高了1-2s左右。

再进一步优化。

第三种实现方式:借助JDK1.7的Fork/Join框架,以多线程的方式,将100万数据插入的总任务拆分为多个子任务。实现异步并行处理,以此来达到优化的目的。

实现如下:自定义批量插入任务。

public class MenuResourceBatchInsertTask extends RecursiveTask<Void> {

    //默认插入1000条数据
    private final int threshold = 1000;

    private List<MenuResource> menuResourceList;

    private MenuResourceMapper menuResourceMapper;

    public MenuResourceBatchInsertTask(List<MenuResource> menuResourceList, MenuResourceMapper menuResourceMapper) {
        this.menuResourceList = menuResourceList;
        this.menuResourceMapper = menuResourceMapper;
    }

    @Override
    protected Void compute() {
        if (menuResourceList.size() < threshold) {
            menuResourceMapper.batchSave(menuResourceList);
        } else {
            int middle = menuResourceList.size() / 2;
            List<MenuResource> startResourceList = menuResourceList.subList(0, middle);
            MenuResourceBatchInsertTask task1 = new MenuResourceBatchInsertTask(startResourceList, menuResourceMapper);
            List<MenuResource> endResourceList = menuResourceList.subList(middle,menuResourceList.size());
            MenuResourceBatchInsertTask task2 = new MenuResourceBatchInsertTask(endResourceList, menuResourceMapper);
            invokeAll(task1, task2);
        }
        return null;
    }
}

Service层实现如下:

@Service
public class MenuResourceServiceImpl extends ServiceImpl<MenuResourceMapper, MenuResource>
    implements MenuResourceService{

    @Resource
    private MenuResourceMapper menuResourceMapper;

    private ForkJoinPool forkJoinPool = new ForkJoinPool();

    @Override
    public Boolean addResource(MenuResourceAddDto menuResourceAddDto) {
        MenuResource menuResource = new MenuResource();
        BeanUtils.copyProperties(menuResourceAddDto, menuResource);
        menuResource.setResourceType(menuResourceAddDto.getResourceTypeEnum().getCode());
        return save(menuResource);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean batchSave(Collection<MenuResourceAddDto> menuResourceAddDtoCollection) {
        /**
         * 新增菜单资源,当新增菜单资源成功后,发送一条邮件信息,给到特定的主管人员
         *
         */
        if (CollectionUtils.isEmpty(menuResourceAddDtoCollection)) {
            throw new BusinessException(ErrorCodeEnum.MENU_RESOURCE_NOT_EMPTY);
        }

        List<MenuResource> menuResourceList =
                menuResourceAddDtoCollection.stream().map(this::convert2Do).collect(Collectors.toList());

        /*long startTime = System.currentTimeMillis();
        int size = menuResourceList.size();
        if (size <= 1000) {
            menuResourceMapper.batchSave(menuResourceList);
            return Boolean.TRUE;
        }

        int i = size / 1000,remainder = size % 1000,j=0;
        for (; j < i; j++) {
            List<MenuResource> dos = menuResourceList.subList(j * 1000, j * 1000 + 1000);
            menuResourceMapper.batchSave(dos);
        }
        if (remainder != 0) {
            List<MenuResource> dos = menuResourceList.subList(j * 1000, j * 1000 + remainder);
            //自定义批量插入,insert into values(1000条数据)
            menuResourceMapper.batchSave(dos);
            //使用mybatis-plus提供的批量插入:耗时 32598 毫秒
//            saveBatch(dos);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("借助Mybatis-Plus实现批量插入耗时:" + (endTime - startTime) + " 毫秒");
*/





//        menuResourceMapper.batchSave(menuResourceList);

        long startTime = System.currentTimeMillis();
        MenuResourceBatchInsertTask insertTask = new MenuResourceBatchInsertTask(menuResourceList,menuResourceMapper);
        forkJoinPool.invoke(insertTask);
        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) + " 毫秒");
        return Boolean.TRUE;
    }

    private <T> MenuResourceVo convert2Vo(T dto) {
        if (Objects.isNull(dto)) {
            return null;
        }
        MenuResourceVo menuResourceVo = MenuResourceVo.builder().build();
        BeanUtils.copyProperties(dto, menuResourceVo);
        return menuResourceVo;
    }

    private <T> MenuResource convert2Do(T dto) {
        if (Objects.isNull(dto)) {
            return null;
        }
        MenuResource menuResource = new MenuResource();
        BeanUtils.copyProperties(dto, menuResource);
        menuResource.setResourceType(0);
        return menuResource;
    }


}

经测试发现:通过Fork/Join结合自定义mapper动态SQL批量插入100万条数据,耗时大约:10s

测试类如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MenuResourceServiceTest {

    @Resource
    private MenuResourceService menuResourceService;

    @Test
    public void testBatchSave() {
        List<MenuResourceAddDto> list = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            String uuid = UUID.randomUUID().toString();
            MenuResourceAddDto dto = new MenuResourceAddDto();
            dto.setResourceKey(uuid);
            dto.setPath(uuid + "/" + i);
            dto.setParentId(1L);
            dto.setStatus(0);
            dto.setResourceTypeEnum(MenuResourceTypeEnum.MENU);
            list.add(dto);
        }
        menuResourceService.batchSave(list);
    }
}

有些读者可能会突发奇想,能不能用Mybatis-plus的saveBatch方法替换掉

MenuResourceBatchInsertTask类compute方法的自定义批量插入方法。答案是不能。经过测试发现。此处效率会特别慢。

原因:Mybatis-Plus的saveBatch方法,是单个insert语句执行的

总结:针对大数据量的批量插入、删除、修改。都可以借助Fork/Join+自定义mapper文件的方式来提升程序性能。 

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

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

相关文章

HTTP中ETag语法及使用实战详解

1.1 ETag 是什么 ETag&#xff08;Entity Tag&#xff09;是万维网协议 HTTP 的一部分。它是 HTTP 协议提供的若干机制中的一种 Web 缓存验证机制&#xff0c;并且允许客户端进行缓存协商。这使得缓存变得更加高效&#xff0c;而且节省带宽。如果资源的内容没有发生改变&#x…

电脑网速慢怎么解决?4个方法有效提升电脑网速!

案例&#xff1a;电脑网速慢怎么解决 【谁懂啊&#xff01;我的电脑网速太慢了&#xff01;总是上不了网&#xff0c;打开浏览器也是一直在转圈圈&#xff01;太折磨了&#xff01;这是为什么呢&#xff1f;谁能帮帮我呀&#xff01;】 随着互联网的发展和普及&#xff0c;电…

继续学c++

由于c里面有很多和c语言很像的东西&#xff0c;这里就来总结一点不像的或者要注意的&#xff0c;或者是我已经快忘记的&#xff1b; 先来一个浮点型也就是实型类型的总结&#xff1b; 知道浮点型有这两个类型&#xff1a;float和double型&#xff1b; 然后float型占四个字节…

To B第六年,腾讯过分温柔

腾讯做2B&#xff0c;方向是正确的&#xff0c;初心是果决的&#xff0c;行动是温柔的&#xff0c;事实是掉队的。 2018年&#xff0c;率先打出“互联网的下半场属于产业互联网”的大旗&#xff0c;宣布“拥抱产业互联网”&#xff0c;腾讯发力To B业务&#xff0c;绝对是有先发…

HTB-Jarvis

HTB-Jarvis 信息收集80端口 www-data(sqlmap)www-data(myPhpAdmin)www-data -> pepperpepper -> root 信息收集 80端口 目录扫描 我啥也没干咋就被ban了&#xff0c;可能是gobuster流量太大被逮住了。 老老实实等90秒&#xff0c;先从已有的目录收集信息。 phpMyAdmin 4…

X 态及基于 VCS 的 X-Propagation 检测

&#x1f525;点击查看精选 IC 技能树系列文章&#x1f525; &#x1f525;点击进入【芯片设计验证】社区&#xff0c;查看更多精彩内容&#x1f525; &#x1f4e2; 声明&#xff1a; &#x1f96d; 作者主页&#xff1a;【MangoPapa的CSDN主页】。⚠️ 本文首发于CSDN&#…

Lightroom Classic2022图文安装教程

Lightroom Classic是一款专业的数字照片处理软件&#xff0c;具有数字照片编辑、照片整理和管理、批量处理、智能预览、输出等特点。 该软件适用于摄影师和数字照片后期处理爱好者&#xff0c;可以帮助用户提高处理效率和照片质量。 Lightroom Classic是Adobe公司推出的系列软…

ai智能写作软件哪个好-ai智能写作免费

人工智能自动写作软件 人工智能自动写作软件是如今数字营销领域中备受瞩目的一种工具。无论是网络文章、博客、报告、新闻稿或者其他一些营销内容&#xff0c;人工智能自动写作软件可以以相当高的速度和质量将其生成&#xff0c;从而释放人类营销人员的时间和精力。 尽管自动写…

深度学习实战案例:基于 AutoRec 构建电影推荐系统( 附 PyTorch 版代码)

文章目录 技术交流前言AutoRec 模型介绍损失函数基于 AutoRec 的推荐过程实验对比消融实验代码实践总结参考 本文要介绍的 AutoRec 模型是由澳大利亚国立大学在2015年提出的&#xff0c;它将自编码器(AutoEncoder)的思想与协同过滤(Collaborative Filter)的思想结合起来&#x…

GitHub 创建 Pull Request 将代码提交至别人的仓库

GitHub 创建 Pull Request 将代码提交至别人的仓库 1 Forking the repository 1.1 About forks (关于 forks) A fork is a new repository that shares code and visibility settings with the original upstream repository. A fork 是一个新的存储库&#xff0c;它与原 ups…

U-Boot 命令使用

进入 uboot 的命令行模式以后输入“help”或者“&#xff1f;”&#xff0c;然后按下回车即可查看当前 uboot 所 支持的命令&#xff0c;如图 所示&#xff1a; 我们输入“help(或?) 命令名”既可以查看命令的详细用法&#xff0c;以“bootz”这 个命令为例&#xff0c;我们输…

4种吃子跳棋

目录 一&#xff0c;双玩家吃子跳棋 玻璃跳棋 大人物跳棋 二&#xff0c;单玩家吃子跳棋 智力游戏67跳棋&#xff08;5&#xff09; 一个挑战 跳瓶盖 欢乐跳跳棋 三&#xff0c;单玩家多目吃子跳棋——Hopping dots 1&#xff0c;Hopping dots 2&#xff0c;规则 3…

说过的话就一定要办到 - redo日志

一、什么是redo日志&#xff1f; 如果我们只在内存的 Buffer Pool 中修改了页面&#xff0c;假设在事务提交后突然发生了某个故障&#xff0c;导致内存中的数据都失效了&#xff0c;那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了&#xff0c;这会导致事务会失去…

火力全开,重新定义蓝牙耳机!新一代南卡OE Pro不入耳式蓝牙耳机震撼来袭

中国专业声学品牌Nank南卡&#xff0c;在近期推出了南卡OE Pro不入耳蓝牙耳机&#xff0c;是业内首款功能配置齐全的蓝牙耳机&#xff0c;以创新开放式听音方式&#xff0c;让更多人感受到不入耳开放式耳机带来的魅力之处。据了解&#xff0c;有不少媒体猜测&#xff0c;南卡OE…

工作面试老大难 - 锁

一、概述 为保证数据的一致性和完整性&#xff0c;需要对 事务间并发操作进行控制 &#xff0c;因此产生了 锁 。锁冲突 也是影响数据库 并发访问性能 的一个重要因素。所以锁对数据库而言显得尤其重要&#xff0c;也更加复杂。 二、并发问题 MySQL并发事务访问相同记录 &am…

硬件设计--DAPLINK设计

1 参考网站 1、打造属于你自己的STM32下载器调试器--------DAPLink 2、ARMmebed官方开源代码DAPLink 3、ARMmebed官方开源代码DAPLink github加速网站 4、ARMmebed官方开源硬件旧版 5、ARMmebed官方开源硬件新版 6、自制DAPLink – ARM官方源码以及STM32F103C8T6 7、如何做一个…

软件测试之测试名词解释

1. 白盒测试&#xff0c;英文是white-box testing 是通过程序的源代码进行测试而不使用用户界面。这种类型的测试需要从代码句法发现内部代码在算法&#xff0c;溢出&#xff0c;路径&#xff0c;条件等等中的缺点或者错误&#xff0c;进而加以修正。 2. 黑盒测试&#xff0c;英…

word脚标【格式:第X页(共X页)】

不得不吐槽一下这个论文&#xff0c;真的我好头疼啊。我又菜又不想改。但是还是得爬起来改 &#xff08;是谁大半夜不能睡觉加班加点改格式啊&#xff09; 如何插入页码。 格式、要求如下: 操作步骤&#xff1a; ①双击页脚&#xff0c;填好格式&#xff0c;宋体小四和居中都…

除了 Swagger,这个开源 API 管理工具生成文档更高效

提起 Swagger&#xff0c;经常接触接口开发的朋友&#xff0c;一定知道并且都熟练使用了。 Swagger 是一个用于生成、描述和调用 RESTful 接口的 Web 服务。通俗的来讲&#xff0c;Swagger 就是将项目中所有&#xff08;想要暴露的&#xff09;接口展现在页面上&#xff0c;并且…

VMware ESXi 8.0U1 发布 - 领先的裸机 Hypervisor

请访问原文链接&#xff1a;https://sysin.org/blog/vmware-esxi-8-u1/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 2023-04-18, VMware vSphere 8.0U1 发布。 详见&#xff1a;VMware vSphere 8 Update 1 新增功能 产品简…