Mybatis 批处理数据优化
前言
批处理是 JDBC 编程中的另一种优化手段。JDBC 在执行 SQL 语句时,会将 SQL 语句以及实参通过网络请求的方式发送到数据库,一次执行一条 SQL 语句,一方面会减小请求包的有效负载,另一个方面会增加耗费在网络通信上的时间。
通过批处理的方式,我们就可以在 JDBC 客户端缓存多条 SQL 语句,然后在 flush 或缓存满的时候,将多条 SQL 语句打包发送到数据库执行,这样就可以有效地降低上述两方面的损耗,从而提高系统性能。
不过,有一点需要特别注意:
每次向数据库发送的 SQL 语句的条数是有上限的,如果批量执行的时候超过这个上限值,数据库就会抛出异常,拒绝执行这一批 SQL 语句,
所以我们需要控制批量发送 SQL 语句的条数和频率。
分析
我们知道每个SqlSession
都会拥有一个Executor
对象,这个对象才是执行 SQL 语句的幕后黑手,
我们也知道Spring跟Mybatis整合的时候使用的SqlSession
是SqlSessionTemplate
,默认用的是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
对象(此时里面的Executor
是BatchExecutor
)去获得一个新的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>
欢迎一起来学习和指导,谢谢关注!
本文来自博客园,作者:xiexie0812,转载请注明原文链接:https://www.cnblogs.com/mask-xiexie/p/16359359.html