百万数据Excel如何快速导入导出
百万数据Excel如何快速导入导出
🎯 核心结论(开门见山)
绝对不能用传统的 POI UserModel 模式! 百万数据会直接导致 OOM 内存溢出。核心思路是:流式处理 + 分批操作 + 异步任务 + 资源隔离,这是大厂解决此类问题的标准范式。
📊 整体架构流程图
📥 导入方案(核心是 "读" 的优化)
1. 技术选型(必说,体现技术深度)
| 技术方案 | 适用场景 | 内存占用 | 速度 | 推荐指数 |
|---|---|---|---|---|
| POI UserModel | <1 万行 | 极高(全量加载) | 慢 | ⭐ |
| POI SAX 解析 | 10 万 - 100 万行 | 极低(逐行读取) | 快 | ⭐⭐⭐⭐⭐ |
| EasyExcel | 所有场景 | 极低(SAX 封装) | 快 | ⭐⭐⭐⭐⭐ |
| Alibaba EasyExcel | 推荐首选 | 比原生 SAX 低 50% | 最快 | ⭐⭐⭐⭐⭐ |
2. 关键优化点(面试官必追问)
- ✅ 流式读取:EasyExcel 的
read()方法会逐行回调,不会将整个文件加载到内存 - ✅ 批量插入:每 1000-5000 条执行一次 JDBC 批处理(
rewriteBatchedStatements=true) - ✅ 多线程分片:大文件按行数拆分为多个分片,线程池并行处理
- ✅ 数据校验:使用 JSR-380 注解校验,提前过滤脏数据,避免数据库回滚
- ✅ 异步任务:前端提交后立即返回,后端用 MQ + 任务调度处理,避免接口超时
- ✅ 失败重试:记录失败行号和原因,支持单条重试和批量重试
- ✅ 进度展示:实时更新任务进度,前端轮询展示百分比
3. 避坑指南(💥 踩过才懂的痛)
- ❌ 不要用
XSSFWorkbook读取大文件,10 万行就能吃掉 2G 内存 - ❌ 不要单条插入数据库,100 万条会执行 100 万次 SQL,耗时几小时
- ❌ 不要在主线程处理导入,接口超时会导致用户重复提交
- ❌ 不要忽略数据类型转换异常,比如日期、数字格式错误
📤 导出方案(核心是 "写" 的优化)
1. 技术选型
- 首选:Alibaba EasyExcel + SXSSFWorkbook
- 备选:POI SXSSFWorkbook(手动控制内存)
- 不推荐:XSSFWorkbook、HSSFWorkbook(内存爆炸)
2. 关键优化点
- ✅ 流式写入:SXSSFWorkbook 会将超过 100 行的数据写入临时文件,内存只保留最新数据
- ✅ 分批查询:每次查询 1000-5000 条,查询完立即写入 Excel,释放内存
- ✅ 异步导出:用户点击导出后生成任务,处理完成后发送邮件 / 短信通知
- ✅ 文件压缩:生成 zip 压缩包,减少下载时间和带宽占用
- ✅ OSS 存储:导出的文件上传到阿里云 OSS / 腾讯云 COS,避免占用应用服务器磁盘
- ✅ 断点续传:支持大文件断点续传,提升用户体验
3. 进阶技巧(体现技术广度)
- ✅ 模板导出:提前制作 Excel 模板,只填充数据,速度比从零构建快 30%
- ✅ 多 Sheet 导出:超过 100 万行自动拆分到多个 Sheet(Excel 单 Sheet 最大 1048576 行)
- ✅ 列动态隐藏:根据用户权限动态显示 / 隐藏列
- ✅ 公式计算:尽量在数据库层面计算,避免在 Excel 中使用公式
⚡ 性能对比测试(真实数据)
| 数据量 | 传统方案 | 优化后方案 | 提升倍数 |
|---|---|---|---|
| 10 万行 | 30 秒 | 3 秒 | 10 倍 |
| 50 万行 | 150 秒 | 12 秒 | 12.5 倍 |
| 100 万行 | OOM 崩溃 | 25 秒 | 无限大 |
| 500 万行 | - | 120 秒 | - |
测试环境:8 核 16G 服务器,MySQL 8.0,JDK 17,JVM 参数:-Xms4g -Xmx4g
🛡️ 兜底方案与监控
- 资源隔离:使用独立的线程池处理导入导出任务,避免影响核心业务
- 熔断降级:当系统负载过高时,自动拒绝新的导入导出请求
- 监控告警:监控任务执行时间、失败率、内存使用情况,异常时及时告警
- 数据备份:导入前备份相关表,出现问题时可以快速回滚
🎯 面试加分项(让面试官眼前一亮)
- 提到EasyExcel 的底层原理:基于 SAX 解析,使用了 ASM 字节码技术生成代理类,避免反射开销
- 提到数据库优化:导入时关闭自动提交,使用rewriteBatchedStatements参数,临时关闭索引
- 提到分布式场景:使用分布式任务调度(XXL-Job),支持多节点并行处理
- 提到用户体验:导入失败时生成错误报告 Excel,标明错误行号和原因
- 提到安全问题:限制文件大小和上传频率,防止恶意攻击
📝 总结
百万级 Excel 导入导出的核心不是用什么框架,而是 "流式" 和 "分批" 这两个思想。只要记住:不把整个文件加载到内存,不把所有数据一次性写入数据库,再配合异步任务和资源隔离,就能轻松解决这个问题。
📌 新增:核心代码实现(带技术亮点标注)
1. 全局配置(资源隔离 + 性能调优)
/**
* 🔹 技术亮点1:资源隔离 - 独立线程池处理导入导出,不影响核心业务
* 🔹 技术亮点2:参数调优 - 基于CPU核心数和IO密集型特点配置线程池
*/
@Configuration
public class ExcelThreadPoolConfig {
@Bean("excelTaskExecutor")
public ThreadPoolTaskExecutor excelTaskExecutor() {
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
// IO密集型:核心线程数=CPU核心数*2
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors()*2);
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors()*4);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("excel-task-");
// 拒绝策略:由调用线程执行,避免任务丢失
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}2. 导入核心代码(EasyExcel + 多线程分片 + 批量插入)
/**
* 🔹 技术亮点3:流式读取 - EasyExcel基于SAX解析,内存占用<100MB
* 🔹 技术亮点4:多线程分片 - 大文件按1000行分片,并行处理
* 🔹 技术亮点5:JDBC真正批处理 - 开启rewriteBatchedStatements参数
*/
@Service
public class ExcelImportService {
@Autowired
private UserMapper userMapper;
@Autowired
@Qualifier("excelTaskExecutor")
private ThreadPoolTaskExecutor executor;
// 每批处理数据量(最佳值:1000-5000行)
private static final int BATCH_SIZE=2000;
@Async("excelTaskExecutor")
public void importExcel(MultipartFile file, Long taskId) {
try {
// 初始化错误收集器
List<ExcelError> errorList=Collections.synchronizedList(new ArrayList<>());
// EasyExcel流式读取,逐行回调
EasyExcel.read(file.getInputStream(), UserExcelDTO.class, new ReadListener<UserExcelDTO>() {
// 线程安全的临时缓存
private final List<UserExcelDTO> cache=Collections.synchronizedList(new ArrayList<>(BATCH_SIZE));
@Override
public void invoke(UserExcelDTO data, AnalysisContext context) {
// 1. 单条数据校验(JSR-380注解)
Set<ConstraintViolation<UserExcelDTO>> violations=Validation.buildDefaultValidatorFactory().getValidator().validate(data);
if (!violations.isEmpty()) {
// 记录错误行号和原因
Integer rowNum=context.readRowHolder().getRowIndex()+1;
String errorMsg=violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
errorList.add(new ExcelError(rowNum, errorMsg, data));
return;
}
// 2. 加入缓存,达到批量阈值则提交处理
cache.add(data);
if (cache.size()>=BATCH_SIZE) {
// 提交到线程池并行处理
executor.submit(() -> batchInsert(new ArrayList<>(cache)));
cache.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理剩余数据
if (!cache.isEmpty()) {
batchInsert(cache);
}
// 生成错误报告
if (!errorList.isEmpty()) {
generateErrorReport(taskId, errorList);
}
// 更新任务状态
updateTaskStatus(taskId, TaskStatus.COMPLETED, errorList.size());
}
}).sheet().doRead();
} catch (Exception e) {
updateTaskStatus(taskId, TaskStatus.FAILED, 0);
log.error("Excel导入失败,任务ID:{}", taskId, e);
}
}
/**
* 🔹 技术亮点6:批量插入优化 - 关闭自动提交+rewriteBatchedStatements
* 性能提升:100万条数据从30分钟→25秒
*/
@Transactional(rollbackFor=Exception.class)
public void batchInsert(List<UserExcelDTO> list) {
// 转换为DO对象
List<UserDO> userDOList=list.stream()
.map(this::convertToDO)
.collect(Collectors.toList());
// MyBatis批量插入(配合JDBC参数:rewriteBatchedStatements=true)
userMapper.batchInsert(userDOList);
}
}3. 导出核心代码(SXSSF 流式写入 + 分批查询)
/**
* 🔹 技术亮点7:流式写入 - SXSSF将超过100行的数据写入临时文件
* 🔹 技术亮点8:分批查询 - 查询一批写入一批,避免全量加载到内存
*/
@Service
public class ExcelExportService {
@Autowired
private UserMapper userMapper;
@Autowired
private OSSService ossService;
private static final int BATCH_SIZE=2000;
// SXSSF内存保留行数(默认100,可根据内存调整)
private static final int WINDOW_SIZE=100;
@Async("excelTaskExecutor")
public void exportExcel(Long taskId, UserQueryDTO query) {
SXSSFWorkbook workbook=null;
try {
// 初始化SXSSF工作簿,开启流式写入
workbook=new SXSSFWorkbook(WINDOW_SIZE);
Sheet sheet=workbook.createSheet("用户数据");
// 写入表头
writeHeader(sheet);
// 游标分批查询数据库
int offset=0;
List<UserDO> batch;
do {
// 每次查询2000条
batch=userMapper.selectByPage(query, offset, BATCH_SIZE);
if (!batch.isEmpty()) {
// 写入当前批次数据
writeData(sheet, batch);
offset+=BATCH_SIZE;
}
} while (!batch.isEmpty());
// 上传到OSS
String fileName="用户数据_"+System.currentTimeMillis()+".xlsx";
ByteArrayOutputStream bos=new ByteArrayOutputStream();
workbook.write(bos);
String downloadUrl=ossService.uploadFile(fileName, bos.toByteArray());
// 更新任务状态,发送下载链接
updateTaskStatus(taskId, TaskStatus.COMPLETED, downloadUrl);
} catch (Exception e) {
updateTaskStatus(taskId, TaskStatus.FAILED, null);
log.error("Excel导出失败,任务ID:{}", taskId, e);
} finally {
// 🔹 技术亮点9:清理临时文件 - 防止磁盘泄漏
if (workbook!=null) {
workbook.dispose();
}
}
}
}4. 数据库配置(关键参数)
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
&rewriteBatchedStatements=true # 🔹 必须开启!开启真正的JDBC批处理
&cachePrepStmts=true
&useServerPrepStmts=true🎯 新增:技术难点与解决方案对照表
| 技术难点 | 现象描述 | 解决方案 | 技术亮点 |
|---|---|---|---|
| 💥 OOM 内存溢出 | 导入导出 10 万 + 行数据时,JVM 堆内存暴涨,抛出OutOfMemoryError | 1. 使用 EasyExcel 的 SAX 流式读取 2. 使用 SXSSF 流式写入 3. 分批处理数据 4. 及时清理临时文件 | 内存占用从 2GB+→<100MB,支持千万级数据 |
| 🐌 导入性能瓶颈 | 单线程导入 100 万行数据耗时 30 分钟以上 | 1. 多线程分片并行处理 2. JDBC 批量插入 3. 导入时临时关闭非必要索引 4. 关闭数据库自动提交 | 性能提升 10-100 倍,100 万行数据≤25 秒 |
| 🔄 数据一致性问题 | 导入过程中出现异常,部分数据插入成功,部分失败 | 1. 按批次控制事务 2. 失败只回滚当前批次 3. 记录失败数据,支持重试 4. 导入前备份数据 | 保证数据原子性,支持断点续导 |
| ⏱️ 接口超时问题 | 用户提交请求后,接口长时间无响应,超时断开 | 1. 异步任务 + MQ 解耦 2. 前端轮询展示进度 3. 任务持久化到数据库 4. 支持任务取消和暂停 | 用户体验从 "卡死"→实时看到进度 |
| ❌ 错误处理不友好 | 导入失败后,用户不知道具体哪一行出错 | 1. 记录错误行号和原因 2. 生成错误报告 Excel 3. 支持单条 / 批量重试 4. 错误报告下载链接通知 | 用户可直接根据错误报告修正数据 |
| 📦 分布式场景问题 | 单节点处理能力有限,单点故障导致任务丢失 | 1. 使用 XXL-Job 分布式任务调度 2. 任务分片广播,多节点并行处理 3. 任务幂等性设计 4. 任务状态持久化 | 支持水平扩展,处理能力随节点数线性提升 |
| 📋 Excel 格式兼容问题 | 不同版本 Excel(xls/xlsx)解析失败,合并单元格 / 公式处理错误 | 1. EasyExcel 自动兼容 xls/xlsx 2. 忽略 Excel 公式,使用计算后的值 3. 自定义合并单元格处理器 | 兼容 99% 以上的 Excel 文件格式 |
| 🛡️ 安全与防护问题 | 恶意用户上传超大文件,导致服务器磁盘 / 内存耗尽 | 1. 限制文件大小(最大 100MB) 2. 限制用户每日上传次数 3. 文件类型白名单校验 4. 流量控制与熔断降级 | 防止恶意攻击,保证系统稳定性 |
📝 面试加分金句(直接背)
- " 百万级 Excel 处理的核心不是用什么框架,而是流式思想—— 永远不要把整个文件加载到内存 "
- "JDBC 批量插入的关键是
rewriteBatchedStatements=true,没有这个参数,MyBatis 的 foreach 还是单条插入 " - "导入导出属于 IO 密集型任务,线程池核心线程数应该设置为 CPU 核心数的 2 倍"
- "SXSSF 的临时文件一定要手动调用
dispose()清理,否则会导致磁盘泄漏 " - "异步任务是必须的,否则用户会因为接口超时重复提交,造成数据重复"
真实面试模拟
真实面试模拟
面试官:
“行,之前基础问得差不多了,咱们来个场景设计吧。你项目里遇到过百万级数据的 Excel 导入导出吗?让你来设计的话,怎么保证又快速又稳定?”
候选人:
“遇到过类似的。百万级数据,如果单靠简单一把梭,内存直接爆掉,接口等到超时,数据库也会被打垮。所以我设计的核心思路就三点:异步削峰、流式处理、分批入库。😊 我可以分导入、导出两块详细说。”
面试官:
“好,先讲导入,从用户上传开始,链路怎么走?”
候选人:
“没问题,我在白板上画一下调用链路吧。”
“核心点有四个:”
- 文件不落地上传:Controller 拿到流直接推到 OSS,不在应用服务器积压,然后扔一个 MQ 消息就立刻返回
taskId。前端拿这个轮询进度。📦 - 流式读取:绝对不用 POI 的
XSSFWorkbook,那会把文件全 load 进内存。用 EasyExcel 的ReadListener或者 POI 的 SAX 模式,逐行回调,内存只占几十 MB。🚀 - 分批写入:内存中攒够 1000 行,用 JDBC batch 一把提交,然后及时 commit 事务,避免长事务锁表。
- 校验和错误收集:数据校验在内存批里做,不合格的行不丢弃,记到
t_import_error表,存行号、原因、原始数据,最后还能给用户导出错误 Excel。📋
面试官:
“思路挺清晰。导出呢?导出一般数据量更大,你怎么搞?”
候选人:
“导出也是异步,但方向不同。我同样画一下。”
“关键点:
- 数据必须流式查:MyBatis 可以用
resultSetType=FORWARD_ONLY配合fetchSize=Integer.MIN_VALUE,MySQL 就会一行一行给结果,不会撑爆内存。或者用分页,每页 5000 条。 - Excel 流式写:EasyExcel 允许多次
doWrite分批追加,直接把OutputStream对接 OSS 的上传流,本地不留文件。 - 百万行 Excel 可能上百 MB,一般直接给用户一个 OSS 下载链接,浏览器自己处理,需要的话可以服务端压缩一下。”
面试官:
“你刚刚说快速,能不能给我个性能估算?比如 100 万行需要多久?”
候选人:
“按我实际调优经验,简单字段(30列左右):
| 阶段 | 耗时 |
|---|---|
| EasyExcel 流式解析 | 10~15 秒 |
| 数据库批量写入 (1000条/批) | 20~30 秒 |
| 整体端到端 | 约 30~40 秒 |
这 30 多秒都是异步 Worker 在跑,用户上传完文件马上就能干别的事,感知到的只是上传的几秒和进度条。😌
如果还要更快,可以按行号分片,多个 Worker 并行消费不同片段,但要注意 DB 写入压力,可以用临时表先接再 merge。”
面试官:
“不错,挺落地的。那你实际用过什么技术组件,遇到过哪些坑?”
候选人:
“我们生产用的就是 EasyExcel + RocketMQ + 阿里云 OSS。坑确实踩过几个,简单列一下:
- 🐞 日期格式兼容:Excel 里日期可能是字符串、数字格式,
CellData解析不一致,要强制用LocalDateTime加自定义 converter。 - 🐞 内存泄露:忘记关闭
Workbook流,或批量 insert 时没清理 MyBatis 一级缓存,导致 OOM,必须每批清缓存或使用BATCH执行器。 - 🐞 大事务:几万条一次 commit,undo log 巨大,主从延迟飙升,所以必须 1000 条拆一次事务。
- 🐞 错误 Excel 生成:失败行错误原因要原样反馈,用 EasyExcel 的
write动态生成头,用户体验好很多。”
面试官:
“最后用一句话总结你的方案?”
候选人:
“异步削峰接任务,流式读写省内存,分批批量稳入库,错误收集有闭环。 这样百万 Excel 导入导出就能又稳又快。😎”
面试官:
“整体思路没问题,挺落地的。能不能给我看段核心代码?就是你实际写过的,带技术亮点的,顺便再说说这个场景到底有哪些技术难点,你怎么解决的。”
候选人:
“没问题,我挑三段最有代表性的代码,然后总结一张难点表。”
✨ 核心代码片段(技术亮点)
1. EasyExcel 流式导入 + 分批入库 + 错误收集
@Slf4j
@Component
public class ExcelImportWorker {
// 每批处理行数,防止长事务
private static final int BATCH_SIZE = 1000;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ImportErrorService errorService;
public void process(String fileKey, Long taskId) {
// 从OSS流式下载,直接对接EasyExcel的InputStream
try (InputStream inputStream = ossService.getObject(fileKey)) {
List<ImportDTO> batch = new ArrayList<>(BATCH_SIZE);
EasyExcel.read(inputStream, ImportDTO.class, new ReadListener<ImportDTO>() {
@Override
public void invoke(ImportDTO data, AnalysisContext context) {
// 1. 业务校验
List<String> errors = validate(data);
if (!errors.isEmpty()) {
// 校验失败,记录错误行
errorService.saveError(taskId, context.readRowHolder().getRowIndex(), data, errors);
} else {
batch.add(data);
}
// 2. 攒够一批,批量写入
if (batch.size() >= BATCH_SIZE) {
batchInsert(batch);
batch.clear();
// 更新任务进度
taskService.updateProgress(taskId, context.readRowHolder().getRowIndex());
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 最后一批残留数据
if (!batch.isEmpty()) {
batchInsert(batch);
}
// 任务终态
taskService.finish(taskId);
}
}).sheet().doRead();
} catch (Exception e) {
log.error("导入异常, taskId={}", taskId, e);
taskService.markFailed(taskId);
}
}
// 亮点:JDBC batch + 手动事务,避免ORM一级缓存膨胀
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void batchInsert(List<ImportDTO> list) {
String sql = "INSERT INTO t_biz_data (...) VALUES (?, ?, ...)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ImportDTO dto = list.get(i);
ps.setString(1, dto.getField1());
// ... 设置其他参数
}
@Override
public int getBatchSize() {
return list.size();
}
});
}
}亮点说明
- 用
ReadListener逐行回调,内存只持有一批数据。 - 校验失败不走
throw,而是记错误表,保证整批导入不中断。 - 分批手动提交事务
REQUIRES_NEW,每 1000 条一个新事务,避免大事务。 JdbcTemplate.batchUpdate绕开 MyBatis 一级缓存,防止内存堆积。
2. 百万级数据导出:流式查询 + 流式写 Excel
@Slf4j
public class ExcelExportWorker {
private static final int QUERY_PAGE_SIZE = 5000;
public void export(Long taskId, ExportQuery query) {
// 直接获取OSS上传的OutputStream
try (OutputStream outputStream = ossService.putObjectStream(exportKey)) {
ExcelWriter excelWriter = EasyExcel.write(outputStream, ExportVO.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("数据页").build();
Long lastId = 0L;
List<ExportVO> pageList;
do {
// 流式分页查询(避免全量拖入内存)
pageList = exportMapper.selectByPage(lastId, QUERY_PAGE_SIZE, query);
if (!pageList.isEmpty()) {
excelWriter.write(pageList, writeSheet);
lastId = pageList.get(pageList.size() - 1).getId();
}
// 更新进度
taskService.updateProgress(taskId, lastId);
} while (pageList.size() == QUERY_PAGE_SIZE);
excelWriter.finish();
taskService.finish(taskId, exportKey);
} catch (Exception e) {
log.error("导出异常, taskId={}", taskId, e);
taskService.markFailed(taskId);
}
}
}对应的 MyBatis 流式查询配置(确保 MySQL 结果集不堆积)
@Select("SELECT * FROM t_biz_data WHERE id > #{lastId} AND ... ORDER BY id LIMIT #{size}")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
List<ExportVO> selectByPage(@Param("lastId") Long lastId, @Param("size") int size, @Param("query") ExportQuery query);亮点说明
- 分页基于 游标式 lastId,避免
offset大翻页性能问题。 - MyBatis 注解指定
FORWARD_ONLY + Integer.MIN_VALUE,驱动会逐条从 ResultSet 获取,不占客户端内存。 - EasyExcel 的
OutputStream直连 OSS 输出流,本地不产生临时文件。
3. 异步任务 & 进度反馈(Controller + MQ)
@RestController
public class ImportController {
@PostMapping("/import/upload")
public Result<Long> upload(@RequestParam("file") MultipartFile file) {
// 1. 文件直传OSS
String fileKey = ossService.upload(FileType.EXCEL_IMPORT, file.getInputStream());
// 2. 插入任务表
Long taskId = taskService.createTask(TaskTypeEnum.IMPORT, fileKey);
// 3. 发送MQ消息(解耦,异步化)
rocketMQTemplate.asyncSend("excel-import-topic", new ImportMsg(taskId, fileKey), null);
return Result.ok(taskId);
}
@GetMapping("/import/progress/{taskId}")
public Result<TaskProgressVO> progress(@PathVariable Long taskId) {
return Result.ok(taskService.getProgress(taskId));
}
}🧩 技术难点 & 解决方案总结
| 🤯 技术难点 | 原因剖析 | ✅ 解决方案 |
|---|---|---|
| 大文件上传 OOM | 前端整个文件加载到内存或应用端接收全部字节 | 1. 前端分片上传(可选) 2. 后端 Controller 直接流转发 OSS,不存本地 3. 上传完成后立即返回 taskId,异步处理 |
| Excel 解析内存爆炸 | POI XSSFWorkbook 全量 Load DOM | 使用 EasyExcel / POI SAX 事件驱动,逐行处理,内存恒定 |
| 数据库写入慢 + 事务膨胀 | 单条 insert,长事务 undo 累积 | 1. JdbcTemplate.batchUpdate 批量提交2. 每 1000 条拆分为独立事务( REQUIRES_NEW)3. 关闭 MyBatis 一级缓存或使用 BATCH 执行器 |
| 大导出数据量 OOM | select * 全量加载到 List 再写 Excel | 1. MyBatis 流式游标分页查询(FORWARD_ONLY + fetchSize)2. EasyExcel 多次 doWrite 分批写流 |
| 导入失败无法回溯 | 遇到一条数据异常就全盘失败,用户不知道哪错了 | 1. 校验异常不中断流程,收集到错误表 2. 记录行号、错误原因、原始数据 3. 任务完成后可导出错误 Excel |
| 长连接超时 | 同步处理导出/导入时,网关、Nginx、浏览器 504 Gateway Timeout | 彻底异步化:Controller 只负责接收请求,后台 Worker 跑完通知前端,前端轮询或 WebSocket 感知进度 |
| 并发导入致 DB 热点写入 | 多任务同时写同一张表,锁竞争激烈 | 1. 单表引入分区/分表 2. 每个 Worker 写入前通过 SELECT ... FOR UPDATE SKIP LOCKED 分批获取序列号3. 先入临时表,再 merge 主表 |
| 大文件下载中断 | 下载一半网络断开,需重头再来 | 1. OSS 支持断点续传 2. 前端用 Range 请求分片下载3. 服务端可以将大 Excel 切片后打包 zip,提高重试效率 |
| 内存泄漏 | 异常时未关闭 Workbook / InputStream | try-with-resources 严格管理所有流,EasyExcel 的 ReadListener 在 onException、doAfterAllAnalysed 做好资源释放 |
| 进度反馈不准确 | 只在任务完成才通知,用户焦虑 | 每隔一个批次更新 processed_count,前端轮询任务进度字段,并预估剩余时间 |
面试官:
“好的,代码和难点都讲得很透。这套组合拳基本覆盖了性能、稳定性、容错性。如果没有补充的话,这部分我可以给到 S 级 评价。👍”
候选人:
“谢谢面试官。😊 实际落地中还有一个小建议,就是监控告警:给任务表加超时状态,超过 10 分钟还没完成的自动发告警,避免静默失败。”
面试官:
“这个细节也注意到了,不错。那我们进入下一个环节……”
