记录一次用mybatis-plus时saveOrUpdate的坑

背景

每天都需要拉取对应广告平台的子账号数据落库,然后已存在的数据更新,新增数据插入,顺手就用了mybatis-plus的saveOrUpdate方法。

但是跑起来发现慢的离谱,五百条记录用了8s多。

排查思路

看了下慢sql:
slow sql 8601 millis. INSERT INTO ad_xxx_daily (biz_key, record_date, ...) VALUES (?, ?, ...)
都是单条插入,然后看了下Druid的StatFilter对批量的计时:

statementExecuteBatchBefore(statement);          // 记下 lastExecuteStartNano
int[] updateCounts = chain.statement_executeBatch(statement);
statementExecuteBatchAfter(statement, updateCounts);  // 算 elapsed,超阈值就打日志

可以看到是在excecuteBatch()方法之前启动,执行完停止,是总共的所有记录

8600 / 502 ≈ 17ms,差不多就是一次网络往返时间,那就看数据库连接,发现没有添加rewriteBatchedStatements批量参数:

jdbc:mysql://host:3306/db?...&rewriteBatchedStatements=true

结束了吗?还没有!

上面改完后首次全量插入很快了,但是第二天发现依然很慢,看了下大部分走的是更新,这里有个隐藏坑
下面是源码:

  @Transactional(rollbackFor = Exception.class)
      @Override
      public boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize) {
          TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
          Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
          String keyProperty = tableInfo.getKeyProperty();
          Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!");
          return SqlHelper.saveOrUpdateBatch(this.entityClass, this.mapperClass, this.log, entityList, batchSize, (sqlSession, entity) -> {
              Object idVal = tableInfo.getPropertyValue(entity, keyProperty);
              return StringUtils.checkValNull(idVal)
                  || CollectionUtils.isEmpty(sqlSession.selectList(getSqlStatement(SqlMethod.SELECT_BY_ID), entity));
          }, (sqlSession, entity) -> {
              MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
              param.put(Constants.ENTITY, entity);
              sqlSession.update(getSqlStatement(SqlMethod.UPDATE_BY_ID), param);
          });
    }

    /**
     * 批量更新或保存
     *
     * @param entityClass 实体
     * @param log         日志对象
     * @param list        数据集合
     * @param batchSize   批次大小
     * @param predicate   predicate(新增条件) notNull
     * @param consumer    consumer(更新处理) notNull
     * @param <E>         E
     * @return 操作结果
     * @since 3.4.0
     */
    public static <E> boolean saveOrUpdateBatch(Class<?> entityClass, Class<?> mapper, Log log, Collection<E> list, int batchSize, BiPredicate<SqlSession, E> predicate, BiConsumer<SqlSession, E> consumer) {
        String sqlStatement = getSqlStatement(mapper, SqlMethod.INSERT_ONE);
        return executeBatch(entityClass, log, list, batchSize, (sqlSession, entity) -> {
            if (predicate.test(sqlSession, entity)) {
                sqlSession.insert(sqlStatement, entity);
            } else {
                consumer.accept(sqlSession, entity);
            }
        });
    }

可以看到每次都会一条一条查出是否有这条主键id,如果没有就会插入一条,有则更新。

那么就变成了每次查一次更新一次

修复

  1. 连接串打开批量改写
&rewriteBatchedStatements=true
  1. 不用 saveOrUpdateBatch,自己分两批

在内存里做 merge 的时候,本来就知道每一行是新增还是更新 —— 从库里查出来的有 id,新构造的 id 为 null。那个 selectById 就完全是多余的了。

public final class BatchRows<T> {
    private final List<T> toInsert = new ArrayList<>();
    private final List<T> toUpdate = new ArrayList<>();
    public void insert(T row) { toInsert.add(row); }
    public void update(T row) { toUpdate.add(row); }
    public void flush(IService<T> service) {
        if (!toInsert.isEmpty()) { service.saveBatch(toInsert); }
        if (!toUpdate.isEmpty()) { service.updateBatchById(toUpdate); }
    }
}

saveBatch 和 updateBatchById 内部不含任何查询,批次能完整攒到默认的 1000 条再刷新(IService.DEFAULT_BATCH_SIZE = 1000,见 SqlHelper.executeBatch),配合 rewrite 就是真正的批量。

结论

数据库记得开批量改写
批量尽量不要用saveOrUpdateBatch() !

posted @ 2026-09-17 15:14  TyrionZhang  阅读(3)  评论(0)    收藏  举报