作者今天在开发一个后台发送消息的功能时,由于需要给多个用户发送消息,于是使用了 mybatis plus 提供的 saveBatch() 方法,在测试环境测试通过上预发布后,测试反应发送消息接口很慢得等 5、6 秒,于是我就登录预发布环境查看执行日志,发现是 mybatis plus 提供的 saveBatch() 方法执行很慢导致,于是也就有了本篇文章。

mybatis plus 是一个流行的 ORM 框架,它基于 mybatis,提供了很多便利的功能,比如代码生成器、通用 CRUD、分页插件、乐观锁插件等。它可以让我们更方便地操作数据库,减少重复的代码,提高开发效率。

注意:本文所使用的 mybatis plus 版本是 3.5.2 版本。

案发现场还原

/**
* 先保存通知消息,在批量保存用户通知记录
*/
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveNotice(Notify notify, String receiveUserIds) {
long begin = System.currentTimeMillis();
notify.setCreateTime(new Date());
notify.setCreateBy(ShiroUtil.getSessionUid());
if (notify.getPublishTime() == null) {
notify.setPublishTime(new Date());
}
boolean insert = save(notify);
List<NotifyRecord> collect = new ArrayList<>();
List<String> receiveUserList = fillNotifyRecordList(notify, receiveUserIds, collect);
notifyRecordService.saveBatch(collect);
long end = System.currentTimeMillis();
System.out.println(end - begin);
...
return insert;
} /**
* 根据用户id,组装用户通知记录集合,返回200条记录
*/
public List<String> fillNotifyRecordList(Notify notify, String receiveUserIds, List<NotifyRecord> collect) {
List<String> noticeRecordList = new ArrayList<>(200);
...
// 组将两百条用户通知记录
return noticeRecordList;
}

如上代码,我有一个 saveNotice() 方法用于保存通知消息以及用户通知记录。执行逻辑如下,

  1. 保存通知消息
  2. 根据用户 id,组装用户通知记录集合,返回 200 条用户通知记录
  3. 批量保存用户通知记录集合

前两步骤耗时都很少,我们直接看第三步操作耗时,结合 sql 执行日志,如下,

-- slow sql 5542 millis. INSERT INTO oa_notify_record  ( notifyId, receiveUserId, receiveUserName, isRead,  createTime )  VALUES  ( ?, ?, ?, ?,  ? )[225,"fcd90fe3990e505d07c90a238f75e9c1","niuwawa",false,"2023-10-30 23:54:04"]
5681

再结合 mybatis free log 插件打印完整 sql 如下图,

可以看出,我们批量保存用户通知记录是一条一条保存得,已经可以猜测就是批量插入方法导致耗时较高。

这里使用 mybatis log free 插件,它可以自动帮我们在控制台打印完整得 mybatis sql 语句。有需要可以在 idea 插件中心搜索 mybatis log free 下载安装。

结合 saveBatch() 底层源码也能够看出,mybatis plus 对于批量操作是在 executeBatch() 方法内使用 for 循环执行插入操作得,源码如下图,

到这里我们应该也能猜出了在测试环境执行较快得原因,因为在测试环境需要批量保存得用户通知记录比较少,只有几条记录,所以很快。但是上预发布后,由于预发布中需要批量保存得用户通知记录比较多达到了数百条,所以执行较慢,耗时达到了 5、6 秒之久。

由上述源码可以看出,mybatis plus 的批量操作底层使用的还是 mybatis 提供的 batch 模式实现批量插入以及更新的。而 mybatis 提供的 batch 模式操作底层使用的还是 jdbc 驱动提供的批量操作模式,jdbc 批量操作示例代码如下,

public static void main(String[] args) {
Connection conn = null;
PreparedStatement statement = null;
try {
// 数据库连接
String url = "jdbc:mysql://*************?autoReconnect=true&nullCatalogMeansCurrent=true&failOverReadOnly=false&useUnicode=true&characterEncoding=UTF-8";
String user = "******";
String password = "************";
// 添加批处理参数
// url = url + "&rewriteBatchedStatements=true";
// 加载驱动类
Class.forName("com.mysql.cj.jdbc.Driver");
// 创建连接
conn = DriverManager.getConnection(url, user, password);
// 创建预编译 sql 对象
statement = conn.prepareStatement("UPDATE table_test_demo set code = ? where id = ?");
long a = System.currentTimeMillis(); // 计时
// 这里添加 100 个批处理参数
for (int i = 1; i <= 100; i++) {
statement.setString(1, "测试1");
statement.setInt(2, i);
statement.addBatch(); // 批量添加
} long b = System.currentTimeMillis(); // 计时
System.out.println("添加参数耗时:" + (b-a)); // 计时 int[] r = statement.executeBatch(); // 批量提交
statement.clearBatch(); // 清空批量添加的 sql 命令列表缓存 long c = System.currentTimeMillis(); // 计时
System.out.println("执行sql耗时:" + (c-b)); // 计时
} catch (Exception e) {
e.printStackTrace();
} finally {
// 主动释放资源
try {
if (statement != null) {
statement.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
  • statement.addBatch() 将 sql 语句打包到一个容器中
  • statement.executeBatch() 将容器中的 sql 语句提交
  • statement.clearBatch() 清空容器,为下一次打包做准备

推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。

github 地址:https://github.com/wayn111/waynboot-mall

那么问题出现在哪里了?明明已经使用了批量操作,但耗时还是很慢,别急,跟着我往下看。

解决方法

到这里,也就是本文得重点所在了,那怎么解决这个问题嘞?如何既利用 mybatis plus 提供得便携性,也能够解决批量操作耗时较高得问题。

虽然我们使用了 mybatis plus -> mybatis -> jdbc 这一条批量操作链路,但是其实我们还需要在 jdbcurl 上添加一个 rewriteBatchedStatements=true 参数即可解决这个问题。

MySQL 的 JDBC 连接的 url 中要加 rewriteBatchedStatements 参数,并保证 5.1.13 以上版本的驱动,才能实现高性能的批量插入。

MySQL JDBC 驱动在默认情况下会无视 executeBatch()语句,把我们期望批量执行的一组 sql 语句拆散,一条一条地发给 MySQL 数据库,批量插入实际上是单条插入,直接造成较低的性能。只有把 rewriteBatchedStatements 参数置为 true, 驱动才会帮你批量执行 SQL。另外这个选项对 INSERT/UPDATE/DELETE 都有效。

rewriteBatchedStatements=true 的意思是,当你在 Java 程序中使用批量插入/修改/删除(batching)时,MySQL JDBC 驱动程序将尝试重新编写(rewrite)你的 SQL 语句,以便更有效地执行这些批量插入操作。

OK,在我们给 jdbcurl 上添加了参数后,看看效果,如下图,

可以看到 jdbcurl 添加了 rewriteBatchedStatements=true 参数后,批量操作的执行耗时已经只有 200 毫秒,自此也就解决了 mybatis plus 提供的 saveBatch() 方法执行耗时较高得问题。

总结

mybatis plus 给开发人员带来了很多便利,但是其中也有一些坑点,比如上文所提到得批量操作耗时问题,如果不注意的话,就有可能调入坑里,各位开发同学可以检查自己或者公司项目中 jdbcurl 是否缺失 rewriteBatchedStatements=true 参数,加以改正,避免重复掉入这个坑里。

mybatis plus很好,但是我被它坑了!的更多相关文章

  1. IDEA中写MyBatis的xml配置文件编译报错的坑

    IDEA中写MyBatis的xml配置文件编译报错的坑 说明:用IDEA编译工具在项目中使用Mybatis框架,编写mybatis-config.xml和Mapper.xml配置文件时,编译项目出现错 ...

  2. 使用MyBatis的动态SQL表达式时遇到的“坑”(integer)

    现有一项目,ORM框架使用的MyBatis,在进行列表查询时,选择一状态(值为0)通过动态SQL拼接其中条件但无法返回正常的查询结果,随后进行排查. POJO private Integer stat ...

  3. mybatis逆向工程的text类型的一个小坑

    数据库如果配有text的数据类型的 mybatis生成逆向工程的时候会单独将text提取出来 ByExampleWithBLOBs 会生成上面后缀的查询和修改的语句 因此查询起来会产生没有必要的麻烦, ...

  4. 服务器CPU很高-怎么办(Windbg使用坑点)

    最近,碰到了一个线上CPU服务器很高的问题,并且也相当紧急,接到这个任务后,我便想到C#性能分析利器,Windbg. 终于在折腾半天之后,找出了问题,成功解决,这里就和大家分享一下碰到的问题. 问题1 ...

  5. Mybatis 批量操作以及多参数操作遇到的坑

    查考地址:https://blog.csdn.net/shengtianbanzi_/article/details/80147134 待整理中......

  6. mybatis返回自增主键问题踩坑

    1 <insert id="insert" keyProperty="id" useGeneratedKeys="true"
 par ...

  7. springboot mybatis 多数据源配置支持切换以及一些坑

    一 添加每个数据源的config配置,单个直接默认,多个需要显示写出来 @Configuration @MapperScan(basePackages ="com.zhuzher.*.map ...

  8. 这份Mybatis总结,我觉得你很需要!

    前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y Mybatis应该是国内用得最多的「数据访问层」 ...

  9. 【Java EE 学习 79 上】【mybatis 基本使用方法】

    一.简介 mybatis类似于hibernate,都是简化对数据库操作的框架,但是和hibernate不同的是,mybatis更加灵活,整体来说框架更小,这体现在它需要我们手写SQL语句,而hiber ...

  10. MyBatis与Hibernate对比

    一.相同点 都屏蔽 jdbc api 的底层访问细节,使用我们不用与 jdbc api 打交道,就可以访问数据. jdbc api 编程流程固定,还将 sql 语句与 java 代码混杂在了一起,经常 ...

随机推荐

  1. python:修改pdf的书签

    我觉得修改pdf书签总体来说最方便的方式就是: 导出pdf书签为文本文件,修改书签文本文件后再导入到pdf中. 1.直接修改pdf书签 python中比较好用的pdf处理的库是pymupdf: pip ...

  2. golang channel 未关闭导致的内存泄漏

    现象 某一个周末我们的服务 oom了,一个比较重要的job 没有跑完,需要重跑,以为是偶然,重跑成功,因为是周末没有去定位原因 又一个工作日,它又oom了,重跑成功,持续观察,job 在oom之前竟然 ...

  3. cgroup Linux中的资源限制

    参考链接:容器技术的基石:cgroup 直接上实验: # docker run --rm -d --cpus=0.1 --memory=100M --name=test redis:alpine WA ...

  4. javascript中一些难以理解的专有名词 1(也不是很专有)

    变量提升 变量提升:是指js代码执行过程中,js引擎把变量的声明和函数的声明提升到代码的开头的"行为". 变量和函数在代码里的位置是不会变的,而是在编译阶段被js引擎放入内存中. ...

  5. Kubernetes: Kubectl 源码分析

    0. 前言 kubectl 看了也有一段时间,期间写了两篇设计模式的文章,是时候对 kubectl 做个回顾了. 1. kubectl 入口:Cobra kubectl 是 kubernetes 的命 ...

  6. K8S | Service服务发现

    服务发现与负载均衡. 一.背景 在微服务架构中,这里以开发环境「Dev」为基础来描述,在K8S集群中通常会开放:路由网关.注册中心.配置中心等相关服务,可以被集群外部访问: 对于测试「Tes」环境或者 ...

  7. [linux]frp内网穿透

    前言 假设有如下网络拓扑 A可以访问B,但B无法访问A.A和B都能访问C.如果B需要访问A的8000端口,一般有如下方法: 网络管理员做路由转发.硬件层面网络转发,性能一般来说更好,但需要熟悉路由配置 ...

  8. 个人用C#编写的壁纸管理器 - 开源研究系列文章

    今天介绍一下笔者自己用C#开发的一个小工具软件:壁纸管理器. 开发这个小工具的初衷是因为Windows操作系统提供的功能个人不满意,而且现在闲着,所以就随意写了个代码.如果对读者有借鉴参考作用就更好了 ...

  9. 《深入理解Java虚拟机》读书笔记:垃圾收集器

    垃圾收集器 HotSpot虚拟机包含的所有收集器如图3-5所示.图3-5展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用. 新生代收集器:Serial.ParNew ...

  10. 程序员 不得不知道的 API 接口常识

    说实话,我非常希望自己能早点看到本篇文章,大学那个时候懵懵懂懂,跟着网上的免费教程做了一个购物商城就屁颠屁颠往简历上写. 至今我仍清晰地记得,那个电商教程是怎么定义接口的: 管它是增加.修改.删除.带 ...