MyBatis批量插入優(yōu)化的方法步驟
上周接了個(gè)數(shù)據(jù)遷移的活,要把10萬條數(shù)據(jù)從老系統(tǒng)導(dǎo)入新系統(tǒng)。
寫了個(gè)簡單的批量插入,跑起來一看——5分鐘。
領(lǐng)導(dǎo)說太慢了,能不能快點(diǎn)?
折騰了一下午,最后優(yōu)化到3秒,記錄一下過程。
最初的代碼(5分鐘)
最開始寫的很簡單,foreach循環(huán)插入:
// 方式1:循環(huán)單條插入(最慢)
for (User user : userList) {
userMapper.insert(user);
}
10萬條數(shù)據(jù),每條都要走一次網(wǎng)絡(luò)請(qǐng)求、一次SQL解析、一次事務(wù)提交。
算一下:假設(shè)每條插入需要3ms,10萬條就是300秒 = 5分鐘。
這是最蠢的寫法,但我見過很多項(xiàng)目都這么寫。
第一次優(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有個(gè)參數(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會(huì)緩存SQL,最后一次性發(fā)送給數(shù)據(jù)庫執(zhí)行。配合rewriteBatchedStatements=true,MySQL驅(qū)動(dòng)會(huì)把多條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) {
throw new RuntimeException(e);
}
}
executor.shutdown();
}
從8秒降到3秒。
注意事項(xiàng):
- 線程數(shù)不要超過數(shù)據(jù)庫連接池大小
- 如果需要事務(wù)一致性,這個(gè)方案不適用
- 要考慮主鍵沖突的問題
優(yōu)化效果對(duì)比
| 方案 | 耗時(shí) | 提升倍數(shù) |
|---|---|---|
| 循環(huán)單條插入 | 300秒 | 基準(zhǔn) |
| 批量SQL | 30秒 | 10倍 |
| JDBC批處理 | 8秒 | 37倍 |
| 多線程并行 | 3秒 | 100倍 |
踩過的坑
坑1:foreach拼接SQL過長
<foreach collection="list" item="item" separator=",">
如果一次插入太多條,SQL會(huì)非常長,可能超過max_allowed_packet限制。
解決:分批插入,每批500-1000條。
坑2:rewriteBatchedStatements不生效
檢查幾個(gè)點(diǎn):
- URL參數(shù)是否正確:
rewriteBatchedStatements=true - 是否使用了
ExecutorType.BATCH - MySQL驅(qū)動(dòng)版本是否太舊
坑3:自增主鍵返回問題
批量插入時(shí)想獲取自增主鍵:
<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
注意:rewriteBatchedStatements=true時(shí),自增主鍵返回可能有問題,需要升級(jí)MySQL驅(qū)動(dòng)到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
public class 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)化點(diǎn) | 關(guān)鍵配置 |
|---|---|
| 批量SQL | foreach拼接,分批1000條 |
| JDBC批處理 | rewriteBatchedStatements=true + ExecutorType.BATCH |
| 多線程 | 線程數(shù) ≤ 連接池大小 |
核心原則:減少網(wǎng)絡(luò)往返 + 減少事務(wù)次數(shù) + 并行處理。
到此這篇關(guān)于MyBatis批量插入優(yōu)化的方法步驟的文章就介紹到這了,更多相關(guān)MyBatis批量插入優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何基于SpringMVC實(shí)現(xiàn)斷點(diǎn)續(xù)傳(HTTP)
這篇文章主要介紹了如何基于SpringMVC實(shí)現(xiàn)斷點(diǎn)續(xù)傳(HTTP),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
SpringSecurity微服務(wù)實(shí)戰(zhàn)之公共模塊詳解
這篇文章主要為大家介紹了SpringSecurity微服務(wù)實(shí)戰(zhàn)之公共模塊詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
jdk環(huán)境配置Maven環(huán)境配置實(shí)踐
文章介紹了如何配置JDK和Maven環(huán)境變量,并通過命令驗(yàn)證配置是否成功,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-02-02
SpringLDAP目錄服務(wù)之LdapTemplate與LDAP操作方式
本文將深入探討Spring LDAP的核心概念、LdapTemplate的使用方法以及如何執(zhí)行常見的LDAP操作,幫助開發(fā)者有效地將LDAP集成到Spring應(yīng)用中,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
spark中使用groupByKey進(jìn)行分組排序的示例代碼
這篇文章主要介紹了spark中使用groupByKey進(jìn)行分組排序的實(shí)例代碼,本文通過實(shí)例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
Java Eclipse中實(shí)現(xiàn)快速替換變量
這篇文章主要介紹了Java Eclipse中實(shí)現(xiàn)快速替換變量,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09

