Spring Boot读取50万行Excel后执行saveAll时触发SocketTimeoutException
问题:导入50万行Excel后执行saveAll()出现SocketTimeoutException且无控制台异常输出
在Spring Boot应用中读取包含50万余行数据的Excel文件时,已完成所有行的读取操作后,出现java.net.SocketTimeoutException: Read timed out异常,且控制台未显示任何错误或异常信息。经排查发现,代码执行到xyzsRepository.saveAll()方法时停滞,相关代码如下:
private List<XYZEntry> importFile(XYZ file) throws ServiceException { var xyzToImport = new HashSet<XYZ>(); List<XYZEntry> invalidXYZ = new ArrayList<>(); try(var wb = WorkbookFactory.create(new ByteArrayInputStream(file.getContent()))) { for(var row: wb.getSheetAt(0)) { if(row.getRowNum() == 0) { // 跳过表头 continue; } DataFormatter formatter = new DataFormatter(); var potentialXYZ = formatter.formatCellValue(row.getCell(0)).trim(); if(isXYZValidToImport(potentialXYZ)) { xyzsToImport.add(new XYZ(potentialXYZ)); // 注意:变量名与定义的xyzToImport不一致,存在笔误 } else { invalidXYZ.add(new XYZEntry(potentialXYZ, row.getRowNum() + 1)); } } if(CollectionUtils.isNotEmpty(invalidXYZ)) { file.setInvalidEntries(invalidXYZ.size()); log.info( "导入文件{}时,跳过了{}条无效XYZ:{}", file.getFilename(), invalidXYZ.size(), String.join(",", invalidXYZ.toString()) ); } if(CollectionUtils.isEmpty(xyzToImport)) { log.warn( "文件{}中未找到有效XYZ,无数据导入。", file.getFilename() ); file.setStatus(XYZStateEnum.NO_IMPORTS); } else { transactionHelper.executeInNewTransaction(() -> { xyzsRepository.deleteAll(); xyzsRepository.saveAll(xyzsToImport); // 此处代码无法到达 return null; }); file.setValidEntries(xyzsToImport.size()); file.setStatus(XYZStateEnum.IMPORTED); } } catch(IOException e) { file.setStatus(XYZStateEnum.ERROR); filesRepository.save(file); throw new ServiceException("XYZ导入失败。", e); } filesRepository.save(file); return invalidXYZ; }
问题原因分析
- 变量名笔误:代码中定义的变量是
xyzToImport,但后续添加元素时使用的是xyzsToImport,会导致编译错误,需优先修正。 - 批量插入效率低下:直接调用
saveAll()插入50万条数据,默认会逐条执行SQL(Hibernate默认行为),耗时极长,触发数据库连接超时。 - 事务未设置超时:新事务中执行
deleteAll()+saveAll()大操作,未配置事务超时时间,导致底层数据库连接因等待过久超时。 - 异常未完整捕获:事务内部的异常未被显式捕获输出,仅在底层抛出
SocketTimeoutException,控制台无异常日志。 - 数据库连接超时配置不足:数据库连接池或数据库本身的超时阈值设置过短,无法支撑大批次数据操作的耗时。
修复方案
1. 修正变量名笔误
将xyzToImport统一修正为xyzsToImport,确保变量引用一致:
var xyzsToImport = new HashSet<XYZ>();
2. 优化批量插入逻辑
开启JPA批量插入支持,并分批次插入数据,降低内存占用和数据库压力:
- 配置文件(application.yml)添加批量插入参数:
spring: jpa: properties: hibernate: jdbc: batch_size: 500 # 每批次插入500条 order_inserts: true # 确保插入语句按顺序执行,提升批量效率 order_updates: true
- 改造
saveAll()为分批次插入:
transactionHelper.executeInNewTransaction(() -> { xyzsRepository.deleteAllInBatch(); // 用批量删除替代deleteAll(),提升效率 List<XYZ> xyzList = new ArrayList<>(xyzsToImport); int batchSize = 500; for (int i = 0; i < xyzList.size(); i += batchSize) { int endIdx = Math.min(i + batchSize, xyzList.size()); xyzsRepository.saveAll(xyzList.subList(i, endIdx)); // 手动刷新并清理实体管理器,避免内存溢出 entityManager.flush(); entityManager.clear(); } return null; });
注:需注入
EntityManager实例到当前类中使用。
3. 设置事务超时
为大操作事务设置足够的超时时间(例如300秒),避免事务未完成就被中断:
如果transactionHelper支持传入超时参数,可直接指定:
transactionHelper.executeInNewTransaction(() -> { // 批量操作逻辑 return null; }, 300); // 超时时间300秒
如果是使用@Transactional注解,可添加timeout属性:
@Transactional(timeout = 300) public void batchImport(List<XYZ> xyzList) { // 批量操作逻辑 }
4. 完善异常捕获与日志输出
在事务内部添加异常捕获,确保异常能被记录到控制台:
transactionHelper.executeInNewTransaction(() -> { try { xyzsRepository.deleteAllInBatch(); // 分批次插入逻辑 return null; } catch (Exception e) { log.error("批量导入XYZ数据失败", e); throw e; // 抛出异常触发事务回滚 } });
5. 调整数据库连接超时配置
修改数据库连接池(以HikariCP为例)和数据库的超时参数,适配大批次操作:
- 连接池配置:
spring: datasource: hikari: connection-timeout: 300000 # 连接超时5分钟 idle-timeout: 300000 # 空闲连接超时5分钟 max-lifetime: 600000 # 连接最大生命周期10分钟
- MySQL数据库配置(my.cnf):
wait_timeout = 600 # 非交互连接超时10分钟 interactive_timeout = 600 # 交互连接超时10分钟
内容的提问来源于stack exchange,提问作者tdog
相关产品推荐
相关产品推荐

