mybatis-plus 批量插入效率低的問(wèn)題解決
背景
由于項(xiàng)目中需要大批量將數(shù)據(jù)插入數(shù)據(jù)庫(kù),直接使用mybatis-plus中的批量插入方法,結(jié)果發(fā)現(xiàn)效率奇低無(wú)比,線上批量插入一千條數(shù)據(jù)居然花銷(xiāo)八九秒的時(shí)間。而我們的目標(biāo)是想要單次插入一萬(wàn)條數(shù)據(jù),這樣的效率完全無(wú)法接受。
問(wèn)題追蹤
mybatis-plus的源碼IService中是有單次批量插入的大小,默認(rèn)的DEFAULT_BATCH_SIZE=1000,可以看到很多批量方法里面都有設(shè)置;通過(guò)修改調(diào)用方法的入?yún)⒅?,可以增加單次批量插入的?shù)據(jù),但實(shí)際發(fā)現(xiàn)并沒(méi)有什么提升。以下為mybatis-plus中service源碼:
public interface IService<T> {
/**
* 默認(rèn)批次提交數(shù)量
*/
int DEFAULT_BATCH_SIZE = 1000;
/**
* 插入一條記錄(選擇字段,策略插入)
*
* @param entity 實(shí)體對(duì)象
*/
default boolean save(T entity) {
return SqlHelper.retBool(getBaseMapper().insert(entity));
}
/**
* 插入(批量)
*
* @param entityList 實(shí)體對(duì)象集合
*/
@Transactional(rollbackFor = Exception.class)
default boolean saveBatch(Collection<T> entityList) {
return saveBatch(entityList, DEFAULT_BATCH_SIZE);
}
/**
* 插入(批量)
*
* @param entityList 實(shí)體對(duì)象集合
* @param batchSize 插入批次數(shù)量
*/
boolean saveBatch(Collection<T> entityList, int batchSize);
/**
* 批量修改插入
*
* @param entityList 實(shí)體對(duì)象集合
*/
@Transactional(rollbackFor = Exception.class)
default boolean saveOrUpdateBatch(Collection<T> entityList) {
return saveOrUpdateBatch(entityList, DEFAULT_BATCH_SIZE);
}
/**
* 批量修改插入
*
* @param entityList 實(shí)體對(duì)象集合
* @param batchSize 每次的數(shù)量
*/
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
......
}繼續(xù)接著上插入的源碼研究,發(fā)現(xiàn)底層在SqlHeper類(lèi)中有個(gè)executeBatch的方法有點(diǎn)異常。該方法顯示 sqlSession.flushStatements()的調(diào)用居然是循環(huán)的。也就是說(shuō)sql層面實(shí)際上是一堆insert語(yǔ)句再sqlSession中循環(huán)flush,而不是一個(gè)大insert一次flush操作完。這就是效率低的本質(zhì)原因。
/**
* 執(zhí)行批量操作
*
* @param entityClass 實(shí)體類(lèi)
* @param log 日志對(duì)象
* @param list 數(shù)據(jù)集合
* @param batchSize 批次大小
* @param consumer consumer
* @param <E> T
* @return 操作結(jié)果
* @since 3.4.0
*/
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
int size = list.size();
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if ((i % batchSize == 0) || i == size) {
sqlSession.flushStatements();
}
i++;
}
});
}解決方案
因?yàn)樯鲜鲈颍紤]自己寫(xiě)一個(gè)批量插入的sql語(yǔ)句,這是最簡(jiǎn)單的。
但我們此處不采取此方法,而是直接重寫(xiě)sql注入。以下DefaultSqlInjector為mybatis-plus默認(rèn)的sql注入實(shí)現(xiàn)類(lèi),繼承的是AbstractSqlInjector類(lèi)。該默認(rèn)實(shí)現(xiàn)類(lèi)中添加的Insert、Delete、DeleteByMap等等,實(shí)際上就是對(duì)應(yīng)Mapper中所調(diào)用的各種方法。
/**
* SQL 默認(rèn)注入器
*
* @author hubin
* @since 2018-04-10
*/
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new Delete(),
new DeleteByMap(),
new DeleteById(),
new DeleteBatchByIds(),
new Update(),
new UpdateById(),
new SelectById(),
new SelectBatchByIds(),
new SelectByMap(),
new SelectOne(),
new SelectCount(),
new SelectMaps(),
new SelectMapsPage(),
new SelectObjs(),
new SelectList(),
new SelectPage()
).collect(toList());
}
}我們需要在默認(rèn)實(shí)現(xiàn)的基礎(chǔ)上將額外的sql注入進(jìn)去,所以直接繼承默認(rèn)的實(shí)現(xiàn)類(lèi)做改進(jìn)。以下為源碼:
/**
* 重寫(xiě)DefaultSqlInjector
*/
public class SqlInjectorPlus extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
//繼承原有方法
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
//注入新方法
methodList.add(new InsertBatchSomeColumn());
return methodList;
}
}
/**
* 注入
*/
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
/**
* 增強(qiáng)sql注入的Bean
*
* @return
*/
@Bean
public SqlInjectorPlus sqlInjectorPlus() {
return new SqlInjectorPlus();
}
}
/**
* 重寫(xiě)B(tài)aseMapper
*/
public interface BaseMapperPlus <T> extends BaseMapper<T> {
/**
* 高效率批量插入
* entityList數(shù)量不能太大,否則存在丟包問(wèn)題;
* 單次entityList數(shù)量務(wù)必控制在一萬(wàn)內(nèi),或者在service中再次封裝控制數(shù)量;
* @param entityList 數(shù)據(jù)列表
* @return 成功標(biāo)示
*/
Integer insertBatchSomeColumn(Collection<T> entityList);
}SqlInjectorPlus繼承 DefaultSqlInjector 進(jìn)行重寫(xiě),繼承原有增刪改查方法,加入新方法InsertBatchSomeColumn(該類(lèi)為mybatis-plus源碼中就有的,但并未放開(kāi)來(lái)使用)。用MybatisPlusConfig 將Bean注入到spring管理,最后再重寫(xiě)一個(gè)BaseMapper并加入新方法。后續(xù)的mapper直接繼承BaseMapperPlus 就可以調(diào)用批量插入的insertBatchSomeColumn方法了。
InsertBatchSomeColumn類(lèi)
該類(lèi)為mybatis-plus源碼中就有的,但并未放開(kāi)來(lái)使用。主要目的就是實(shí)現(xiàn)批量插入,生產(chǎn)的是一個(gè)單個(gè)大insert語(yǔ)句,注意如果數(shù)據(jù)量也不宜過(guò)大。因?yàn)閱未蝔lush一個(gè)大sql過(guò)去,如果數(shù)據(jù)量過(guò)大,產(chǎn)生丟包,則會(huì)導(dǎo)致該此批量插入失敗。最好再service中再封裝一次,做成分批循環(huán)調(diào)用。該類(lèi)的源碼如下:
/**
* 批量新增數(shù)據(jù),自選字段 insert
* <p> 不同的數(shù)據(jù)庫(kù)支持度不一樣!!! 只在 mysql 下測(cè)試過(guò)!!! 只在 mysql 下測(cè)試過(guò)!!! 只在 mysql 下測(cè)試過(guò)!!! </p>
* <p> 除了主鍵是 <strong> 數(shù)據(jù)庫(kù)自增的未測(cè)試 </strong> 外理論上都可以使用!!! </p>
* <p> 如果你使用自增有報(bào)錯(cuò)或主鍵值無(wú)法回寫(xiě)到entity,就不要跑來(lái)問(wèn)為什么了,因?yàn)槲乙膊恢?!! </p>
* <p>
* 自己的通用 mapper 如下使用:
* <pre>
* int insertBatchSomeColumn(List<T> entityList);
* </pre>
* </p>
*
* <li> 注意: 這是自選字段 insert !!,如果個(gè)別字段在 entity 里為 null 但是數(shù)據(jù)庫(kù)中有配置默認(rèn)值, insert 后數(shù)據(jù)庫(kù)字段是為 null 而不是默認(rèn)值 </li>
*
* <p>
* 常用的 {@link Predicate}:
* </p>
*
* <li> 例1: t -> !t.isLogicDelete() , 表示不要邏輯刪除字段 </li>
* <li> 例2: t -> !t.getProperty().equals("version") , 表示不要字段名為 version 的字段 </li>
* <li> 例3: t -> t.getFieldFill() != FieldFill.UPDATE) , 表示不要填充策略為 UPDATE 的字段 </li>
*
* @author miemie
* @since 2018-11-29
*/
@NoArgsConstructor
@AllArgsConstructor
public class InsertBatchSomeColumn extends AbstractMethod {
/**
* 字段篩選條件
*/
@Setter
@Accessors(chain = true)
private Predicate<TableFieldInfo> predicate;
@SuppressWarnings("Duplicates")
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) +
this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(ENTITY_DOT, false) +
this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主鍵處理邏輯,如果不包含主鍵當(dāng)普通字段處理
if (tableInfo.havePK()) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主鍵 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}
@Override
public String getMethod(SqlMethod sqlMethod) {
// 自定義 mapper 方法名
return "insertBatchSomeColumn";
}
}總結(jié):
mybatis-plus批量插入效率低的本質(zhì)原因是底層代碼中在sqlsession中循環(huán)flush的多條insert語(yǔ)句,因此改進(jìn)方案有兩個(gè):1.寫(xiě)一個(gè)sql實(shí)現(xiàn)循環(huán)插入;2.重寫(xiě)DefaultSqlInjector類(lèi),加入自帶的InsertBatchSomeColumn。
到此這篇關(guān)于mybatis-plus 批量插入效率低的問(wèn)題解決的文章就介紹到這了,更多相關(guān)mybatis-plus 批量插入效率低內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Eclipse的Servers視圖中無(wú)法添加Tomcat6/Tomcat7的方法
這篇文章主要介紹了解決Eclipse的Servers視圖中無(wú)法添加Tomcat6/Tomcat7的方法的相關(guān)資料,需要的朋友可以參考下2017-02-02
多個(gè)版本Java切換環(huán)境變量配置的三種高效方法
這篇文章主要為大家詳細(xì)介紹了多個(gè)版本Java切換環(huán)境變量配置的三種高效方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-10-10
Spring整合CXF webservice restful實(shí)例詳解
這篇文章主要為大家詳細(xì)介紹了Spring整合CXF webservice restful的實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
深度解析MyBatis?動(dòng)態(tài)?SQL?與緩存機(jī)制
本文從動(dòng)態(tài)SQL核心語(yǔ)法、緩存實(shí)現(xiàn)原理、性能優(yōu)化及面試高頻問(wèn)題四個(gè)維度,結(jié)合源碼與工程實(shí)踐,系統(tǒng)解析MyBatis的核心特性與最佳實(shí)踐,感興趣的朋友一起看看吧2025-06-06
MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not f
這篇文章主要介紹了MybatisPlus BaseMapper 中的方法全部 Invalid bound statement (not found)的Error處理方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
教你怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧
本篇文章為大家詳細(xì)介紹了怎么用Java數(shù)組和鏈表實(shí)現(xiàn)棧,文中有非常詳細(xì)的代碼示例及注釋,對(duì)正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05

