百万数据Excel如何快速导入导出

百万数据Excel如何快速导入导出

🎯 核心结论(开门见山)

绝对不能用传统的 POI UserModel 模式! 百万数据会直接导致 OOM 内存溢出。核心思路是:流式处理 + 分批操作 + 异步任务 + 资源隔离,这是大厂解决此类问题的标准范式。

📊 整体架构流程图


关于这个问题的底层原理和更多实战细节,我整理了一份《大厂面试手册》,包含大厂高频面试题、源码解析和性能调优案例。

关注公众号【Rain的Java大神之路】,回复"Java"即可免费领取,持续更新中。


📥 导入方案(核心是 "读" 的优化)

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

🛡️ 兜底方案与监控

  • 资源隔离:使用独立的线程池处理导入导出任务,避免影响核心业务
  • 熔断降级:当系统负载过高时,自动拒绝新的导入导出请求
  • 监控告警:监控任务执行时间、失败率、内存使用情况,异常时及时告警
  • 数据备份:导入前备份相关表,出现问题时可以快速回滚

🎯 面试加分项(让面试官眼前一亮)

  1. 提到EasyExcel 的底层原理:基于 SAX 解析,使用了 ASM 字节码技术生成代理类,避免反射开销
  2. 提到数据库优化:导入时关闭自动提交,使用rewriteBatchedStatements参数,临时关闭索引
  3. 提到分布式场景:使用分布式任务调度(XXL-Job),支持多节点并行处理
  4. 提到用户体验:导入失败时生成错误报告 Excel,标明错误行号和原因
  5. 提到安全问题:限制文件大小和上传频率,防止恶意攻击

📝 总结

百万级 Excel 导入导出的核心不是用什么框架,而是 "流式" 和 "分批" 这两个思想。只要记住:不把整个文件加载到内存,不把所有数据一次性写入数据库,再配合异步任务和资源隔离,就能轻松解决这个问题。

📌 新增:核心代码实现(带技术亮点标注)

1. 全局配置(资源隔离 + 性能调优)

java 复制代码
/**
 * 🔹 技术亮点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 + 多线程分片 + 批量插入)

java 复制代码
/**
 * 🔹 技术亮点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 流式写入 + 分批查询)

java 复制代码
/**
 * 🔹 技术亮点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. 数据库配置(关键参数)

yaml 复制代码
# 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. 生成错误报告 Excel3. 支持单条 / 批量重试4. 错误报告下载链接通知 用户可直接根据错误报告修正数据
📦 分布式场景问题 单节点处理能力有限,单点故障导致任务丢失 1. 使用 XXL-Job 分布式任务调度2. 任务分片广播,多节点并行处理3. 任务幂等性设计4. 任务状态持久化 支持水平扩展,处理能力随节点数线性提升
📋 Excel 格式兼容问题 不同版本 Excel(xls/xlsx)解析失败,合并单元格 / 公式处理错误 1. EasyExcel 自动兼容 xls/xlsx2. 忽略 Excel 公式,使用计算后的值3. 自定义合并单元格处理器 兼容 99% 以上的 Excel 文件格式
🛡️ 安全与防护问题 恶意用户上传超大文件,导致服务器磁盘 / 内存耗尽 1. 限制文件大小(最大 100MB)2. 限制用户每日上传次数3. 文件类型白名单校验4. 流量控制与熔断降级 防止恶意攻击,保证系统稳定性

📝 面试加分金句(直接背)

  1. " 百万级 Excel 处理的核心不是用什么框架,而是流式思想------ 永远不要把整个文件加载到内存 "
  2. "JDBC 批量插入的关键是rewriteBatchedStatements=true,没有这个参数,MyBatis 的 foreach 还是单条插入 "
  3. "导入导出属于 IO 密集型任务,线程池核心线程数应该设置为 CPU 核心数的 2 倍"
  4. "SXSSF 的临时文件一定要手动调用dispose()清理,否则会导致磁盘泄漏 "
  5. "异步任务是必须的,否则用户会因为接口超时重复提交,造成数据重复"

真实面试模拟

真实面试模拟

面试官:

"行,之前基础问得差不多了,咱们来个场景设计吧。你项目里遇到过百万级数据的 Excel 导入导出吗?让你来设计的话,怎么保证又快速又稳定?"

候选人:

"遇到过类似的。百万级数据,如果单靠简单一把梭,内存直接爆掉,接口等到超时,数据库也会被打垮。所以我设计的核心思路就三点:异步削峰、流式处理、分批入库。😊 我可以分导入、导出两块详细说。"

面试官:

"好,先讲导入,从用户上传开始,链路怎么走?"

候选人:

"没问题,我在白板上画一下调用链路吧。"

"核心点有四个:"

  1. 文件不落地上传 :Controller 拿到流直接推到 OSS,不在应用服务器积压,然后扔一个 MQ 消息就立刻返回 taskId。前端拿这个轮询进度。📦
  2. 流式读取 :绝对不用 POI 的 XSSFWorkbook,那会把文件全 load 进内存。用 EasyExcelReadListener 或者 POI 的 SAX 模式,逐行回调,内存只占几十 MB。🚀
  3. 分批写入:内存中攒够 1000 行,用 JDBC batch 一把提交,然后及时 commit 事务,避免长事务锁表。
  4. 校验和错误收集 :数据校验在内存批里做,不合格的行不丢弃,记到 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 流式导入 + 分批入库 + 错误收集

java 复制代码
@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

java 复制代码
@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 结果集不堆积)

java 复制代码
@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)

java 复制代码
@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 的 ReadListeneronExceptiondoAfterAllAnalysed 做好资源释放
进度反馈不准确 只在任务完成才通知,用户焦虑 每隔一个批次更新 processed_count,前端轮询任务进度字段,并预估剩余时间

面试官:

"好的,代码和难点都讲得很透。这套组合拳基本覆盖了性能、稳定性、容错性。如果没有补充的话,这部分我可以给到 S 级 评价。👍"

候选人:

"谢谢面试官。😊 实际落地中还有一个小建议,就是监控告警:给任务表加超时状态,超过 10 分钟还没完成的自动发告警,避免静默失败。"

面试官:

"这个细节也注意到了,不错。那我们进入下一个环节......"


如果本文对你有帮助,欢迎关注我的公众号【Rain的Java大神之路】。

专注 Java 面试、源码、高并发实战,回复"Java"领取《大厂面试手册》,持续更新。

相关推荐
Rain的Java大神实战圈1 天前
如何快速上传10G文件
场景设计题
Rain的Java大神实战圈4 天前
数据脱敏是怎么做的
场景设计题
Rain的Java大神实战圈5 天前
对加密的手机号如何进行模糊查询
场景设计题
Rain的Java大神实战圈6 天前
如何自定义一个MyBatis插件
场景设计题
Rain的Java大神实战圈7 天前
高并发下的抽奖系统:不只是扣库存,还要防作弊与风控
场景设计题
Rain的Java大神实战圈10 天前
高并发下的秒杀系统架构全景:从单体到云原生的演进之路
场景设计题
Rain的Java大神实战圈11 天前
Spring事务失效的场景有哪些
场景设计题
Rain的Java大神实战圈12 天前
高并发下的排行榜系统:如何用Redis快速实现千万用户实时排名
场景设计题
Rain的Java大神实战圈13 天前
手写一个Spring容器
场景设计题