Java中大數(shù)據(jù)量批處理的性能優(yōu)化指南
一、批處理核心原則
- 單條處理:20萬(wàn)次網(wǎng)絡(luò)往返 + 20萬(wàn)次SQL解析 = 極慢
- 批量處理:200次網(wǎng)絡(luò)往返 + 200次SQL解析 = 快1000倍
批次大小選擇平衡點(diǎn):
- 太小(1~100):網(wǎng)絡(luò)往返次數(shù)多,總耗時(shí)長(zhǎng)
- 太大(10000+):?jiǎn)未蝺?nèi)存占用高,SQL 過(guò)長(zhǎng),數(shù)據(jù)庫(kù)鎖持有時(shí)間長(zhǎng)
- 最佳區(qū)間:1000~5000
二、導(dǎo)入優(yōu)化
2.1 批量 INSERT(核心)
// 慢:逐條 INSERT(20萬(wàn)次網(wǎng)絡(luò)IO)
for (Entity e : list) {
mapper.insert(e); // 每條一次網(wǎng)絡(luò)往返
}
// 快:批量 INSERT(100次網(wǎng)絡(luò)IO)
for (int i = 0; i < list.size(); i += 2000) {
int end = Math.min(i + 2000, list.size());
mapper.batchInsert(list.subList(i, end));
}
XML:
<insert id="batchInsert">
INSERT INTO my_table (col1, col2, col3) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.col1}, #{item.col2}, #{item.col3})
</foreach>
</insert>2.2 分頁(yè)處理模式
int pageSize = 2000;
int page = 1;
while (true) {
List<Detail> batch = mapper.selectPage(taskId, page, pageSize);
if (batch.isEmpty()) break;
// 校驗(yàn)或入庫(kù)
processBatch(batch);
// 回寫(xiě)狀態(tài)
mapper.batchUpdateStatus(batch);
page++;
}
2.3 完整導(dǎo)入示例
public void importData(List<RawData> allData) {
int batchSize = 2000;
int total = allData.size();
for (int i = 0; i < total; i += batchSize) {
int end = Math.min(i + batchSize, total);
List<RawData> batch = allData.subList(i, end);
// 1. 校驗(yàn)
List<Entity> validList = new ArrayList<>();
for (RawData raw : batch) {
if (validate(raw)) {
validList.add(convert(raw));
}
}
// 2. 批量入庫(kù)
if (!validList.isEmpty()) {
mapper.batchInsert(validList);
}
}
}
三、導(dǎo)出優(yōu)化
3.1 SXSSFWorkbook 流式寫(xiě)入
SXSSFWorkbook workbook = new SXSSFWorkbook(200); // 窗口200行
Sheet sheet = workbook.createSheet("數(shù)據(jù)");
int pageSize = 5000;
int page = 1;
int rowIndex = 1;
while (true) {
List<Entity> batch = mapper.selectPage(page, pageSize);
if (batch.isEmpty()) break;
for (Entity e : batch) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(e.getName());
row.createCell(1).setCellValue(e.getAmount());
}
page++;
}
workbook.write(outputStream);
workbook.dispose(); // 清理臨時(shí)文件
3.2 EasyExcel 流式寫(xiě)入
ExcelWriter writer = EasyExcelFactory.write(outputStream, ExportRow.class).build();
WriteSheet sheet = EasyExcelFactory.writerSheet("數(shù)據(jù)").build();
int page = 0;
while (true) {
List<ExportRow> batch = queryData(param, page, 5000);
if (batch.isEmpty()) break;
writer.write(batch, sheet);
page++;
}
writer.finish();
四、方案對(duì)比
導(dǎo)入方案
| 方案 | 20萬(wàn)條耗時(shí) | 內(nèi)存 | 復(fù)雜度 |
|---|---|---|---|
| 逐條INSERT | 15~30分鐘 | 低 | 低 |
| 批量INSERT(2000/批) | 5~10秒 | 中(25MB) | 低 |
| MQ異步+批量INSERT | 5~10秒(用戶(hù)無(wú)感) | 中 | 高 |
| 線(xiàn)程池異步+批量INSERT | 3~8秒(用戶(hù)無(wú)感) | 中 | 中 |
導(dǎo)出方案
| 方案 | 20萬(wàn)條耗時(shí) | 內(nèi)存 | 文件大小 |
|---|---|---|---|
| XSSFWorkbook全內(nèi)存 | 15~25秒 | 400MB+ | 8~12MB |
| SXSSFWorkbook+分頁(yè)查 | 4~8秒 | 20~30MB | 8~12MB |
| EasyExcel流式 | 3~6秒 | 15~25MB | 8~12MB |
| CSV | 1~3秒 | <5MB | 15~20MB |
五、完整示例
@Service
public class BatchProcessService {
@Resource
private DataMapper dataMapper;
private static final int BATCH_SIZE = 2000;
/** 批量導(dǎo)入. */
public void batchImport(List<ImportRow> rawData) {
List<DataEntity> buffer = new ArrayList<>(BATCH_SIZE);
for (ImportRow raw : rawData) {
if (!validate(raw)) continue;
buffer.add(convert(raw));
if (buffer.size() >= BATCH_SIZE) {
dataMapper.batchInsert(buffer);
buffer.clear();
}
}
// 處理剩余
if (!buffer.isEmpty()) {
dataMapper.batchInsert(buffer);
}
}
/** 流式導(dǎo)出. */
public void streamExport(OutputStream out, QueryParam param) {
SXSSFWorkbook wb = new SXSSFWorkbook(200);
try {
Sheet sheet = wb.createSheet("導(dǎo)出數(shù)據(jù)");
writeHeader(sheet);
int page = 1, rowIdx = 1;
while (true) {
List<DataEntity> batch = dataMapper.selectByPage(param, page, 5000);
if (batch.isEmpty()) break;
for (DataEntity e : batch) {
writeRow(sheet, rowIdx++, e);
}
page++;
}
wb.write(out);
} finally {
wb.dispose();
}
}
}
六、關(guān)鍵配置
# 數(shù)據(jù)庫(kù)連接池(批處理需要足夠連接)
spring:
datasource:
hikari:
maximum-pool-size: 20 # 并發(fā)批處理時(shí)需要足夠連接
connection-timeout: 30000
# MySQL批量操作需要的URL參數(shù)
url: jdbc:mysql://host:3306/db?rewriteBatchedStatements=true
# ↑ 關(guān)鍵!開(kāi)啟批量重寫(xiě),性能提升5~10倍rewriteBatchedStatements=true 讓 JDBC 驅(qū)動(dòng)將多條 INSERT 合并為一條多值 INSERT,是批量寫(xiě)入性能的關(guān)鍵配置。
七、批量 UPDATE 優(yōu)化
7.1 逐條 vs 批量
// 慢:逐條UPDATE
for (Detail d : list) {
mapper.updateStatus(d.getId(), d.getStatus()); // 20萬(wàn)次
}
// 快:CASE WHEN 批量UPDATE
mapper.batchUpdateStatus(list); // 按批次執(zhí)行
XML 實(shí)現(xiàn):
<update id="batchUpdateStatus">
UPDATE exchange_task_detail
SET status = CASE id
<foreach collection="list" item="item">
WHEN #{item.id} THEN #{item.status}
</foreach>
END,
data = CASE id
<foreach collection="list" item="item">
WHEN #{item.id} THEN #{item.data}
</foreach>
END
WHERE id IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.id}
</foreach>
</update>7.2 另一種方式:臨時(shí)表 JOIN UPDATE
數(shù)據(jù)量極大時(shí)(50萬(wàn)+),可以先把要更新的數(shù)據(jù)寫(xiě)入臨時(shí)表,再用 JOIN UPDATE:
-- 1. 創(chuàng)建臨時(shí)表并插入數(shù)據(jù) CREATE TEMPORARY TABLE tmp_update (id INT, status INT); INSERT INTO tmp_update VALUES (1, 3), (2, 3), ...; -- 2. JOIN UPDATE UPDATE exchange_task_detail d JOIN tmp_update t ON d.id = t.id SET d.status = t.status;
八、并發(fā)批處理
8.1 線(xiàn)程池分片
public void parallelImport(List<Entity> allData) {
int batchSize = 2000;
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < allData.size(); i += batchSize) {
int start = i;
int end = Math.min(i + batchSize, allData.size());
List<Entity> batch = allData.subList(start, end);
futures.add(executor.submit(() -> {
mapper.batchInsert(batch);
}));
}
// 等待全部完成
for (Future<?> f : futures) {
f.get();
}
executor.shutdown();
}
注意:并發(fā)寫(xiě)入時(shí)需確保沒(méi)有唯一鍵沖突,且數(shù)據(jù)庫(kù)連接池大小 >= 線(xiàn)程數(shù)。
8.2 性能對(duì)比
| 方式 | 20萬(wàn)條INSERT耗時(shí) | 數(shù)據(jù)庫(kù)連接占用 |
|---|---|---|
| 單線(xiàn)程 2000/批 | 5~10秒 | 1個(gè) |
| 4線(xiàn)程 2000/批 | 2~4秒 | 4個(gè) |
| 8線(xiàn)程 2000/批 | 1~2.5秒 | 8個(gè) |
九、內(nèi)存優(yōu)化技巧
9.1 避免全量加載
// 錯(cuò)誤:20萬(wàn)條全加載到內(nèi)存
List<Entity> all = mapper.selectAll(); // ~200MB
// 正確:分頁(yè)加載,用完即棄
int page = 1;
while (true) {
List<Entity> batch = mapper.selectPage(page, 5000); // ~5MB
if (batch.isEmpty()) break;
process(batch);
page++;
// batch 在下一次循環(huán)時(shí)可被GC
}
9.2 流式讀取 Excel(導(dǎo)入時(shí))
// EasyExcel 流式讀取,不會(huì)把20萬(wàn)行全加載到內(nèi)存
EasyExcelFactory.read(inputStream, RowData.class,
new PageReadListener<RowData>(batch -> {
// 每100行回調(diào)一次,處理后釋放
processBatch(batch);
}, 100))
.sheet()
.doRead();
十、rewriteBatchedStatements詳解
這是 MySQL JDBC 驅(qū)動(dòng)的關(guān)鍵參數(shù):jdbc:mysql://host:3306/db?rewriteBatchedStatements=true
| 設(shè)置 | 實(shí)際執(zhí)行的SQL | 網(wǎng)絡(luò)往返 |
|---|---|---|
| false(默認(rèn)) | INSERT INTO t VALUES(1); INSERT INTO t VALUES(2); ... | N次 |
| true | INSERT INTO t VALUES(1),(2),(3),... | 1次 |
對(duì)批量 INSERT 性能提升 5~10倍。對(duì) UPDATE 也有類(lèi)似優(yōu)化(合并為多條一次發(fā)送)。
十一、總結(jié)
導(dǎo)入優(yōu)化公式:
- 總耗時(shí) = (數(shù)據(jù)量 / 批次大小) × 單批耗時(shí)
- 單批耗時(shí) = 網(wǎng)絡(luò)往返(~2ms) + SQL執(zhí)行(~5ms) + 業(yè)務(wù)處理(~1ms)
- 20萬(wàn)條 / 2000批 × 8ms = 0.8秒(理想值)
- 實(shí)際加上GC、連接等待等 ≈ 5~10秒
導(dǎo)出優(yōu)化公式:
- 總耗時(shí) = 查詢(xún)時(shí)間 + 寫(xiě)入時(shí)間
- 查詢(xún) = (數(shù)據(jù)量 / 頁(yè)大小) × 單頁(yè)查詢(xún)時(shí)間
- 寫(xiě)入 = SXSSFWorkbook 流式寫(xiě)(不受內(nèi)存限制)
批次大小選擇:
┌─────────────┬──────────────────────────────┐
│ 數(shù)據(jù)量 │ 推薦批次大小 │
├─────────────┼──────────────────────────────┤
│ < 1萬(wàn) │ 1000 │
│ 1萬(wàn)~10萬(wàn) │ 2000 │
│ 10萬(wàn)~50萬(wàn) │ 2000~5000 │
│ > 50萬(wàn) │ 5000 + 并發(fā)線(xiàn)程池 │
└─────────────┴──────────────────────────────┘
以上就是Java中大數(shù)據(jù)量批處理的性能優(yōu)化指南的詳細(xì)內(nèi)容,更多關(guān)于Java大數(shù)據(jù)處理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot中@EnableAutoConfiguration和@Configuration的區(qū)別
這篇文章主要介紹了SpringBoot中@EnableAutoConfiguration和@Configuration的區(qū)別,@SpringBootApplication相當(dāng)于@EnableAutoConfiguration,@ComponentScan,@Configuration三者的集合,需要的朋友可以參考下2023-08-08
解決java執(zhí)行cmd命令調(diào)用ffmpeg報(bào)錯(cuò)Concat error - No such filter ''[0,0]
這篇文章主要介紹了java執(zhí)行cmd命令,調(diào)用ffmpeg報(bào)錯(cuò)Concat error - No such filter '[0,0]'解決方法,本文通過(guò)截圖實(shí)例代碼說(shuō)明給大家介紹的非常詳細(xì),對(duì)大家的工作或?qū)W習(xí)有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03
基于Java實(shí)現(xiàn)無(wú)向環(huán)和有向環(huán)的檢測(cè)
這篇文章主要介紹了如何在?Java?中實(shí)現(xiàn)無(wú)向環(huán)和有向環(huán)的檢測(cè),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java有一定的幫助,需要的可以參考一下2022-04-04
Springboot使用Rabbitmq的延時(shí)隊(duì)列+死信隊(duì)列實(shí)現(xiàn)消息延期消費(fèi)
本文介紹了RabbitMQ的延時(shí)隊(duì)列和死信隊(duì)列,解釋了它們的工作原理及其應(yīng)用場(chǎng)景,延時(shí)隊(duì)列允許消息在設(shè)定的時(shí)間后被消費(fèi),結(jié)合實(shí)際案例,展示了如何實(shí)現(xiàn)和使用延時(shí)隊(duì)列和死信隊(duì)列,感興趣的朋友一起看看吧2025-01-01
Java獲取音頻文件的持續(xù)時(shí)間的實(shí)現(xiàn)方案
在音視頻處理開(kāi)發(fā)中,FFmpeg 一直是最常用的跨平臺(tái)音視頻處理工具,然而在很多實(shí)際場(chǎng)景中,FFmpeg 并不總是理想的選擇,本文將展示一種完全不依賴(lài) FFprobe / FFmpeg 的方案,使用 Java 自帶的 javax.sound.sampled API 來(lái)獲取音頻文件的持續(xù)時(shí)間,需要的朋友可以參考下2025-11-11
SpringMVC統(tǒng)一異常處理實(shí)例代碼
這篇文章主要介紹了SpringMVC統(tǒng)一異常處理實(shí)例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11

