MySQL批量更新最佳实践

写在文章开头

近期进行项目优化梳理工作时,发现某些功能模块进行MySQL数据库批量更新操作比较耗时,对此笔者查阅相关资料比进行压测后,得出最优解,遂以此文章记录一下笔者的解决方案。

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

前置准备

为方便演示,笔者先说明一下本文进行实验的数据表,对应的DDL语句如下,可以看到该表有一个自增的主键ID和9个字段以及一个日期字段:

sql 复制代码
CREATE TABLE `batch_insert_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `fileid_1` varchar(100) DEFAULT NULL,
  `fileid_2` varchar(100) DEFAULT NULL,
  `fileid_3` varchar(100) DEFAULT NULL,
  `fileid_4` varchar(100) DEFAULT NULL,
  `fileid_5` varchar(100) DEFAULT NULL,
  `fileid_6` varchar(100) DEFAULT NULL,
  `fileid_7` varchar(100) DEFAULT NULL,
  `fileid_8` varchar(100) DEFAULT NULL,
  `fileid_9` varchar(100) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `batch_insert_test_create_date_IDX` (`create_date`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=19091237 DEFAULT CHARSET=utf8 COMMENT='测试批量插入,一行数据1k左右';

特别注意,读者在根据本文进行操作时需要对数据库连接配置上追加如下两个参数,否则优化方案不会生效:

bash 复制代码
&rewriteBatchedStatements=true&allowMultiQueries=true

三种方案压测实验

逐条更新

首先查看逐条更新的解决方案,笔者通过分页查询查询大约3000条数据,然后逐条进行遍历更新:

java 复制代码
 /**
     * 使用foreach进行逐条插入
     */
    @Test
    public void foreachUpdate() {
        //分页查询3k的数据
        PageHelper.startPage(PAGE, SIZE);
        List<BatchInsertTest> insertTestList = batchInsertTestMapper.selectByExample(null);

        //逐条更新
        StopWatch stopWatch = new StopWatch("foreachUpdate");
        stopWatch.start();
        for (BatchInsertTest insertTest : insertTestList) {
            batchInsertTestMapper.updateByPrimaryKey(insertTest);
        }
        stopWatch.stop();

        log.info("逐条更新完成,size:{},耗时:{}ms", insertTestList.size(), stopWatch.getLastTaskTimeMillis());


    }

对应耗时结果如下,可以看到耗时花费了3s,表现比较逊色,原因很简单,每条数据操作时都涉及网络IO,3000次串行的网络IO+DB更新,执行效率自然上不去:

bash 复制代码
2024-02-15 16:33:42.106  INFO 2852 --- [           main] c.s.mapper.BatchInsertTestMapperTest     : 逐条更新完成,size:3000,耗时:3593ms

并行运算

不知道读者是否留意笔者上文所说的串行DB更新,既然串行的网络IO会降低执行效率,那么我们并行更新呢?

所以笔者将代码进行进一步的优化

java 复制代码
/**
     * 使用并行流foreach进行逐条插入
     */
    @Test
    public void foreachParallelStreamUpdate() {
        PageHelper.startPage(PAGE, SIZE);

        List<BatchInsertTest> insertTestList = batchInsertTestMapper.selectByExample(null);

        //采用并行流的方式进行并行更新
        StopWatch stopWatch = new StopWatch("foreachUpdate");
        stopWatch.start();
        insertTestList.parallelStream()
                .forEach(i -> {
                    batchInsertTestMapper.updateByPrimaryKey(i);
                });


        stopWatch.stop();

        log.info("逐条更新完成,size:{},耗时:{}ms", insertTestList.size(), stopWatch.getLastTaskTimeMillis());


    }

可以看到3000条数据花费了500毫秒左右,执行效率还是很客观的,但笔者认为这还不是最优解,原因很简单,每次进行批量更新操作都需要进行多次网络IO,如果在并发量非常大的场景,很可能导致MySQL的连接被耗尽导致整个业务线崩溃:

bash 复制代码
2024-02-15 16:42:35.061  INFO 28292 --- [           main] c.s.mapper.BatchInsertTestMapperTest     : 逐条更新完成,size:3000,耗时:556ms

批处理更新

笔者希望可以一批更新操作可以一个批次的进行提交,所以接下来介绍这种方案就是一次性组装一批量的更新语句,然后一次性提交。

java 复制代码
 /**
     * 使用批处理进行更新
     */
    @Test
    public void updateBatch() {
        PageHelper.startPage(PAGE, SIZE);

        List<BatchInsertTest> insertTestList = batchInsertTestMapper.selectByExample(null);
        StopWatch stopWatch = new StopWatch("updateBatch");
        stopWatch.start();

        //创建一个进行批处理操作的sqlsession组装一批更新语句
        try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
            BatchInsertTestMapper batchInsertTestMapper = sqlSession.getMapper(BatchInsertTestMapper.class);

            insertTestList.parallelStream()
                    .forEach(i -> {
                        batchInsertTestMapper.updateByPrimaryKey(i);
                    });
            //手动提交
            sqlSession.commit();
            stopWatch.stop();
        } catch (Exception e) {

        }

        log.info("批处理更新完成,size:{},耗时:{}ms", insertTestList.size(), stopWatch.getLastTaskTimeMillis());

    }

最终更新耗时为1s左右,相较于上述方案相对逊色一些,但是网络IO的开销以及MySQL的连接池使用都减小了,综合起来性价比还是蛮高的:

bash 复制代码
2024-02-22 23:25:05.265  INFO 18844 --- [           main] c.s.mapper.BatchInsertTestMapperTest     : 批处理更新完成,size:3000,耗时:1566ms

when-case更新

最后一种,也是笔者个人个人比较推荐的一种,即when case,语法如下,猛的一看比较复杂,实际理解起来还是蛮简单的,对每个字段进行set操作,例如: 当id等于1时,fileid_1则取id为1的那条数据的值,即:

bash 复制代码
update batch_insert_test
        set fileid_1=
            when 1 then id为1的fileid_1的值
      ....其余同理
       where id in (本次批处理的id列表)

所以结合mybatis框架的语法,我们得出下面这样一个SQL语句:

xml 复制代码
<update id="updateBatch" parameterType="java.util.List">
        update batch_insert_test
        set fileid_1=
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid1,jdbcType=VARCHAR}
        </foreach>,
        fileid_2 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid2,jdbcType=VARCHAR}
        </foreach>,
        fileid_3 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid3,jdbcType=VARCHAR}
        </foreach>,
        fileid_4 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid4,jdbcType=VARCHAR}
        </foreach>,
        fileid_5 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid5,jdbcType=VARCHAR}
        </foreach>,
        fileid_6 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid6,jdbcType=VARCHAR}
        </foreach>,
        fileid_7 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid7,jdbcType=VARCHAR}
        </foreach>,
        fileid_8 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid8,jdbcType=VARCHAR}
        </foreach>,
        fileid_9 =
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.fileid9,jdbcType=VARCHAR}
        </foreach>,
        create_date=
        <foreach collection="list" item="item" index="index"
                 separator=" " open="case ID" close="end">
            when #{item.id} then #{item.createDate,jdbcType=TIMESTAMP}
        </foreach>
        where id in
        <foreach collection="list" index="index" item="item"
                 separator="," open="(" close=")">
            #{item.id,jdbcType=INTEGER}
        </foreach>
    </update>

对应的Java代码如下,比较简单,笔者这里就不多做赘述了:

java 复制代码
@Test
    public void updateDateByWhenCase() {
        PageHelper.startPage(PAGE, SIZE);

        List<BatchInsertTest> insertTestList = batchInsertTestMapper.selectByExample(null);
        StopWatch stopWatch = new StopWatch("updateBatch");
        stopWatch.start();

        batchInsertTestMapper.updateBatch(insertTestList);

        stopWatch.stop();

        log.info("使用when case更新完成,size:{},耗时:{}ms", insertTestList.size(), stopWatch.getLastTaskTimeMillis());
    }

最终可以看到耗时800毫秒左右,相较于批处理更加出色一些,而且网络和连接池的开销都是差不多的:

bash 复制代码
2024-02-15 16:47:41.267  INFO 10228 --- [           main] c.s.mapper.BatchInsertTestMapperTest     : 使用when case更新完成,size:3000,耗时:897ms

小结

以上便是笔者本次大量压测后得出的解决方案,总结如下:

  1. 如果网络情况良好且MySQL连接池资源充分的情况下,笔者更推荐使用并行进行逐条更新。
  2. 如果网络情况不好或者MySQL资源紧张,笔者更推荐使用when-case语法的批量更新。
  3. 涉及高并发的场景需要尽可能减少对于MySQL连接池的使用,也同样推荐when-case的批量更新。

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考

mybatis + mysql 高性能批量插入和批量更新:blog.csdn.net/w_g_b/artic...

Mybatis中进行批量更新(updateBatch):blog.csdn.net/xyjawq1/art...

复制代码
相关推荐
喵个咪16 分钟前
go-wind-cms 微服务架构设计:为什么基于 Kratos?
后端·微服务·cms
神奇小汤圆22 分钟前
百度面试官:Redis 内存满了怎么办?你有想过吗?
后端
喵个咪24 分钟前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
开心就好202525 分钟前
HTTPS超文本传输安全协议全面解析与工作原理
后端·ios
小江的记录本28 分钟前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
神奇小汤圆29 分钟前
Spring Batch实战
后端
喵个咪31 分钟前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms
程序员木圭33 分钟前
07-数组入门必看!Java数组的内存分析02
java·后端
喵个咪44 分钟前
Go 语言 CMS 横评:风行 GoWind 对比传统 PHP/Java CMS 核心优势
前端·后端·cms
面向Google编程1 小时前
从零学习Kafka:位移与高水位
大数据·后端·kafka