面對 30 MB、十萬條 INSERT 語句的 SQL 檔案,使用 Navicat 等 GUI 工具匯入極其緩慢甚至卡死。本文分享自研 Java 多線程批量匯入工具,從單線程數小時優化至分鐘級完成,並深入分析 SQLite 並發寫入的鎖機制與最佳實踐。
journal_mode=DELETE,每次提交觸發磁碟同步。PreparedStatement.addBatch() + executeBatch(),減少網路往返。setAutoCommit(false),每 500 條提交一次,大幅減少 fsync。PRAGMA journal_mode=WAL; 讀寫不阻塞,並發效能提升數倍。PRAGMA mmap_size=268435456; 利用 OS 頁面快取加速讀取。package com.geoway.pad.common.tool;
import java.io.*;
import java.sql.*;
import java.util.concurrent.*;
public class BatchTool {
private static final String DDL = "CREATE TABLE IF NOT EXISTS pbeijing_point (" +
"OBJECTID INTEGER, NAME TEXT, ADDRESS TEXT, PHONE TEXT, FAX TEXT, " +
"TYPE TEXT, CITYCODE TEXT, URL TEXT, EMAIL TEXT, NAME2 TEXT, " +
"X INTEGER, Y INTEGER)";
private static final String DB_URL = "jdbc:sqlite:c:/newD.db";
private static final int BATCH_SIZE = 500;
private static final int THREAD_POOL_SIZE = 8; // 建議 CPU 核心數 * 2
private final ExecutorService pool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
private final ThreadLocal threadConn = ThreadLocal.withInitial(this::createConnection);
private Connection createConnection() {
try {
Class.forName("org.sqlite.JDBC");
Connection conn = DriverManager.getConnection(DB_URL);
// 關鍵 PRAGMA 設定
try (Statement st = conn.createStatement()) {
st.execute("PRAGMA journal_mode=WAL;");
st.execute("PRAGMA synchronous=NORMAL;");
st.execute("PRAGMA cache_size=-32768;"); // 32MB 快取
st.execute("PRAGMA mmap_size=268435456;"); // 256MB mmap
st.execute(DDL);
}
conn.setAutoCommit(false);
return conn;
} catch (Exception e) {
throw new RuntimeException("建立 SQLite 連線失敗", e);
}
}
public void readBatchSQL(InputStream is) throws IOException {
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
StringBuilder batchSql = new StringBuilder();
int count = 0;
String line;
while ((line = br.readLine()) != null) {
batchSql.append(line).append('\n');
if (line.trim().endsWith(";")) {
count++;
if (count % BATCH_SIZE == 0) {
pool.submit(new BatchTask(batchSql.toString()));
batchSql.setLength(0);
}
}
}
// 處理剩餘
if (batchSql.length() > 0) {
pool.submit(new BatchTask(batchSql.toString()));
}
} finally {
shutdownPool();
}
}
private void shutdownPool() {
pool.shutdown();
try {
if (!pool.awaitTermination(1, TimeUnit.HOURS)) {
pool.shutdownNow();
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
// 關閉所有 ThreadLocal 連線
// 需維護連線集合或使用 try-with-resources 包裝任務
}
private class BatchTask implements Runnable {
private final String sqlBatch;
BatchTask(String sqlBatch) { this.sqlBatch = sqlBatch; }
@Override
public void run() {
Connection conn = threadConn.get();
try (Statement st = conn.createStatement()) {
String[] sqls = sqlBatch.split(";(?=\\s*(?:INSERT|UPDATE|DELETE|CREATE)|$)");
for (String sql : sqls) {
sql = sql.trim();
if (!sql.isEmpty()) st.addBatch(sql);
}
st.executeBatch();
conn.commit();
} catch (SQLException e) {
try { conn.rollback(); } catch (SQLException ignored) {}
System.err.println("批次執行失敗,已回滾: " + e.getMessage());
}
}
}
public static void main(String[] args) throws IOException {
new BatchTool().readBatchSQL(new FileInputStream("c:/poi.sql"));
System.out.println("匯入完成");
}
}
| 參數 | 建議值 | 說明 |
|---|---|---|
| THREAD_POOL_SIZE | CPU 核心數 × 2 (通常 4~16) | 過多線程反而增加鎖爭用與上下文切換 |
| BATCH_SIZE | 500~2000 | 視 SQL 複雜度與記憶體調整 |
| journal_mode | WAL | 讀寫並行,checkpoint 由獨立線程處理 |
| synchronous | NORMAL | 平衡安全與效能,WAL 模式下足夠 |
| cache_size | -32768 (32MB) | 負數表示 KB,減少磁碟 I/O |
INSERT INTO target SELECT * FROM temp; 利用 SQLite 內部優化。