mybatis-plus批量插入你用对了吗
一次代码review,大佬说了算
记得有一次我们小组code review,组长看了下我们批量插入是使用mybatis原生的xml foreach实现的,于是二话不说,拍桌子,说这有性能问题。叫我们直接使用mybatis-plus,可是为啥呢?怎么用,需要注意哪些地方,也没给我们说个明白。好吧,我们对这一块也没具体调研过,就直接按他的想法去实现了。性能有没有提升了好几倍呢,其实也没实践过,反正review过了。直到有一天。。。
mybatis-plus批量插入就一定比mybatis原生foreach强?
实践是检验真理的唯一标准,我们分别使用mp批量插入方法和mybatis foreach来验证
相关环境准备
引入 mybatis-plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
mapper及对应xml
public interface UserMapper extends BaseMapper<UserInfo> {
/**
* 原生批量插入
* @param list
* @return
*/
int saveBatchByNative(@Param("list") List<UserInfo> list);
}
<insert id="saveBatchByNative" >
INSERT INTO `t_user`(`name`,`age`,`descr`) VALUES
<foreach collection="list" separator="," item="item">
(#{item.name},#{item.age},#{item.descr})
</foreach>
</insert>
按照惯例,controller及service 调用
@Service("mybatisUserService")
public class UserService extends ServiceImpl<UserMapper,UserInfo> {
@Autowired
private UserMapper userMapper;
@Transactional(rollbackFor = Exception.class)
public void insertBatchByPlus(int maxInsert){
List<UserInfo> users = getUsers(maxInsert);
long start = System.currentTimeMillis();
boolean insert = this.saveBatch(users,1000);
System.out.println("mp batch insert row :"+maxInsert+" and spend time(ms) :"+(System.currentTimeMillis()-start));
}
@Transactional(rollbackFor = Exception.class)
public void insertBatchByNative(int maxInsert){
List<UserInfo> users = getUsers(maxInsert);
long start = System.currentTimeMillis();
int insert = userMapper.saveBatchByNative(users);
System.out.println("native batch insert row :"+insert+" and spend time(ms) :"+(System.currentTimeMillis()-start));
}
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/insert")
public void addUser(@RequestParam(name = "max") int max){
// userService.insert();
userService.insertBatchByNative(max);
userService.insertBatchByPlus(max);
}
}
测试结果
分别测 10000 、50000,执行结果截图
意外吧!基于前面的环境,mybatis-plus并没有占优势,反而慢得离谱。难道大佬是瞎说的?其实也不全是。。
开启批量插入
数据源配置url参数加上 rewriteBatchedStatements=true,如
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/study_01?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=xxxxx
再次测试结果
还是一样,分别测试10000、50000 ,执行结果截图
咋一看,mybatis并没有差太多。于是,再调下,mp 的batchsize成2000(前面设置了1000)
这不太对,并非越大越好。于是,调低 ,batchsize = 200
是有一丢丢优势,不过,也并非batchsize越小越好,因为越小不就是和one by one 差不多了吗
难道,开了批量参数rewriteBatchedStatements,mp就一定比mybatis快了?也并非如此,看下
可能遇到的问题
- 关于max_allowed_packet的配置,异常表示当前发送的包太大了
可通过my.cnf 或者 set global max_allowed_packet = 10485760 配置,它们的区别在于重启之后配置的有效性
粗略看下执行流程
mybatis-plus 批量实现
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = sqlStatement(SqlMethod.INSERT_ONE);
try (SqlSession batchSqlSession = sqlSessionBatch()) {
int i = 0;
for (T anEntityList : entityList) {
batchSqlSession.insert(sqlStatement, anEntityList);
if (i >= 1 && i % batchSize == 0) {
//按我们传入的batchsize 分批插入
//不过,是否真的分批还得往下看,也有可能一种假象
batchSqlSession.flushStatements();
}
i++;
}
batchSqlSession.flushStatements();
}
return true;
}
最终走到 com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchInternal
@Override
protected long[] executeBatchInternal() throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
//省略一些无关代码
try {
//省略一些无关代码
// batchHasPlainStatements 默认为false
if (!this.batchHasPlainStatements && this.rewriteBatchedStatements.getValue()) {
if (((PreparedQuery<?>) this.query).getParseInfo().canRewriteAsMultiValueInsertAtSqlLevel()) {
return executeBatchedInserts(batchTimeout);
}
if (!this.batchHasPlainStatements && this.query.getBatchedArgs() != null
&& this.query.getBatchedArgs().size() > 3 /* cost of option setting rt-wise */) {
return executePreparedBatchAsMultiStatement(batchTimeout);
}
}
//如果不开启 rewriteBatchedStatements 则走这个逻辑
return executeBatchSerially(batchTimeout);
} finally {
this.query.getStatementExecuting().set(false);
clearBatch();
}
}
}
先看下executeBatchSerially 方法,作者对这个方法的解释是
protected long[] executeBatchSerially(int batchTimeout) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
// 省略一些无关代码
//nbrCommands 其实就是 batchsize,
// 所以对于该方法,本质就是通过遍历,一条一条插入。
for (batchCommandIndex = 0; batchCommandIndex < nbrCommands; batchCommandIndex++) {
// 省略一些无关代码
// 关注下 executeUpdateInternal 这个方法很重要,可以理解为一次跟server端交互对sql的执行过程
>) arg;
updateCounts[batchCommandIndex] = executeUpdateInternal(queryBindings, true);
// 省略一些无关代码
}
return (updateCounts != null) ? updateCounts : new long[0];
}
}
再看下com.mysql.cj.jdbc.ClientPreparedStatement#executeBatchedInserts,同样,看下解释
protected long[] executeBatchedInserts(int batchTimeout) throws SQLException {
// 省略一些无关代码
for (int i = 0; i < numberArgsToExecute; i++) {
// numValuesPerBatch == numberArgsToExecute 就不会满足
if (i != 0 && i % numValuesPerBatch == 0) {
//一般不会执行到这里,除非批量执行sql 的 size 超过了 max_allowed_packet
try {
updateCountRunningTotal += batchedStatement.executeLargeUpdate();
} catch (SQLException ex) {
sqlEx = handleExceptionForBatch(batchCounter - 1, numValuesPerBatch, updateCounts, ex);
}
getBatchedGeneratedKeys(batchedStatement);
batchedStatement.clearParameters();
batchedParamIndex = 1;
}
//通过遍历给每个sql进行参数设置
batchedParamIndex = setOneBatchedParameterSet(batchedStatement, batchedParamIndex, this.query.getBatchedArgs().get(batchCounter++));
}
try {
//执行批量插入,其实也就是调用了executeUpdateInternal方法
updateCountRunningTotal += batchedStatement.executeLargeUpdate();
} catch (SQLException ex) {
sqlEx = handleExceptionForBatch(batchCounter - 1, numValuesPerBatch, updateCounts, ex);
}
// 省略一些无关代码
}
/**
* JDBC 4.2
* Same as PreparedStatement.executeUpdate() but returns long instead of int.
*/
public long executeLargeUpdate() throws SQLException {
return executeUpdateInternal(true, false);
}
可以看出 executeBatchSerially和executeBatchedInserts 最终都调用 executeUpdateInternal方法(可以简单的理解为jdbc和server服务端一次通信的过程),区别在于是否通过遍历一条一条的发送;
mybatis foreach 执行过程
由于引入的mybatis-plus ,mapper代理类是MybatisMapperProxy
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
最终走到com.mysql.cj.jdbc.ClientPreparedStatement#execute
public boolean execute() throws SQLException {
// 省略一些无关代码
Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket();
// 一次打包发送到server端
rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(),
(((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S'), cachedMetadata, false);
// 省略一些无关代码
}
可以看出,对于mybatis foreach 批量插入,直接就是一次交互过程,跟是否开启rewriteBatchedStatements无关,唯一有关的就是max_allowed_packet的设置
最后
- mybatis原生foreach批量插入,抛开网络阻塞问题,其耗时并非想的差劲。但是,为何在大数据量批量插入场景下不推荐使用,可能就是考虑网络阻塞以及server端处理涉及到长事务问题吧;
- 使用mybatis-plus 批量插入,需要开启rewriteBatchedStatements,且合理设置batchsize,该参数会影响客户端与服务端通信交互次数。如果batchsize太大,超过最大packet,jdbc底层还是会再一次分批,反而影响性能;
- 批量操作用原生还是mybatis-plus?我觉得应该有个范围,比如实际场景一次批量插入最多也就几百条且size也比较小,那使用原生的问题也不大
相关文章
- MyBatis 批量插入别再乱用 foreach 了,5000 条数据花了 14 分钟...
- pxe+kickstart服务器批量无人值守安装脚本
- MyBatis——【入门级】简单了解myBatis
- MyBatis框架:第二章:传统mybatis的hello world 示例
- MyBatis框架:第三章:传统方式mybatis的增,删,改,查实现
- MyBatis框架:第十章:mybatis缓存
- MyBatis框架:第十一章:mybatis 逆向工程
- 对图片批量重命名_重命名批处理最大量
- MyBatis框架:第三章:传统方式mybatis的增,删,改,查实现
- kafka批量删除topic_kafka查看topic数据量
- 批量梯度下降算法
- 浅谈MyBatis批量插入方法,10w条数据处理仅需2秒
- Mybatis插件better-mybatis-generator的下载与使用
- MyBatis MapperProvider MessageFormat拼接批量SQL语句执行报错的原因分析及解决办法
- Mybatis操作MySQL存储过程实现数据库交互(mybatis调用mysql存储过程)
- 批量执行Oracle脚本实现智能化管理(oracle批处理脚本)
- MySQL 批量修改表名:快捷而有效的方法(mysql批量修改表名)
- Linux 批量重命名:一个简单的方法(批量重命名linux)
- 如何使用 MySQL 进行批量数据替换?(mysql批量替换数据)
- Linux批量更名,一键轻松实现(linux批量改名)
- 利用Redis远程实现批量删除(redis远程批量删除)
- Redis批量远程删除的技术挑战(redis远程批量删除)
- 深入解析phpCB批量转换的代码示例