1 批處理插入更新
1.1 簡(jiǎn)介
1.1.1 定義
處理批處理的方式有很多種窗骑,這里不分析各種方式的優(yōu)劣擎淤,只是概述 ExecutorType.BATCH
這種的用法
簡(jiǎn)單了解一下批處理背后的秘密,BatchExecutor
批處理是 JDBC
編程中的另一種優(yōu)化手段。JDBC
在執(zhí)行 SQL
語句時(shí),會(huì)將 SQL
語句以及實(shí)參通過網(wǎng)絡(luò)請(qǐng)求的方式發(fā)送到數(shù)據(jù)庫官卡,一次執(zhí)行一條 SQL
語句,一方面會(huì)減小請(qǐng)求包的有效負(fù)載醋虏,另一個(gè)方面會(huì)增加耗費(fèi)在網(wǎng)絡(luò)通信上的時(shí)間寻咒。
通過批處理的方式,我們就可以在 JDBC
客戶端緩存多條 SQL
語句颈嚼,然后在 flush
或緩存滿的時(shí)候毛秘,將多條 SQL
語句打包發(fā)送到數(shù)據(jù)庫執(zhí)行,這樣就可以有效地降低上述兩方面的損耗粘舟,從而提高系統(tǒng)性能熔脂。
不過貌虾,有一點(diǎn)需要特別注意:
每次向數(shù)據(jù)庫發(fā)送的
SQL
語句的條數(shù)是有上限的郭计,如果批量執(zhí)行的時(shí)候超過這個(gè)上限值,數(shù)據(jù)庫就會(huì)拋出異常癣猾,拒絕執(zhí)行這一批SQL
語句晰骑,所以我們需要控制批量發(fā)送SQL
語句的條數(shù)
和頻率
1.1.2 ExecutorType.BATCH使用步驟
以下是如何在 MyBatis
中使用 ExecutorType.BATCH
的示例:
- 需要獲取一個(gè)批處理類型的
SqlSession
:
SqlSessionFactory sqlSessionFactory = ... // 獲取 SqlSessionFactory
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
- 通過這個(gè)
SqlSession
獲取Mapper
适秩,并執(zhí)行批處理操作:
YourMapper mapper = sqlSession.getMapper(YourMapper.class);
for (YourEntity entity : entities) {
mapper.insert(entity); // 或者 update/delete
}
- 手動(dòng)提交事務(wù),并關(guān)閉
SqlSession
:
sqlSession.commit();
sqlSession.close();
1.1.3 注意事項(xiàng)
需要注意的是:
- 在使用
ExecutorType.BATCH
時(shí)硕舆,MyBatis
不會(huì)立即執(zhí)行SQL
秽荞,而是將SQL
緩存起來,等到調(diào)用SqlSession.commit()
或SqlSession.flushStatements()
時(shí)才會(huì)一次性執(zhí)行抚官。 - 如果在批處理過程中出現(xiàn)異常扬跋,需要確保進(jìn)行適當(dāng)?shù)腻e(cuò)誤處理,例如回滾事務(wù)凌节。
- 在使用
ExecutorType.BATCH
時(shí)钦听,insert、update 和 delete
操作返回的結(jié)果可能不正確(通常為 -2147482646)倍奢。如果需要獲取這些操作的結(jié)果朴上,可以在SqlSession.flushStatements()
之后調(diào)用SqlSession.clearCache()
,然后再執(zhí)行查詢操作卒煞。 - 如果批處理的數(shù)據(jù)量非常大痪宰,可能需要在每處理一定數(shù)量的數(shù)據(jù)后調(diào)用一次
SqlSession.flushStatements()
和SqlSession.clearCache()
,以避免內(nèi)存溢出畔裕。
1.2 JDBC使用批量
使用Batch批量處理數(shù)據(jù)庫衣撬,當(dāng)需要向數(shù)據(jù)庫發(fā)送一批SQL
語句執(zhí)行時(shí),應(yīng)避免向數(shù)據(jù)庫一條條的發(fā)送執(zhí)行柴钻,而應(yīng)采用JDBC
的批處理機(jī)制淮韭,以提升執(zhí)行效率
1.2.1 Statement批處理
使用Statement批處理
Statement.addBatch(sql) list 執(zhí)行批處理SQL語句
executeBatch()方法:執(zhí)行批處理命令
clearBatch()方法:清除批處理命令
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JDBCManager.getConnection();
String sql1 = "insert into user(name,password,email,birthday)
values('kkk','123','abc@sina.com','1978-08-08')";
String sql2 = "update user set password='自定義密碼' where id=3";
st = conn.createStatement();
st.addBatch(sql1); //把SQL語句加入到批命令中
st.addBatch(sql2); //把SQL語句加入到批命令中
st.executeBatch();
} finally{
JDBCManager.DBclose(con,st,rs);
}
采用Statement.addBatch(sql)
方式實(shí)現(xiàn)批處理:
優(yōu)點(diǎn):可以向數(shù)據(jù)庫發(fā)送多條不同的SQL
語句。
缺點(diǎn):SQL語句沒有預(yù)編譯贴届。
當(dāng)向數(shù)據(jù)庫發(fā)送多條語句相同靠粪,但僅參數(shù)不同的SQL語句時(shí),需重復(fù)寫上很多條SQL語句毫蚓。例如:
Insert into user(name,password) values(‘a(chǎn)a’,’111’);
Insert into user(name,password) values(‘bb’,’222’);
Insert into user(name,password) values(‘cc’,’333’);
Insert into user(name,password) values(‘dd’,’444’);
1.2.2 PreparedStatement批處理
PreparedStatement批處理
PreparedStatement.addBatch();
conn = JDBCManager.getConnection();//獲取工具占键;
String sql = "insert into user(name,password,email,birthday) values(?,?,?,?)";
st = conn.prepareStatement(sql);//預(yù)處理sql語句;
for(int i=0;i<50000;i++){
st.setString(1, "aaa" + i);
st.setString(2, "123" + i);
st.setString(3, "aaa" + i + "@sina.com");
st.setDate(4,new Date(1980, 10, 10));
st.addBatch();//將一組參數(shù)添加到此 PreparedStatement 對(duì)象的批處理命令中元潘。
if(i%1000==0){
st.executeBatch();
st.clearBatch();清空此 Statement 對(duì)象的當(dāng)前 SQL 命令列表畔乙。
}
}
st.executeBatch();
將一批命令提交給數(shù)據(jù)庫來執(zhí)行,如果全部命令執(zhí)行成功翩概,則返回更新計(jì)數(shù)組成的數(shù)組牲距。返回?cái)?shù)組的 int
元素的排序?qū)?yīng)于批中的命令返咱,批中的命令根據(jù)被添加到批中的順序排序
采用PreparedStatement.addBatch()
實(shí)現(xiàn)批處理
優(yōu)點(diǎn):發(fā)送的是預(yù)編譯后的SQL
語句,執(zhí)行效率高牍鞠。
缺點(diǎn):只能應(yīng)用在SQL語句相同咖摹,但參數(shù)不同的批處理中。因此此種形式的批處理經(jīng)常用于在同一個(gè)表中批量插入數(shù)據(jù)难述,或批量更新表的數(shù)據(jù)萤晴。
1.3 Mybatis初級(jí)使用批量
廢話不多說,早先時(shí)候項(xiàng)目的代碼里就已經(jīng)存在了批處理的代碼胁后,偽代碼的樣子大概是這樣子的:
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Resource
private TestMapper testMapper;
private int BATCH = 1000;
private void doUpdateBatch(Date accountDate, List<某實(shí)體類> data) {
try (SqlSession batchSqlSession =sqlSessionFactory.openSession(ExecutorType.BATCH, false);){
if (data == null || data.size() == 0) {
return;
}
TestMapper mapper = batchSqlSession .getMapper(TestMapper.class);
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);
}
}
我們先來看看上述這種寫法的幾種問題
然后我們結(jié)合上述寫法店读,它會(huì)在判斷批處理?xiàng)l數(shù)達(dá)到1000
條的時(shí)候會(huì)去手動(dòng)commit
,然后又手動(dòng)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默認(rèn)為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();
}
}
我們會(huì)發(fā)現(xiàn),其實(shí)你直接調(diào)用commit
的情況下紧武,它就已經(jīng)做了clearLocalCache
這件事情剃氧,所以大可不必在commit
后加上一句clearCache
flushStatements
的作用就是將前面所有執(zhí)行過的INSERT、UPDATE阻星、DELETE
語句真正刷新到數(shù)據(jù)庫中朋鞍。底層調(diào)用了JDBC
的statement.executeBatch
方法。
這個(gè)方法的返回值通俗來說如果執(zhí)行的是同一個(gè)方法并且執(zhí)行的是同一條SQL
妥箕,注意這里的SQL
還沒有設(shè)置參數(shù)滥酥,也就是說SQL
里的占位符?
還沒有被處理成真正的參數(shù),那么每次執(zhí)行的結(jié)果共用一個(gè)BatchResult
畦幢,真正的結(jié)果可以通過BatchResult
中的getUpdateCounts
方法獲取坎吻。
另外如果執(zhí)行了SELECT
操作,那么會(huì)將先前的UPDATE宇葱、INSERT瘦真、DELETE
語句刷新到數(shù)據(jù)庫中。這一點(diǎn)去看BatchExecutor中的doQuery
方法即可黍瞧。
1.4 Mybatis升級(jí)使用
由于項(xiàng)目中用到批處理的地方肯定不止一個(gè)诸尽,那每用一次就需要CV一下,能不能一勞永逸
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實(shí)例對(duì)象;
batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合, item -> mapper實(shí)例對(duì)象.insert方法(item));
1.5 Mybatis批量標(biāo)準(zhǔn)寫法
我們知道上面我們提到了BatchExecutor
執(zhí)行器印颤,我們知道每個(gè)SqlSession
都會(huì)擁有一個(gè)Executor
對(duì)象您机,這個(gè)對(duì)象才是執(zhí)行 SQL
語句的幕后黑手,我們也知道Spring
跟Mybatis
整合的時(shí)候使用的SqlSession
是SqlSessionTemplate
,默認(rèn)用的是ExecutorType.SIMPLE
际看,這個(gè)時(shí)候你通過自動(dòng)注入獲得的Mapper
對(duì)象其實(shí)是沒有開啟批處理的
那么我們實(shí)際上是需要通過sqlSessionFactory.openSession(ExecutorType.BATCH)
得到的sqlSession
對(duì)象(此時(shí)里面的Executor
是BatchExecutor
)去獲得一個(gè)新的Mapper
對(duì)象才能生效
所以更改一下這個(gè)通用的方法咸产,把MapperClass
也一塊傳遞進(jìn)來
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++;
}
// 非事務(wù)環(huán)境下強(qiáng)制commit,事務(wù)情況下該commit相當(dāng)于無效
batchSqlSession.commit(!TransactionSynchronizationManager.isSynchronizationActive());
} catch (Exception e) {
batchSqlSession.rollback();
throw new CustomException(e);
} finally {
batchSqlSession.close();
}
return i - 1;
}
}
這里會(huì)判斷是否是事務(wù)環(huán)境仲闽,不是的話會(huì)強(qiáng)制提交锐朴,如果是事務(wù)環(huán)境的話,這個(gè)commit設(shè)置force值是無效的
使用案例:
batchUtils.batchUpdateOrInsert(數(shù)據(jù)集合, xxxxx.class, (item, mapper實(shí)例對(duì)象) -> mapper實(shí)例對(duì)象.insert方法(item));
1.6 使用rewriteBatchedStatements
rewriteBatchedStatements=true
是 MyBatis
配置文件中的一個(gè)屬性蔼囊,用于優(yōu)化批量插入或更新操作的性能。
當(dāng) rewriteBatchedStatements=true
時(shí)衣迷,MyBatis
會(huì)將批量插入或更新的 SQL
語句重寫為單條語句畏鼓,然后發(fā)送給數(shù)據(jù)庫執(zhí)行。這樣可以減少網(wǎng)絡(luò)傳輸?shù)拇螖?shù)壶谒,提高插入或更新操作的效率云矫。
默認(rèn)情況下,rewriteBatchedStatements
的值為 false
汗菜,即不啟用該優(yōu)化让禀。這是因?yàn)樵谀承?shù)據(jù)庫(如 MySQL)中,啟用該優(yōu)化可能會(huì)導(dǎo)致主鍵沖突或其他異常陨界。因此巡揍,如果使用的數(shù)據(jù)庫支持該優(yōu)化并且不會(huì)引起問題,可以將該屬性設(shè)置為 true
菌瘪,以提高批量插入或更新操作的性能腮敌。
需要注意的是,不是所有的數(shù)據(jù)庫和驅(qū)動(dòng)程序都支持 rewriteBatchedStatements
俏扩。因此糜工,在配置該屬性之前,應(yīng)該先了解你所使用的數(shù)據(jù)庫的特性和限制录淡。
具體配置如下:
spring:
application:
name: batch-demo
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: xxxx
password: xxxx
url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&serverTimezone=GMT&characterEncoding=utf8&rewriteBatchedStatements=true