流式解析HTTP请求multipart CSV文件校验后入库的最佳实现方案
大流量CSV流处理实现方案
核心约束与需求
- 输入:HTTP multipart请求返回的CSV输入流,单文件约50万行,每行格式为
邮箱;用户名,示例:aaaa@aaa.ru;Bogdan - 硬限制:禁止全量加载文件到内存、禁止生成临时文件落盘,仅能直接处理请求返回的InputStream
- 目标:逐行校验邮箱格式,有效数据批量写入数据库,无效邮箱统一收集返回
方案1:轻量原生实现(推荐,额外开销最低)
不需要引入Spring Batch这类重框架,核心逻辑是单线程顺序逐行读流+固定大小批量入库,全程内存占用稳定在MB级别,完全不会OOM。
核心实现代码
// 直接从MultipartFile获取输入流,全程不落盘、不缓存全量数据 try (InputStream is = multipartFile.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { // 跳过表头行,无表头可删除该行 br.readLine(); // 批量入库缓存,固定大小1000,凑满就提交 List<Person> validBatch = new ArrayList<>(1000); // 无效邮箱收集,50万行最多10%无效的场景下内存完全可控 List<String> invalidEmails = new ArrayList<>(); // 邮箱校验正则提前预编译,避免逐行创建重复对象 Pattern emailPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"); String line; while ((line = br.readLine()) != null) { // 按分号拆分,最多拆2段避免用户名里带分号导致数组越界 String[] cols = line.split(";", 2); if (cols.length < 2) { invalidEmails.add(line); continue; } String email = cols[0].trim(); String userName = cols[1].trim(); if (!emailPattern.matcher(email).matches()) { invalidEmails.add(email); continue; } validBatch.add(new Person(email, userName)); // 凑够批次大小直接入库,不要单条插入 if (validBatch.size() >= 1000) { personMapper.batchInsert(validBatch); validBatch.clear(); } } // 处理最后一批不足1000条的剩余数据 if (!validBatch.isEmpty()) { personMapper.batchInsert(validBatch); } // 后续直接返回invalidEmails即可 }
性能优化点
- 批量大小设置在500-2000区间是数据库插入性能最优区间,JDBC连接串务必添加
rewriteBatchedStatements=true参数,才能真正触发数据库批量写入,性能比单条循环插入高10倍以上 - 不要给流读取逻辑加并行多线程:InputStream本身不支持并发读取,强行开多线程会出现丢行、重复读、顺序错乱的问题,50万行数据单线程顺序读+批量插入总耗时不会超过1分钟,完全满足性能要求
方案2:Spring Batch适配无落盘流实现
你之前使用的FlatFileItemReader并非只能读取本地文件,将资源替换为输入流资源即可直接处理请求流,不需要落盘。
核心配置代码
@Bean @StepScope // 必须加步长域,因为输入流是请求级别,不是应用启动时加载的静态资源 public FlatFileItemReader<Person> personItemReader( @Value("#{jobExecutionContext['requestInputStream']}") InputStream inputStream) { FlatFileItemReader<Person> reader = new FlatFileItemReader<>(); reader.setLinesToSkip(1); // 直接传入请求输入流,不需要生成本地临时文件 reader.setResource(new InputStreamResource(inputStream)); // 配置分号分隔的行解析规则 DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(); tokenizer.setDelimiter(";"); tokenizer.setNames("email", "name"); DefaultLineMapper<Person> lineMapper = new DefaultLineMapper<>(); lineMapper.setLineTokenizer(tokenizer); lineMapper.setFieldSetMapper(fieldSet -> new Person( fieldSet.readString("email"), fieldSet.readString("name") )); reader.setLineMapper(lineMapper); reader.setStrict(false); return reader; }
后续自定义ItemProcessor做邮箱校验,将无效邮箱放入StepExecution上下文存储,不要使用普通ArrayList跨组件传递,避免线程安全问题。Chunk大小设置为1000即可,不要开启多线程Step:FlatFileItemReader本身是线程不安全的,多线程执行会出现数据错乱。
必看避坑点
- 禁止使用
Files.readAllLines、IOUtils.readLines、将流转为byte数组缓存这类操作,会把全量数据加载到内存,直接触发OOM - 邮箱校验不要用过于复杂的正则,覆盖常规格式即可,复杂正则会大幅拖慢逐行处理速度
- 流处理逻辑必须放在try-with-resources块中,保证请求流、数据库连接能被正常释放
- 不要为了“提升性能”盲目加异步多线程,流读取是整个流程的瓶颈点,而InputStream本身不支持并发读,加线程只会引入额外bug,不会带来性能提升
内容的提问来源于stack exchange,提问作者JD_UA
相关产品推荐
相关产品推荐

