"老板让我导出100万条数据,我点了导出按钮,然后...程序崩了!" 💀
📖 为什么大批量数据操作这么难?
想象你要搬家:
- 小搬家:3个箱子,自己一趟搬完 ✅
- 大搬家:300个箱子,自己搬?累死也搬不完!😱
大批量数据操作的挑战:
100万条数据导出:
❌ 一次性全加载到内存:
100万 × 2KB = 2GB 内存 → OutOfMemoryError!💥
❌ 一次性全写入Excel:
Excel 2007+ 最多 104万行 → 超限报错!
❌ 单线程串行处理:
100万条 ÷ 1000条/秒 = 1000秒 ≈ 17分钟 → 用户等疯了!
问题:
- 内存不够 💾
- 时间太长 ⏱️
- 容易超时 ⏰
- 数据库压力大 📊
🎯 优化策略总览
大批量数据处理三板斧:
1️⃣ 分批处理(Batch Processing)
→ 化整为零,逐批操作
2️⃣ 并行处理(Parallel Processing)
→ 多线程/多进程同时处理
3️⃣ 流式处理(Stream Processing)
→ 边读边处理边写,不占内存
🔥 优化技巧一:数据导入(INSERT)
❌ 反面教材:逐条插入
// 单条插入(最慢!)
public void importUsers(List<User> users) {
for (User user : users) {
userMapper.insert(user); // 每次1条SQL
}
}
// 100万条数据:
// 100万次 SQL
// 100万次网络往返
// 100万次事务提交
// 耗时:约 3000 秒(50分钟)😱
为什么慢?
每次插入都要:
1. 应用 → 数据库(网络往返) 100ms
2. 数据库解析 SQL 1ms
3. 数据库执行 INSERT 1ms
4. 提交事务 10ms
5. 数据库 → 应用(返回结果) 100ms
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
单条耗时:约 212ms
100万条 × 212ms = 58小时!!!
✅ 优化方案1:批量插入(JDBC Batch)
// 批量插入(快1000倍!)
public void importUsersBatch(List<User> users) {
int batchSize = 1000; // 每批1000条
for (int i = 0; i < users.size(); i += batchSize) {
int end = Math.min(i + batchSize, users.size());
List<User> batch = users.subList(i, end);
// 批量插入
userMapper.insertBatch(batch);
}
}
<!-- MyBatis XML -->
<insert id="insertBatch">
INSERT INTO user (name, age, email)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
<!-- 生成的 SQL:
INSERT INTO user (name, age, email)
VALUES
('张三', 25, 'zhang@example.com'),
('李四', 30, 'li@example.com'),
('王五', 28, 'wang@example.com'),
...(1000条)
-->
性能对比:
| 方式 | 100万条耗时 | 提升倍数 |
|---|---|---|
| 单条插入 | 50分钟 | - |
| 批量插入(1000条/批) | 3分钟 | 17倍 ⚡ |
✅ 优化方案2:JDBC PreparedStatement 批处理
// 原生 JDBC 批处理(最快!)
public void importUsersJdbcBatch(List<User> users) throws SQLException {
String sql = "INSERT INTO user (name, age, email) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pst = conn.prepareStatement(sql)) {
// 关闭自动提交
conn.setAutoCommit(false);
int batchSize = 1000;
int count = 0;
for (User user : users) {
pst.setString(1, user.getName());
pst.setInt(2, user.getAge());
pst.setString(3, user.getEmail());
pst.addBatch(); // 添加到批次
if (++count % batchSize == 0) {
pst.executeBatch(); // 执行批次
conn.commit(); // 提交事务
pst.clearBatch(); // 清空批次
}
}
// 处理剩余数据
pst.executeBatch();
conn.commit();
}
}
// 100万条数据:约 2分钟 ⚡⚡
✅ 优化方案3:MyBatis-Plus 批量插入
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
// MyBatis-Plus 提供的批量插入
public void importUsers(List<User> users) {
this.saveBatch(users, 1000); // 每批1000条
}
}
// 底层使用 JDBC Batch
// 简单易用,性能也不错
✅ 优化方案4:并行批量插入(最快!)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private ThreadPoolExecutor executor;
// 并行批量插入
public void importUsersParallel(List<User> users) throws Exception {
int batchSize = 1000;
int threadCount = 10; // 10个线程并行
int totalSize = users.size();
int chunkSize = (totalSize + threadCount - 1) / threadCount;
List<Future<?>> futures = new ArrayList<>();
// 分成10个任务
for (int i = 0; i < threadCount; i++) {
int start = i * chunkSize;
int end = Math.min(start + chunkSize, totalSize);
if (start >= totalSize) break;
List<User> chunk = users.subList(start, end);
// 提交异步任务
Future<?> future = executor.submit(() -> {
// 每个线程处理一批数据
for (int j = 0; j < chunk.size(); j += batchSize) {
int batchEnd = Math.min(j + batchSize, chunk.size());
List<User> batch = chunk.subList(j, batchEnd);
userMapper.insertBatch(batch);
}
});
futures.add(future);
}
// 等待所有任务完成
for (Future<?> future : futures) {
future.get();
}
}
}
// 100万条数据:约 20秒 ⚡⚡⚡
// 性能提升:150倍!
线程池配置:
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor importExecutor() {
return new ThreadPoolExecutor(
10, // 核心线程数
20, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 队列容量
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
}
🔥 优化技巧二:数据导出(SELECT)
❌ 反面教材:一次性全查询
// 一次性查询所有数据(内存爆炸!)
public void exportUsers(HttpServletResponse response) {
List<User> users = userMapper.selectAll(); // 查询100万条
// OutOfMemoryError!💥
// 写入 Excel
ExcelWriter writer = EasyExcel.write(response.getOutputStream()).build();
writer.write(users);
}
// 问题:
// - 100万条 × 2KB = 2GB 内存
// - ResultSet 也要占内存
// - Excel 对象也要占内存
// - 总共可能需要 5GB+ 内存!
✅ 优化方案1:分页查询 + 流式写入
@GetMapping("/export/users")
public void exportUsers(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition",
"attachment;filename=users.xlsx");
// 流式写入(不占内存)
ExcelWriter writer = EasyExcel.write(response.getOutputStream(), User.class)
.build();
WriteSheet sheet = EasyExcel.writerSheet("用户").build();
int pageSize = 10000; // 每页1万条
int pageNum = 1;
while (true) {
// 分页查询
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
if (users.isEmpty()) {
break; // 没有数据了
}
// 流式写入(立即释放内存)
writer.write(users, sheet);
pageNum++;
}
writer.finish();
}
// 内存占用:约 200MB(只保留当前页)
// 时间:约 5分钟(100万条)
✅ 优化方案2:游标查询(MyBatis Cursor)
// Mapper 接口
@Mapper
public interface UserMapper {
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
@Select("SELECT * FROM user")
Cursor<User> selectCursor();
}
// Service
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(readOnly = true) // 必须在事务中
public void exportUsers(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition",
"attachment;filename=users.xlsx");
ExcelWriter writer = EasyExcel.write(response.getOutputStream(), User.class)
.build();
WriteSheet sheet = EasyExcel.writerSheet("用户").build();
// 游标查询(流式读取)
try (Cursor<User> cursor = userMapper.selectCursor()) {
List<User> batch = new ArrayList<>(1000);
for (User user : cursor) {
batch.add(user);
// 每1000条写入一次
if (batch.size() >= 1000) {
writer.write(batch, sheet);
batch.clear(); // 释放内存
}
}
// 写入剩余数据
if (!batch.isEmpty()) {
writer.write(batch, sheet);
}
}
writer.finish();
}
}
// 优势:
// - 内存占用极低(约 50MB)
// - 流式处理,边读边写
// - 不会OOM
✅ 优化方案3:异步导出 + 消息通知
// 适用场景:数据量特别大(100万+),用户等不起
@RestController
public class ExportController {
@Autowired
private ExportService exportService;
// 提交导出任务(立即返回)
@PostMapping("/export/users/async")
public Result<String> exportAsync() {
String taskId = UUID.randomUUID().toString();
// 异步执行
exportService.exportAsync(taskId);
return Result.success(taskId, "导出任务已提交,完成后会发送通知");
}
// 查询任务进度
@GetMapping("/export/task/{taskId}")
public Result<ExportTask> getTask(@PathVariable String taskId) {
ExportTask task = exportService.getTask(taskId);
return Result.success(task);
}
}
@Service
public class ExportService {
@Autowired
private UserMapper userMapper;
@Autowired
private AsyncTaskExecutor executor;
@Autowired
private OSSClient ossClient; // 阿里云OSS
private Map<String, ExportTask> tasks = new ConcurrentHashMap<>();
@Async
public void exportAsync(String taskId) {
ExportTask task = new ExportTask(taskId);
tasks.put(taskId, task);
try {
// 更新状态:处理中
task.setStatus("PROCESSING");
// 导出到临时文件
File tempFile = File.createTempFile("users_", ".xlsx");
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
ExcelWriter writer = EasyExcel.write(fos, User.class).build();
WriteSheet sheet = EasyExcel.writerSheet("用户").build();
int pageSize = 10000;
int pageNum = 1;
int total = userMapper.count();
while (true) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
if (users.isEmpty()) break;
writer.write(users, sheet);
// 更新进度
int processed = Math.min(pageNum * pageSize, total);
task.setProgress(processed * 100 / total);
pageNum++;
}
writer.finish();
}
// 上传到 OSS
String url = ossClient.upload(tempFile);
// 更新状态:完成
task.setStatus("COMPLETED");
task.setDownloadUrl(url);
// 发送通知(短信/邮件/站内信)
notificationService.send(task.getUserId(),
"您的数据导出已完成,下载链接:" + url);
} catch (Exception e) {
task.setStatus("FAILED");
task.setErrorMsg(e.getMessage());
}
}
public ExportTask getTask(String taskId) {
return tasks.get(taskId);
}
}
// 优势:
// - 用户不需要等待 ✅
// - 支持超大数据量(1000万+)✅
// - 不会超时 ✅
// - 用户体验好 ✅
🔥 优化技巧三:Excel 处理优化
EasyExcel vs POI
// ❌ Apache POI(内存占用高)
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("用户");
for (int i = 0; i < users.size(); i++) {
Row row = sheet.createRow(i);
User user = users.get(i);
row.createCell(0).setCellValue(user.getName());
row.createCell(1).setCellValue(user.getAge());
// ...
}
workbook.write(outputStream);
// 100万行:需要 2GB+ 内存
// ✅ EasyExcel(内存占用低)
EasyExcel.write(outputStream, User.class)
.sheet("用户")
.doWrite(users);
// 100万行:只需 100MB 内存(20倍差距!)
EasyExcel 的优势:
- ✅ 内存占用低(10-20倍差距)
- ✅ API 简单易用
- ✅ 支持流式写入
- ✅ 自动样式美化
CSV 导出(更快!)
// CSV 比 Excel 快 5-10 倍!
@GetMapping("/export/users/csv")
public void exportCSV(HttpServletResponse response) throws IOException {
response.setContentType("text/csv");
response.setHeader("Content-Disposition",
"attachment;filename=users.csv");
try (PrintWriter writer = response.getWriter()) {
// 写入表头
writer.println("姓名,年龄,邮箱");
// 流式查询 + 流式写入
try (Cursor<User> cursor = userMapper.selectCursor()) {
for (User user : cursor) {
writer.printf("%s,%d,%s%n",
user.getName(), user.getAge(), user.getEmail());
}
}
}
}
// 性能对比:
// Excel:100万行 5分钟
// CSV: 100万行 1分钟 ⚡
📊 性能对比总结
数据导入性能对比
| 方案 | 100万条耗时 | 内存占用 | 难度 | 推荐指数 |
|---|---|---|---|---|
| 单条插入 | 50分钟 | 低 | ⭐ | ❌ |
| MyBatis批量插入 | 3分钟 | 中 | ⭐⭐ | ⭐⭐⭐ |
| JDBC Batch | 2分钟 | 中 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 并行批量插入 | 20秒 | 中 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
数据导出性能对比
| 方案 | 100万条耗时 | 内存占用 | 难度 | 推荐指数 |
|---|---|---|---|---|
| 一次性查询 | OOM | 5GB+ | ⭐ | ❌ |
| 分页 + EasyExcel | 5分钟 | 200MB | ⭐⭐ | ⭐⭐⭐⭐ |
| 游标 + EasyExcel | 4分钟 | 50MB | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 游标 + CSV | 1分钟 | 50MB | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 异步导出 | 后台执行 | 50MB | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
💡 面试加分回答模板
面试官:"如何优化大批量数据的导入导出?"
标准回答:
"我会从以下几个维度优化:
数据导入优化:
- 批量插入:使用 JDBC Batch 或 MyBatis foreach,每批 1000-5000 条
- 并行处理:多线程并行插入,10线程可提升 10倍
- 事务控制:批量提交事务,避免每条都提交
数据导出优化:
- 流式查询:使用 MyBatis Cursor,避免一次性加载到内存
- 流式写入:使用 EasyExcel 流式API,边读边写
- 异步导出:大数据量(100万+)用异步,完成后通知用户下载
- CSV替代Excel:如果不需要复杂格式,CSV 性能快 5-10 倍
实际案例: 我们有个订单导出功能,原来用 POI 一次性导出,500万订单直接 OOM。 优化后:
- 改用 MyBatis Cursor + EasyExcel 流式写入
- 内存从 5GB 降到 100MB
- 导出时间从无法完成到 10分钟完成
- 用户体验从频繁超时到稳定可用"
🎉 总结
大批量数据处理的核心原则:
1. 分而治之(Divide and Conquer)
→ 大任务拆成小任务
2. 流式处理(Stream Processing)
→ 边读边处理边写,不占内存
3. 并行加速(Parallel Processing)
→ 多线程/多进程同时干活
4. 异步解耦(Async Decoupling)
→ 用户不等待,后台慢慢处理
记住这个公式:
性能 = 批量大小 × 并行度 / 事务提交次数
优化方向:
- 增大批量大小 ↑
- 增加并行度 ↑
- 减少事务提交次数 ↓
最后一句话:
小数据量(<1万):怎么方便怎么来
中数据量(1-100万):批量 + 分页 + 流式
大数据量(>100万):异步 + 并行 + 通知
根据场景选方案,不要一刀切!🎯
祝你的数据导入导出又快又稳! 🚀📦
📚 扩展阅读