MyBatis中批量插入的三個關(guān)鍵優(yōu)化技巧與避坑指南
上周接了個數(shù)據(jù)遷移的活,要把10萬條數(shù)據(jù)從老系統(tǒng)導(dǎo)入新系統(tǒng)。
寫了個簡單的批量插入,跑起來一看——5分鐘。
領(lǐng)導(dǎo)說太慢了,能不能快點?
折騰了一下午,最后優(yōu)化到3秒,記錄一下過程。
最初的代碼(5分鐘)
最開始寫的很簡單,foreach循環(huán)插入:
// 方式1:循環(huán)單條插入(最慢)
for (User user : userList) {
userMapper.insert(user);
}
10萬條數(shù)據(jù),每條都要走一次網(wǎng)絡(luò)請求、一次SQL解析、一次事務(wù)提交。
算一下:假設(shè)每條插入需要3ms,10萬條就是300秒 = 5分鐘。
這是最蠢的寫法,但我見過很多項目都這么寫。
第一次優(yōu)化:批量SQL(30秒)
把循環(huán)插入改成批量SQL:
<!-- Mapper.xml -->
<insert id="batchInsert">
INSERT INTO user (name, age, email) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.age}, #{item.email})
</foreach>
</insert>
// 分批插入,每批1000條
int batchSize = 1000;
for (int i = 0; i < userList.size(); i += batchSize) {
int end = Math.min(i + batchSize, userList.size());
List<User> batch = userList.subList(i, end);
userMapper.batchInsert(batch);
}
從5分鐘降到30秒,提升10倍。
原理:一條SQL插入多條數(shù)據(jù),減少網(wǎng)絡(luò)往返次數(shù)。
但還有問題:30秒還是太慢。
第二次優(yōu)化:JDBC批處理(8秒)
MySQL有個參數(shù)叫rewriteBatchedStatements,開啟后可以把多條INSERT合并成一條。
第一步:修改數(shù)據(jù)庫連接URL
jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
第二步:使用MyBatis的批處理模式
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void batchInsertWithExecutor(List<User> userList) {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int batchSize = 1000;
for (int i = 0; i < userList.size(); i++) {
mapper.insert(userList.get(i));
if ((i + 1) % batchSize == 0) {
sqlSession.flushStatements();
sqlSession.clearCache();
}
}
sqlSession.flushStatements();
sqlSession.commit();
}
}
從30秒降到8秒。
原理:ExecutorType.BATCH模式下,MyBatis會緩存SQL,最后一次性發(fā)送給數(shù)據(jù)庫執(zhí)行。配合rewriteBatchedStatements=true,MySQL驅(qū)動會把多條INSERT合并。
第三次優(yōu)化:多線程并行(3秒)
8秒還是不夠快,上多線程:
public void parallelBatchInsert(List<User> userList) {
int threadCount = 4; // 根據(jù)數(shù)據(jù)庫連接池大小調(diào)整
int batchSize = userList.size() / threadCount;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
int start = i * batchSize;
int end = (i == threadCount - 1) ? userList.size() : (i + 1) * batchSize;
List<User> subList = userList.subList(start, end);
futures.add(executor.submit(() -> {
batchInsertWithExecutor(subList);
}));
}
// 等待所有任務(wù)完成
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
thrownew RuntimeException(e);
}
}
executor.shutdown();
}
從8秒降到3秒。
注意事項:
- 線程數(shù)不要超過數(shù)據(jù)庫連接池大小
- 如果需要事務(wù)一致性,這個方案不適用
- 要考慮主鍵沖突的問題
優(yōu)化效果對比
| 方案 | 耗時 | 提升倍數(shù) |
|---|---|---|
| 循環(huán)單條插入 | 300秒 | 基準(zhǔn) |
| 批量SQL | 30秒 | 10倍 |
| JDBC批處理 | 8秒 | 37倍 |
| 多線程并行 | 3秒 | 100倍 |
踩過的坑
坑1:foreach拼接SQL過長
<foreach collection="list" item="item" separator=",">
如果一次插入太多條,SQL會非常長,可能超過max_allowed_packet限制。
解決: 分批插入,每批500-1000條。
坑2:rewriteBatchedStatements不生效
檢查幾個點:
- URL參數(shù)是否正確:
rewriteBatchedStatements=true - 是否使用了
ExecutorType.BATCH - MySQL驅(qū)動版本是否太舊
坑3:自增主鍵返回問題
批量插入時想獲取自增主鍵:
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
注意:rewriteBatchedStatements=true時,自增主鍵返回可能有問題,需要升級MySQL驅(qū)動到8.0.17+。
坑4:內(nèi)存溢出
10萬條數(shù)據(jù)一次性加載到內(nèi)存,可能OOM。
解決:分頁讀取 + 分批插入。
int pageSize = 10000;
int total = countTotal();
for (int i = 0; i < total; i += pageSize) {
List<User> page = selectByPage(i, pageSize);
batchInsertWithExecutor(page);
}
最終方案代碼
@Service
publicclass BatchInsertService {
@Autowired
private SqlSessionFactory sqlSessionFactory;
/**
* 高性能批量插入
* 10萬條數(shù)據(jù)約3秒
*/
public void highPerformanceBatchInsert(List<User> userList) {
if (userList == null || userList.isEmpty()) {
return;
}
int threadCount = Math.min(4, Runtime.getRuntime().availableProcessors());
int batchSize = (int) Math.ceil((double) userList.size() / threadCount);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
int start = i * batchSize;
int end = Math.min((i + 1) * batchSize, userList.size());
if (start >= userList.size()) {
latch.countDown();
continue;
}
List<User> subList = new ArrayList<>(userList.subList(start, end));
executor.submit(() -> {
try {
doBatchInsert(subList);
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
executor.shutdown();
}
private void doBatchInsert(List<User> userList) {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
for (int i = 0; i < userList.size(); i++) {
mapper.insert(userList.get(i));
if ((i + 1) % 1000 == 0) {
sqlSession.flushStatements();
sqlSession.clearCache();
}
}
sqlSession.flushStatements();
sqlSession.commit();
}
}
}
總結(jié)
| 優(yōu)化點 | 關(guān)鍵配置 |
|---|---|
| 批量SQL | foreach拼接,分批1000條 |
| JDBC批處理 | rewriteBatchedStatements=true+ExecutorType.BATCH |
| 多線程 | 線程數(shù) ≤ 連接池大小 |
核心原則: 減少網(wǎng)絡(luò)往返 + 減少事務(wù)次數(shù) + 并行處理。
到此這篇關(guān)于MyBatis中批量插入的三個關(guān)鍵優(yōu)化技巧與避坑指南的文章就介紹到這了,更多相關(guān)MyBatis批量插入優(yōu)化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot集成/輸出/日志級別控制/持久化開發(fā)實踐
SpringBoot默認(rèn)集成Logback,支持靈活日志級別配置(INFO/DEBUG等),輸出包含時間戳、級別、類名等信息,并可通過Slf4j注解簡化日志記錄,實現(xiàn)控制臺與文件持久化存儲,本文給大家介紹Spring Boot集成/輸出/日志級別控制/持久化開發(fā)實踐,感興趣的朋友一起看看吧2025-08-08
SpringBoot實現(xiàn)單點登錄的實現(xiàn)詳解
在現(xiàn)代的Web應(yīng)用程序中,單點登錄(Single?Sign-On)已經(jīng)變得越來越流行,在本文中,我們將使用Spring?Boot構(gòu)建一個基本的單點登錄系統(tǒng),需要的可以參考一下2023-05-05
Java實現(xiàn)Jar文件的遍歷復(fù)制與文件追加
這篇文章主要為大家詳細(xì)介紹了如何利用Java實現(xiàn)Jar文件的遍歷復(fù)制與文件追加功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-11-11
Spring Security其它權(quán)限校驗方式&自定義權(quán)限校驗方式
這篇文章主要介紹了Spring Security其它權(quán)限校驗方式&自定義權(quán)限校驗方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
Java基礎(chǔ)之?dāng)?shù)組的初始化過程
Java數(shù)組分為靜態(tài)和動態(tài)初始化,靜態(tài)初始化中,程序員設(shè)定元素初始值,系統(tǒng)決定長度;動態(tài)初始化中,程序員設(shè)定長度,系統(tǒng)提供初始值,數(shù)組初始化后長度固定,存儲在堆內(nèi)存中,數(shù)組變量在棧內(nèi)存指向堆內(nèi)存數(shù)組對象,基本類型數(shù)組存儲數(shù)據(jù)值,引用類型數(shù)組存儲對象引用2024-10-10

