记得有一次我们小组code review,组长看了下我们批量插入是使用mybatis原生的xml foreach实现的,于是二话不说,拍桌子,说这有性能问题。叫我们直接使用mybatis-plus,可是为啥呢?怎么用,需要注意哪些地方,也没给我们说个明白。好吧,我们对这一块也没具体调研过,就直接按他的想法去实现了。性能有没有提升了好几倍呢,其实也没实践过,反正review过了。直到有一天。。。
实践是检验真理的唯一标准,我们分别使用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快了?也并非如此,看下
可通过my.cnf 或者 set global max_allowed_packet = 10485760 配置,它们的区别在于重启之后配置的有效性
@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-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的设置
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。