這篇文章會一步一步帶你從一個新手的角度慢慢揭開批處理的神秘面紗琅攘,對于初次寫Mybatis批處理的同學可能會有很大的幫助,建議收藏點贊~
處理批處理的方式有很多種爆袍,這里不分析各種方式的優(yōu)劣首繁,只是概述 ExecutorType.BATCH 這種的用法,另學藝不精陨囊,如果有錯的地方弦疮,還請大佬們指出更正。
問題原因
在公司寫項目的時候蜘醋,有一個自動對賬的需求挂捅,需要從文件中讀取幾萬條數(shù)據(jù)插入到數(shù)據(jù)庫中,后續(xù)可能跟著業(yè)務的增長,會上升到幾十萬闲先,所以對于插入需要進行批處理操作状土,下面我們就來看看我是怎么一步一步踩坑的。
簡單了解一下批處理背后的秘密伺糠,BatchExecutor
批處理是 JDBC 編程中的另一種優(yōu)化手段蒙谓。JDBC 在執(zhí)行 SQL 語句時,會將 SQL 語句以及實參通過網(wǎng)絡請求的方式發(fā)送到數(shù)據(jù)庫训桶,一次執(zhí)行一條 SQL 語句累驮,一方面會減小請求包的有效負載,另一個方面會增加耗費在網(wǎng)絡通信上的時間舵揭。通過批處理的方式谤专,我們就可以在 JDBC 客戶端緩存多條 SQL 語句,然后在 flush 或緩存滿的時候午绳,將多條 SQL 語句打包發(fā)送到數(shù)據(jù)庫執(zhí)行置侍,這樣就可以有效地降低上述兩方面的損耗,從而提高系統(tǒng)性能拦焚。
不過蜡坊,有一點需要特別注意:每次向數(shù)據(jù)庫發(fā)送的 SQL 語句的條數(shù)是有上限的,如果批量執(zhí)行的時候超過這個上限值赎败,數(shù)據(jù)庫就會拋出異常秕衙,拒絕執(zhí)行這一批 SQL 語句,所以我們需要控制批量發(fā)送 SQL 語句的條數(shù)和頻率僵刮。
引用自《深入剖析 MyBatis 核心原理》- 楊四正第18節(jié)
版本1-呱呱墜地
廢話不多說据忘,早先時候項目的代碼里就已經(jīng)存在了批處理的代碼,偽代碼的樣子大概是這樣子的:
@Resource
private 某Mapper類 mapper實例對象;
private int BATCH = 1000;
private void doUpdateBatch(Date accountDate, List<某實體類> data) {
SqlSession batchSqlSession = null;
try {
if (data == null || data.size() == 0) {
return;
}
batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
for (int index = 0; index < data.size(); index++) {
mapper實例對象.更新/插入Method(accountDate, data.get(index).getOrderNo());
if (index != 0 && index % BATCH == 0) {
batchSqlSession.commit();
batchSqlSession.clearCache();
}
}
batchSqlSession.commit();
} catch (Exception e) {
batchSqlSession.rollback();
log.error(e.getMessage(), e);
} finally {
if (batchSqlSession != null) {
batchSqlSession.close();
}
}
}
我們先來看看上述這種寫法的幾種問題
你真的懂commit搞糕、clearCache勇吊、flushStatements嘛?
我們先看看官網(wǎng)給出的解釋
然后我們結合上述寫法寞宫,它會在判斷批處理條數(shù)達到1000條的時候會去手動commit
,然后又手動clearCache
拉鹃,我們先來看看commit
到底都做了一些什么辈赋,以下為調(diào)用鏈
@Override
public void commit() {
commit(false);
}
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
private boolean isCommitOrRollbackRequired(boolean force) {
// autoCommit默認為false,調(diào)用過插入膏燕、更新钥屈、刪除之后的dirty值為true
return (!autoCommit && dirty) || force;
}
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
我們會發(fā)現(xiàn),其實你直接調(diào)用commit
的情況下坝辫,它就已經(jīng)做了clearLocalCache
這件事情篷就,所以大可不必在commit
后加上一句clearCache
,而且clearCache
是做了什么你又知道嘛近忙?就擱這調(diào)用=咭怠智润!
另外flushStatements
的作用官網(wǎng)里有詳細解釋
不具備通用性
由于項目中用到批處理的地方肯定不止一個为狸,那每用一次就需要CV一下,0.0 那會不會顯得太菜了遗契?能不能一勞永逸辐棒?這個時候就得用上Java8中的接口函數(shù)了~
版本2-初具雛形
在解決完上述兩個問題后,我們的代碼版本來到了第2版姊途,你以為這就對了涉瘾?這就完事了?別急捷兰,我們繼續(xù)往下看立叛!
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.function.ToIntFunction;
@Slf4j
@Component
public class MybatisBatchUtils {
/**
* 每次處理1000條
*/
private static final int BATCH = 1000;
@Resource
private SqlSessionFactory sqlSessionFactory;
/**
* 批量處理修改或者插入
*
* @param data 需要被處理的數(shù)據(jù)
* @param function 自定義處理邏輯
* @return int 影響的總行數(shù)
*/
public <T> int batchUpdateOrInsert(List<T> data, ToIntFunction<T> function) {
int count = 0;
SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
for (int index = 0; index < data.size(); index++) {
count += function.applyAsInt(data.get(index));
if (index != 0 && index % BATCH == 0) {
batchSqlSession.flushStatements();
}
}
batchSqlSession.commit();
} catch (Exception e) {
batchSqlSession.rollback();
log.error(e.getMessage(), e);
} finally {
batchSqlSession.close();
}
return count;
}
}
偽代碼使用案例
@Resource
private 某Mapper類 mapper實例對象;
batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合, item -> mapper實例對象.insert方法(item));
這個時候我興高采烈的收工了,直到過了一兩天贡茅,導師問我秘蛇,考慮過這個業(yè)務的性能嘛,后續(xù)量大了可能每天有十多萬筆數(shù)據(jù)顶考,問我現(xiàn)在每天要多久赁还,我才發(fā)現(xiàn) 0.0 兩三萬條數(shù)據(jù)插入居然要7分鐘(不完全是這個問題導致這么慢,還有Oracle插入語句的原因驹沿,下面會描述)艘策,,哈哈渊季,笑不活了朋蔫,簡直就是Bug制造機,我就開始思考為什么會這么慢却汉,肯定是批處理沒生效驯妄,我就思考為什么會沒生效?
版本3-標準寫法
我們知道上面我們提到了BatchExecutor
執(zhí)行器合砂,我們知道每個SqlSession
都會擁有一個Executor
對象青扔,這個對象才是執(zhí)行 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對象才能生效L赶ⅰ!励两!
所以我們更改一下這個通用的方法黎茎,把MapperClass也一塊傳遞進來
public class MybatisBatchUtils {
/**
* 每次處理1000條
*/
private static final int BATCH_SIZE = 1000;
@Resource
private SqlSessionFactory sqlSessionFactory;
/**
* 批量處理修改或者插入
*
* @param data 需要被處理的數(shù)據(jù)
* @param mapperClass Mybatis的Mapper類
* @param function 自定義處理邏輯
* @return int 影響的總行數(shù)
*/
public <T,U,R> int batchUpdateOrInsert(List<T> data, Class<U> mapperClass, BiFunction<T,U,R> function) {
int i = 1;
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++;
}
// 非事務環(huán)境下強制commit,事務情況下該commit相當于無效
batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
} catch (Exception e) {
batchSqlSession.rollback();
log.error(e.getMessage(), e);
} finally {
batchSqlSession.close();
}
return i - 1;
}
}
這里會判斷是否是事務環(huán)境当悔,不是的話會強制提交傅瞻,如果是事務環(huán)境的話,這個commit設置force值是無效的盲憎,這個在前面的官網(wǎng)截圖中有提到嗅骄。
使用案例:
batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合, xxxxx.class, (item, mapper實例對象) -> mapper實例對象.insert方法(item));
附:Oracle批量插入優(yōu)化
我們都知道Oracle主鍵序列生成策略跟MySQL不一樣,我們需要弄一個序列生成器饼疙,這里就不詳細展開描述了溺森,然后Mybatis Generator生成的模板代碼中,insert的id
是這樣獲取的
<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">
select XXX.nextval from dual
</selectKey>
如此窑眯,就相當于你插入1萬條數(shù)據(jù)屏积,其實就是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>
最后這樣一頓操作渣聚,批處理 + 語句優(yōu)化一下,這個業(yè)務直接從7分多鐘變成10多秒僧叉,完美解決奕枝,撒花慶祝~
作者:Linn
鏈接:https://juejin.cn/post/7078237987011559460