Mybatis 批处理数据优化

前言

        批处理是 JDBC 编程中的另一种优化手段。JDBC 在执行 SQL 语句时,会将 SQL 语句以及实参通过网络请求的方式发送到数据库,一次执行一条 SQL 语句,一方面会减小请求包的有效负载,另一个方面会增加耗费在网络通信上的时间。

  通过批处理的方式,我们就可以在 JDBC 客户端缓存多条 SQL 语句,然后在 flush 或缓存满的时候,将多条 SQL 语句打包发送到数据库执行,这样就可以有效地降低上述两方面的损耗,从而提高系统性能。

       不过,有一点需要特别注意:

每次向数据库发送的 SQL 语句的条数是有上限的,如果批量执行的时候超过这个上限值,数据库就会抛出异常,拒绝执行这一批 SQL 语句,

所以我们需要控制批量发送 SQL 语句的条数和频率。

 

分析

我们知道每个SqlSession都会拥有一个Executor对象,这个对象才是执行 SQL 语句的幕后黑手,

我们也知道Spring跟Mybatis整合的时候使用的SqlSessionSqlSessionTemplate,默认用的是ExecutorType.SIMPLE,这个时候你通过自动注入获得的Mapper对象其实是没有开启批处理的;

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

 

那么我们实际上是需要通过sqlSessionFactory.openSession(ExecutorType.BATCH)得到的sqlSession对象(此时里面的ExecutorBatchExecutor)去获得一个新的Mapper对象才能生效!!!

 

示例

package com.new3s.gasanalysis.common.util;

import com.new3s.gasanalysis.common.exception.CoreServiceException;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.annotation.Resource;
import java.util.List;
import java.util.function.BiFunction;

/**
 * @Description mybatis批处理数据工具类
 * @Date 2022-06-09 14:37
 * @Author xie
 */
@Component
public class MybatisBatchUtils {

    /**
     * 每次处理1000条
     */
    private static final int BATCH_SIZE = 1000;

    @Resource
    private SqlSessionFactory sqlSessionFactory;


    /**
     * 批量处理修改或者插入
     *
     * @param data     需要被处理的数据
     * @param mapperClass  Mybatis的Mapper类
     * @param function 自定义处理逻辑
     * @return int 影响的总行数
     */
    public <T,U,R> int batchUpdateOrInsert(List<T> data, Class<U> mapperClass, BiFunction<T, U, R> function) {
        int i = 1;
        // SqlSession 修改为批处理类型(获得一个新的Mapper对象才能生效)
        SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        try {
            U mapper = batchSqlSession.getMapper(mapperClass);
            int size = data.size();
            for (T element : data) {
                function.apply(element,mapper);
                if ((i % BATCH_SIZE == 0) || i == size) {
                    batchSqlSession.flushStatements();
                }
                i++;
            }
            // 非事务环境下强制commit,事务情况下该commit相当于无效
            batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
        } catch (Exception e) {
            batchSqlSession.rollback();
            throw new CoreServiceException();
        } finally {
            batchSqlSession.close();
        }
        return i - 1;
    }

}

 

调用(单纯为了演示)

@Autowired
private MybatisBatchUtils mybatisBatchUtils;

@Override
@Transactional(rollbackFor = {Exception.class, RuntimeException.class})
public ResMsg<StationVo> addStation(StationVo stationVo) throws IOException, InterruptedException {

    // 测试数据,实际情况可能会从文本中读取
    List<StationVo> stationList = getStationList(StationDto.builder().stationName(stationVo.getStationName()).build()).getData();

    mybatisBatchUtils.batchUpdateOrInsert(stationList, StationMapper.class, (item, stationMapper) -> stationMapper.insert(item));
    
    return ResMsg.success();
}

 

附:Oracle批量插入优化 

我们都知道Oracle主键序列生成策略跟MySQL不一样,我们需要弄一个序列生成器,这里就不详细展开描述了,然后Mybatis Generator生成的模板代码中,insert的id是这样获取的 

<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">
  select XXX.nextval from dual
</selectKey> 

如此,就相当于你插入1万条数据,其实就是insert和查询序列合计预计2万次交互,耗时竟然达到10s多。我们改为用原生的Batch插入,这样子的话,只要500多毫秒,也就是0.5秒的样子 

<insert id="insert" parameterType="user">
    insert into table_name(id, username, password)
    values(SEQ_USER.NEXTVAL,#{username},#{password})
</insert>

 

posted @ 2022-06-09 15:01  xiexie0812  阅读(357)  评论(0编辑  收藏  举报