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

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

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

案发现场还原

/** * 先保存通知消息,在批量保存用户通知记录 */@Transactional(rollbackFor = Exception.class)@Overridepublic 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 collect = new ArrayList();    List receiveUserList = fillNotifyRecordList(notify, receiveUserIds, collect);    notifyRecordService.saveBatch(collect);    long end = System.currentTimeMillis();    System.out.println(end - begin);    ...    return insert;}/** * 根据用户id,组装用户通知记录集合,返回200条记录 */public List fillNotifyRecordList(Notify notify, String receiveUserIds, List collect) {    List 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参数,加以改正,避免重复掉入这个坑里。