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...

复制代码
相关推荐
无风听海19 分钟前
ASP.NET Core .NET 10 错误响应体系全景:从 BadRequest 到编译器基础设施
后端·asp.net·.net
文心快码BaiduComate20 分钟前
从个人效能到组织资产:文心快码企业版Agent Hub上线,提升团队AI编程效能
前端·后端·程序员
雪隐1 小时前
个人电脑玩AI00-前言
人工智能·后端
我是一颗柠檬1 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
前端Hardy1 小时前
CSS 动画真的比 JS 快?Josh Comeau 做了组实验,结果跟直觉不一样
前端·javascript·后端
Front思1 小时前
调取支付宝支付正式环境不可以唤起来,但是沙箱可以
后端
foggyprojects1 小时前
AI 生成 SQL 模板以后,为什么还需要固定 helper 规则
后端
明天一点1 小时前
Cloudflare 通知转发钉钉机器人
前端·后端
前端Hardy2 小时前
前端日历组件,要变天了?Schedule-X v4.6 彻底杀疯了
前端·javascript·后端
Oo_行者_oO2 小时前
微服务 Feign 从“万能公共服务”到“业务客户端”
后端·架构