一次代码review,大佬说了算
记得有一次我们小组code review,组长看了下我们批量插入是使用mybatis原生的xml foreach实现的,于是二话不说,拍桌子,说这有性能问题。叫我们直接使用mybatis-plus,可是为啥呢?怎么用,需要注意哪些地方,也没给我们说个明白。好吧,我们对这一块也没具体调研过,就直接按他的想法去实现了。性能有没有提升了好几倍呢,其实也没实践过,反正review过了。直到有一天。。。
mybatis-plus批量插入就一定比mybatis原生foreach强?
实践是检验真理的唯一标准,我们分别使用mp批量插入方法和mybatis foreach来验证
相关环境准备
引入 mybatis-plus
代码语言:txt复制 <dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
mapper及对应xml
代码语言:txt复制public interface UserMapper extends BaseMapper<UserInfo> {
/**
* 原生批量插入
* @param list
* @return
*/
int saveBatchByNative(@Param("list") List<UserInfo> list);
}
代码语言:txt复制 <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 调用
代码语言:txt复制@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,如
代码语言:txt复制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 批量实现
代码语言:txt复制 @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
代码语言:txt复制@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 方法,作者对这个方法的解释是
代码语言:txt复制 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,同样,看下解释
代码语言:txt复制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
代码语言:txt复制 @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
代码语言:txt复制 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也比较小,那使用原生的问题也不大