记录一次用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,如果没有就会插入一条,有则更新。
那么就变成了每次查一次更新一次
修复
- 连接串打开批量改写
&rewriteBatchedStatements=true
- 不用 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() !

浙公网安备 33010602011771号