Spring Boot 批量数据导入性能优化实战指南 Spring Boot 批量数据导入性能优化实战指南一、问题背景在企业级系统中Excel 批量导入是高频业务场景。当导入数据量从几十条增长到数千条时未经优化的逐条处理方式会导致接口响应时间呈线性增长甚至触发 HTTP 超时。核心矛盾校验逻辑需要逐条判断但数据库交互不应逐条执行。二、性能瓶颈分析典型的未优化导入流程解析 Excel → 循环每条数据 { 查询表A验证是否存在1次DB 查询表B验证是否重复1次DB 插入记录1次DB } → 返回结果N 条数据 3N 次数据库交互耗时分布5000条数据操作单次耗时总次数累计耗时JDBC 连接获取/释放~1ms15000~15sSQL 执行~3-5ms15000~45-75s网络传输开销~0.5ms15000~7.5s合计~60-90s注博客https://blog.csdn.net/badao_liumang_qizhi三、优化方案对比方案 A逐条查询 逐条保存原始方式for(ExcelRowrow:dataList){Entityentityrepository.findByCode(row.getCode());// N次if(exists(entity)){fail;continue;}ExistingrecordrecordRepo.findByCode(row.getCode());// N次if(exists(record)){fail;continue;}recordRepo.save(newRecord);// N次}优点逻辑简单缺点DB 交互 3N 次5000 条约 60-90s方案 B批量预查询 内存校验 批量保存推荐// 1. 收集所有待校验的 codeListStringallCodesextractCodes(dataList);// 2. 批量查询转 Map/Set2次DBMapString,EntityentityMapbatchQuery(allCodes);SetStringexistingCodesbatchQueryExisting(allCodes);// 3. 内存中循环校验0次DBfor(ExcelRowrow:dataList){EntityentityentityMap.get(row.getCode());// O(1)if(existingCodes.contains(row.getCode())){fail;}// O(1)successList.add(buildRecord(row));}// 4. 批量保存1次DBrepository.saveAll(successList);优点DB 交互仅 3 次5000 条约 5-10s缺点内存占用略高方案 C异步 MQ 处理大数据量场景接口层解析Excel → 发MQ消息 → 立即返回taskId 消费端校验 保存 更新导入状态 前端轮询 taskId 获取进度优点不受 HTTP 超时限制支持数万条缺点架构复杂需 MQ 状态管理 前端轮询方案对比总结维度方案A逐条方案B批量预查询方案C异步MQDB 交互次数3N33异步5000条耗时60-90s5-10s接口1s实现复杂度低中高内存占用低中中适用场景100条100~10000条10000条四、关键技术点4.1 批量 IN 查询的注意事项MySQL 的IN子句有长度限制通常建议不超过 1000 个参数。当数据量超出时需分批查询// 分批查询每批1000条privateTListTbatchQuery(ListStringcodes,FunctionListString,ListTqueryFn){ListTresultnewArrayList();intbatchSize1000;for(inti0;icodes.size();ibatchSize){ListStringbatchcodes.subList(i,Math.min(ibatchSize,codes.size()));result.addAll(queryFn.apply(batch));}returnresult;}4.2 Map 构建时的去重策略一个 key 可能对应多条记录时需指定冲突合并策略// (m1, m2) - m1 表示取第一条MapString,Entitymaplist.stream().collect(Collectors.toMap(Entity::getCode,e-e,(m1,m2)-m1));4.3 saveAll 的批量 flush 配置JPA 默认saveAll仍是逐条persist需配合 Hibernate 批量配置才能真正发挥批量 INSERT 性能spring:jpa:properties:hibernate:jdbc:batch_size:500order_inserts:trueorder_updates:true4.4 批量保存的容错降级批量保存可能因某条数据违反约束而整体失败需降级为逐条保存以精确定位try{repository.saveAll(successList);// 尝试批量}catch(Exceptione){// 降级为逐条精确捕获失败记录for(Recordrecord:successList){try{repository.save(record);}catch(Exceptionex){failList.add(record.getCode(),ex.getMessage());}}}4.5 导入条数限制防止内存溢出和接口超时应在入口处限制单次导入量if(dataList.size()MAX_IMPORT_SIZE){thrownewBusinessException(单次导入不能超过MAX_IMPORT_SIZE条);}五、完整示例代码5.1 Controller 层Slf4jRestControllerRequestMapping(/api/employee)publicclassEmployeeImportController{privatestaticfinalintMAX_IMPORT_SIZE5000;ResourceprivateEmployeeImportServiceemployeeImportService;PostMapping(/import)publicRestResultImportResultDtoimportEmployee(RequestParam(file)MultipartFilefile){// 1. 文件校验if(filenull||file.isEmpty()){thrownewBizException(导入文件不能为空);}// 2. 解析ExcelListEmployeeExcelDtodataListExcelUtil.parse(file,EmployeeExcelDto.class);if(dataListnull||dataList.isEmpty()){thrownewBizException(导入数据为空请重新导入);}// 3. 条数限制if(dataList.size()MAX_IMPORT_SIZE){thrownewBizException(单次导入不能超过MAX_IMPORT_SIZE条当前数据量dataList.size()条);}// 4. 执行导入ImportResultDtoresultemployeeImportService.batchImport(dataList);returnRestResult.success(result);}}5.2 Service 层核心优化逻辑Slf4jServicepublicclassEmployeeImportServiceImplimplementsEmployeeImportService{ResourceprivateDepartmentRepositorydepartmentRepository;ResourceprivateEmployeeRepositoryemployeeRepository;ResourceprivateAliOssTemplatealiOssTemplate;OverridepublicImportResultDtobatchImport(ListEmployeeExcelDtodataList){ImportResultDtoresultDtonewImportResultDto();ListString[]failListnewArrayList();ListEmployeesuccessListnewArrayList();// 第一步提取所有待校验的编码 ListStringallDeptCodesdataList.stream().map(EmployeeExcelDto::getDeptCode).filter(Objects::nonNull).map(String::trim).distinct().collect(Collectors.toList());ListStringallEmpNosdataList.stream().map(EmployeeExcelDto::getEmpNo).filter(Objects::nonNull).map(String::trim).distinct().collect(Collectors.toList());// 第二步批量预查询仅2次DB // 查询所有部门转为 MapdeptCode, DepartmentMapString,DepartmentdeptMapnewHashMap();if(!allDeptCodes.isEmpty()){ListDepartmentdeptListdepartmentRepository.findByCodeIn(allDeptCodes);if(deptList!null){deptMapdeptList.stream().collect(Collectors.toMap(Department::getCode,d-d,(d1,d2)-d1));}}// 查询已存在的员工工号SetStringexistingEmpNosnewHashSet();if(!allEmpNos.isEmpty()){ListEmployeeexistingListemployeeRepository.findByEmpNoIn(allEmpNos);if(existingList!null){existingEmpNosexistingList.stream().map(Employee::getEmpNo).collect(Collectors.toSet());}}// 第三步内存中逐条校验0次DB SetStringbatchDuplicatenewHashSet();// 批次内去重for(EmployeeExcelDtodto:dataList){StringempNodto.getEmpNo();StringdeptCodedto.getDeptCode();// 校验工号不能为空if(empNonull||empNo.trim().isEmpty()){failList.add(newString[]{empNo,员工工号不能为空});continue;}StringtrimmedNoempNo.trim();// 校验批次内重复if(batchDuplicate.contains(trimmedNo)){failList.add(newString[]{empNo,工号在导入文件中重复});continue;}// 校验部门是否存在内存Map查找 O(1)DepartmentdeptdeptMap.get(deptCode!null?deptCode.trim():);if(deptnull){failList.add(newString[]{empNo,部门编码不存在});continue;}// 校验工号是否已存在内存Set查找 O(1)if(existingEmpNos.contains(trimmedNo)){failList.add(newString[]{empNo,员工工号已存在});continue;}// 校验通过构建实体EmployeeemployeenewEmployee();employee.setEmpNo(trimmedNo);employee.setName(dto.getName());employee.setDeptId(dept.getId());employee.setCreateTime(newDate());successList.add(employee);batchDuplicate.add(trimmedNo);}// 第四步批量保存1次DB intsuccessCount0;if(!successList.isEmpty()){try{employeeRepository.saveAll(successList);successCountsuccessList.size();}catch(Exceptione){log.error(批量保存失败降级为逐条保存,e);for(Employeeemp:successList){try{employeeRepository.save(emp);successCount;}catch(Exceptionex){failList.add(newString[]{emp.getEmpNo(),保存失败ex.getMessage()});}}}}// 第五步生成失败报告 resultDto.setSuccessCount(successCount);resultDto.setFailCount(failList.size());if(!failList.isEmpty()){resultDto.setFailFileUrl(generateFailExcel(failList));}returnresultDto;}/** * 生成失败数据Excel并上传OSS. */privateStringgenerateFailExcel(ListString[]failList){try(XSSFWorkbookworkbooknewXSSFWorkbook()){Sheetsheetworkbook.createSheet(导入失败数据);// 表头样式CellStyleheaderStylecreateHeaderStyle(workbook);RowheaderRowsheet.createRow(0);createCell(headerRow,0,员工工号,headerStyle);createCell(headerRow,1,失败原因,headerStyle);// 失败原因红色字体样式CellStylefailStylecreateFailStyle(workbook);// 填充数据for(inti0;ifailList.size();i){Rowrowsheet.createRow(i1);row.createCell(0).setCellValue(failList.get(i)[0]!null?failList.get(i)[0]:);Cellcellrow.createCell(1);cell.setCellValue(failList.get(i)[1]!null?failList.get(i)[1]:);cell.setCellStyle(failStyle);}sheet.setColumnWidth(0,20*256);sheet.setColumnWidth(1,35*256);// 写临时文件 → 上传OSS → 删除临时文件FiletempFileFile.createTempFile(import-fail-,.xlsx);try(FileOutputStreamfosnewFileOutputStream(tempFile)){workbook.write(fos);}StringurlaliOssTemplate.uploadFile(tempFile);tempFile.delete();returnurl;}catch(Exceptione){log.error(生成失败报告异常,e);return;}}privateCellStylecreateHeaderStyle(XSSFWorkbookworkbook){Fontfontworkbook.createFont();font.setBold(true);font.setColor(IndexedColors.WHITE.getIndex());CellStylestyleworkbook.createCellStyle();style.setFont(font);style.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());style.setFillPattern(FillPatternType.SOLID_FOREGROUND);style.setAlignment(HorizontalAlignment.CENTER);returnstyle;}privateCellStylecreateFailStyle(XSSFWorkbookworkbook){Fontfontworkbook.createFont();font.setColor(IndexedColors.RED.getIndex());CellStylestyleworkbook.createCellStyle();style.setFont(font);returnstyle;}privatevoidcreateCell(Rowrow,intcol,Stringvalue,CellStylestyle){Cellcellrow.createCell(col);cell.setCellValue(value);cell.setCellStyle(style);}}5.3 DTO 定义DatapublicclassImportResultDto{/** 成功数量. */privateintsuccessCount;/** 失败数量. */privateintfailCount;/** 失败文件下载URL. */privateStringfailFileUrl;}DatapublicclassEmployeeExcelDto{/** 员工工号. */privateStringempNo;/** 员工姓名. */privateStringname;/** 部门编码. */privateStringdeptCode;}六、性能对比结论数据量方案A逐条方案B批量预查询提升倍数100条~3s~0.5s6x1000条~15s~2s7.5x5000条~60s~5-10s6-12x核心思想将 N 次数据库 IO 压缩为常数次把校验逻辑从数据库驱动转变为内存驱动。