JDBC 大批量寫入 (10 萬+ 行) 時,預設逐條提交效能極差。本文實測對比三種方案:**基礎 addBatch**、**關閉自動提交 + 手動 commit**、**啟用 rewriteBatchedStatements 重寫 SQL**,最終耗時從 47 秒降至 10 秒,**效能提升 4.7 倍**。
PreparedStatement pst = conn.prepareStatement(sql); pst.addBatch(); // 累積參數組 pst.executeBatch(); // 一次性發送執行 conn.commit(); // 手動提交事務
package cyl.demo.ipsearcher;
import java.io.*;
import java.sql.*;
public class DbStoreHelper {
private String connectStr = "jdbc:mysql://localhost:3306/db_ip";
private String insertSql = "INSERT INTO tb_ipinfos (iplong1,iplong2,ipstr1,ipstr2,ipdesc) VALUES (?,?,?,?,?)";
private String charset = "gbk";
private String username = "root";
private String password = "***";
public void storeToDb(String srcFile) throws IOException {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), charset));
Connection conn = DriverManager.getConnection(connectStr, username, password)) {
Class.forName("com.mysql.jdbc.Driver");
conn.setAutoCommit(false); // 關鍵:關閉自動提交
try (PreparedStatement pst = conn.prepareStatement(insertSql)) {
int count = 0;
String line;
while ((line = br.readLine()) != null) {
String[] infos = line.split(";");
if (infos.length < 5) continue;
pst.setLong(1, Long.parseLong(infos[0]));
pst.setLong(2, Long.parseLong(infos[1]));
pst.setString(3, infos[2]);
pst.setString(4, infos[3]);
pst.setString(5, infos[4]);
pst.addBatch();
if (++count % 1000 == 0) { // 分批提交,避免記憶體暴漲
pst.executeBatch();
conn.commit();
}
}
pst.executeBatch(); // 餘數提交
conn.commit();
System.out.println("All down: " + count);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
| 階段 | 連接字串 | 關鍵設定 | 103,498 行耗時 | 備註 |
|---|---|---|---|---|
| 1. 基礎逐條 | 預設 | autoCommit=true, 無 batch | ~ 幾分鐘 | 每行一次網路往返 |
| 2. addBatch + 手動提交 | 預設 | autoCommit=false, addBatch/executeBatch/commit | 47 秒 | 減少網路往返,但驅動端仍逐條構造 SQL |
| 3. 啟用 rewriteBatchedStatements | ?useServerPrepStmts=false&rewriteBatchedStatements=true | 同上 + 驅動端合併為 INSERT ... VALUES (...),(...),... | 10 秒 | 單條多值 SQL,網路封包大幅減少,效能最佳 |
rewriteBatchedStatements=true:**核心開關**,驅動端將同一 PreparedStatement 的多組參數重寫為單條多值 INSERT,減少解析與網路開銷。useServerPrepStmts=false:使用客戶端預編譯(而非 Server-side PreparedStatement),配合重寫功能生效。若為 true,重寫可能不生效。executeBatch() + commit(),平衡記憶體與事務大小。BatchUpdateException,解析 getUpdateCounts() 定位失敗行,決定重試或回滾。maximumPoolSize、autoCommit=false(由應用控制)。rewriteBatchedStatements 可能失效,需測試驗證或改用 LOAD DATA INFILE。max_allowed_packet (如 64M)、net_buffer_length,防止超大封包被截斷。| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| JDBC Batch + rewriteBatchedStatements | 代碼可控、支援複雜邏輯、事務一致性 | 需應用層實現、受限於 JVM 記憶體 | 業務邏輯複雜、需事務保證的批量寫入 |
| LOAD DATA INFILE | 速度最快、服務端直讀檔案 | 需檔案落地、權限要求高、難以在事務中混合業務邏輯 | 純資料導入、ETL、離線大量載入 |
| MySQL Shell / mysqlpump 平行導入 | 多線程、支援邏輯/物理備份格式 | 工具依賴、學習成本 | 跨實例遷移、大規模初始化 |
對於 JDBC 批量寫入,**務必**同時做到三點:
conn.setAutoCommit(false)PreparedStatement.addBatch() / executeBatch()?useServerPrepStmts=false&rewriteBatchedStatements=true缺一不可。實測 10 萬行從 47 秒降至 10 秒,代碼零侵入,立竿見影。