MyBatis批量插入優(yōu)化的方法步驟
上周接了個數(shù)據(jù)遷移的活,要把10萬條數(shù)據(jù)從老系統(tǒng)導入新系統(tǒng)。
寫了個簡單的批量插入,跑起來一看——5分鐘。
領導說太慢了,能不能快點?
折騰了一下午,最后優(yōu)化到3秒,記錄一下過程。
最初的代碼(5分鐘)
最開始寫的很簡單,foreach循環(huán)插入:
// 方式1:循環(huán)單條插入(最慢)
for (User user : userList) {
userMapper.insert(user);
}
10萬條數(shù)據(jù),每條都要走一次網(wǎng)絡請求、一次SQL解析、一次事務提交。
算一下:假設每條插入需要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)絡往返次數(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);
}));
}
// 等待所有任務完成
for (Future<?> future : futures) {
try {
future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
executor.shutdown();
}
從8秒降到3秒。
注意事項:
- 線程數(shù)不要超過數(shù)據(jù)庫連接池大小
- 如果需要事務一致性,這個方案不適用
- 要考慮主鍵沖突的問題
優(yōu)化效果對比
| 方案 | 耗時 | 提升倍數(shù) |
|---|---|---|
| 循環(huán)單條插入 | 300秒 | 基準 |
| 批量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
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();
}
}
}
總結
| 優(yōu)化點 | 關鍵配置 |
|---|---|
| 批量SQL | foreach拼接,分批1000條 |
| JDBC批處理 | rewriteBatchedStatements=true + ExecutorType.BATCH |
| 多線程 | 線程數(shù) ≤ 連接池大小 |
核心原則:減少網(wǎng)絡往返 + 減少事務次數(shù) + 并行處理。
到此這篇關于MyBatis批量插入優(yōu)化的方法步驟的文章就介紹到這了,更多相關MyBatis批量插入優(yōu)化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot使用Spring Security實現(xiàn)登錄注銷功能
這篇文章主要介紹了SpringBoot使用Spring Security實現(xiàn)登錄注銷功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2020-09-09
Spring Boot項目利用Redis實現(xiàn)session管理實例
本篇文章主要介紹了Spring Boot項目利用Redis實現(xiàn)session管理實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
詳解SpringBoot項目整合Vue做一個完整的用戶注冊功能
本文主要介紹了SpringBoot項目整合Vue做一個完整的用戶注冊功能,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07
tk.mybatis實現(xiàn)uuid主鍵生成的示例代碼
本文主要介紹了tk.mybatis實現(xiàn)uuid主鍵生成的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12

