写在文章开头
近期进行项目优化梳理工作时,发现某些功能模块进行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
小结
以上便是笔者本次大量压测后得出的解决方案,总结如下:
- 如果网络情况良好且
MySQL
连接池资源充分的情况下,笔者更推荐使用并行进行逐条更新。 - 如果网络情况不好或者
MySQL
资源紧张,笔者更推荐使用when-case
语法的批量更新。 - 涉及高并发的场景需要尽可能减少对于
MySQL
连接池的使用,也同样推荐when-case
的批量更新。
我是sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程 、JVM 、MySQL数据库个人专栏导航。
参考
mybatis + mysql 高性能批量插入和批量更新:blog.csdn.net/w_g_b/artic...
Mybatis中进行批量更新(updateBatch):blog.csdn.net/xyjawq1/art...