為什麼要批量寫入?
單條 INSERT 每次往返網路 RTT + 解析 + 鎖 + Redo Log fsync,10 萬條可能耗時數分鐘。批量寫入將多條 SQL 合併一次網路往返、一次解析、一次提交,效能提升 10-100 倍 是常態。
方案對比與選型建議
| 方案 | 效能 | 複雜度 | 適用場景 | 備註 |
| JDBC PreparedStatement + addBatch | ⭐⭐⭐⭐⭐ | 低 | 通用、高效能、無框架依賴 | 首選方案 |
| JDBC Statement + addBatch | ⭐⭐⭐ | 低 | SQL 固定、無參數化需求 | 有注入風險、無法複用執行計劃 |
| Hibernate 批次插入 | ⭐⭐ | 中 | 已深度使用 Hibernate 專案 | 需配置 hibernate.jdbc.batch_size、order_inserts |
| MyBatis 批次 (ExecutorType.BATCH) | ⭐⭐⭐⭐ | 中 | MyBatis 專案 | 需手動 commit、清理快取 |
| LOAD DATA INFILE | ⭐⭐⭐⭐⭐+ | 中 | 超大量離線導入 (億級) | 需檔案權限、跳過約束檢查、不觸發 Trigger |
核心實現:PreparedStatement 分批提交模板
public void batchInsert(List users, int batchSize) {
String sql = "INSERT INTO `user` (`username`, `email`, `created_at`) VALUES (?, ?, ?)";
Connection conn = null;
PreparedStatement ps = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 關鍵:手動控制事務
ps = conn.prepareStatement(sql);
int count = 0;
for (User u : users) {
ps.setString(1, u.getUsername());
ps.setString(2, u.getEmail());
ps.setTimestamp(3, new Timestamp(u.getCreatedAt().getTime()));
ps.addBatch(); // 累積到批次
if (++count % batchSize == 0) { // 達到批次大小
ps.executeBatch(); // 執行批次
conn.commit(); // 提交事務
ps.clearBatch(); // 清理批次緩衝 (部分驅動需要)
}
}
// 餘數提交
if (count % batchSize != 0) {
ps.executeBatch();
conn.commit();
}
} catch (SQLException e) {
if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {}
throw new RuntimeException("批量寫入失敗", e);
} finally {
closeQuietly(ps);
closeQuietly(conn);
}
}
關鍵參數調優
- batchSize:500-5000 之間,視行大小、網路延遲、記憶體調整。過大 → 記憶體壓力、鎖持有時間長;過小 → 往返次數多
- rewriteBatchedStatements=true (MySQL Connector/J 關鍵參數):
jdbc:mysql://host:3306/db?rewriteBatchedStatements=true
驅動會將 INSERT ... VALUES (?,?), (?,?) 重寫為單條多值 INSERT ... VALUES (...), (...),效能再提升 3-10 倍
- useServerPrepStmts=true +
cachePrepStmts=true + prepStmtCacheSize=250 + prepStmtCacheSqlLimit=2048:啟用服務端預處理與客戶端快取
批量刪除 / 更新同理
// 批量刪除
String delSql = "DELETE FROM `user` WHERE `id` = ?";
ps = conn.prepareStatement(delSql);
for (Long id : ids) {
ps.setLong(1, id);
ps.addBatch();
if (++count % batchSize == 0) { ps.executeBatch(); conn.commit(); }
}
ps.executeBatch(); conn.commit();
// 批量更新
String updSql = "UPDATE `user` SET `status` = ? WHERE `id` = ?";
ps = conn.prepareStatement(updSql);
for (User u : users) {
ps.setInt(1, u.getStatus());
ps.setLong(2, u.getId());
ps.addBatch();
// ... 同理分批提交
}
Hibernate 整合批量寫入 (避開 Session 快取爆炸)
@Transactional
public void hibernateBatchInsert(List entities) {
Session session = entityManager.unwrap(Session.class);
int batchSize = 500;
for (int i = 0; i < entities.size(); i++) {
session.persist(entities.get(i));
if (i % batchSize == 0) {
session.flush(); // 發送 SQL
session.clear(); // 清理一級快取,防止 OOM
}
}
}
// 關鍵配置 (hibernate.cfg.xml / application.yml)
properties:
hibernate.jdbc.batch_size: 500
hibernate.order_inserts: true
hibernate.order_updates: true
hibernate.jdbc.batch_versioned_data: true // 含 @Version 也能批次
MyBatis 批量寫入
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User u : users) {
mapper.insert(u);
}
session.commit(); // 必須手動 commit
} finally {
session.close();
}
// Mapper XML
INSERT INTO `user` (`username`, `email`) VALUES (#{username}, #{email})
效能實測參考 (環境:8 核 16G、NVMe、MySQL 8.0、千兆網)
| 方案 | 10 萬條耗時 | QPS | 備註 |
| 單條 INSERT (自動提交) | ~180 秒 | ~550 | 基線 |
| PreparedStatement + addBatch (batch=1000) | ~8 秒 | ~12,500 | 22x 提升 |
| + rewriteBatchedStatements=true | ~3 秒 | ~33,000 | 60x 提升 |
| LOAD DATA INFILE | ~1.2 秒 | ~83,000 | 極致,但受限多 |
生產環境避坑清單
- ✅ 關閉自動提交,手動分批
commit()
- ✅ 異常時
rollback(),避免髒資料
- ✅
PreparedStatement、Connection 必須在 finally 關閉
- ✅ 大批次建議每批次後
ps.clearBatch() (部分驅動版本有記憶體洩漏)
- ✅ 監控
Threads_running、Innodb_row_lock_waits,避免批次過大阻塞其他業務
- ✅ 唯一鍵衝突處理:
INSERT ... ON DUPLICATE KEY UPDATE 或預先去重
- ✅ 外鍵/觸發器會大幅降低批量效能,評估是否暫停或改用異步