AI 生成的 Excel 导入并行化方案,为什么“成功条数”经常对不上
文件导入功能看起来通常不复杂。用户上传一份 Excel系统读取每一行校验字段再把有效数据写入数据库。数据量少时一条条处理也许没问题当文件变成几千行、几万行开发者自然会想到并行化。于是AI 很容易给出类似方案AtomicIntegersuccessCountnewAtomicInteger();AtomicIntegerfailedCountnewAtomicInteger();rows.parallelStream().forEach(row-{try{validate(row);customerRepository.save(convert(row));successCount.incrementAndGet();}catch(Exceptione){failedCount.incrementAndGet();log.warn(import row failed: {},row.getRowNo(),e);}});最后返回导入完成成功 9856 条失败 144 条这段代码能运行也可能在小文件上表现正常。但文件一大、并发一高、数据规则一复杂很多问题会开始出现页面显示成功 9856 条数据库里实际只有 9821 条某些行显示失败但重试后又产生重复数据某个分片事务回滚了内存计数器已经提前累加应用重启后任务状态丢失不知道哪些行已经落库用户重新上传同一个文件系统重复写入导入结束后总数永远对不上日志里有异常但无法定位到底是哪一行、哪个字段、哪个批次出了问题。真正的问题往往不在 Excel 解析库也不在parallelStream()本身。而在于导入过程被当成了“逐行执行的循环”而不是一条需要记录状态、控制事务、允许恢复、最终对账的工程链路。一、最常见的错误把“处理成功”当成“数据已成功落库”下面这段逻辑很常见rows.parallelStream().forEach(row-{try{validate(row);customerRepository.save(convert(row));successCount.incrementAndGet();}catch(Exceptione){failedCount.incrementAndGet();}});它默认了一个前提代码走到successCount.incrementAndGet()就代表这一行已经成功完成。但真实系统中“成功”至少可能有不同含义成功类型含义解析成功Excel 这一行能正常被读取格式校验成功日期、数字、必填字段格式正确业务校验成功数据符合当前业务规则任务提交成功这一行已经被分配给某个处理分片数据库写入成功数据已经真正提交后续处理成功关联关系、索引、异步任务也已完成最终对账成功本批次统计与数据库结果一致如果把“方法没有抛异常”直接当作“导入成功”统计就很容易失真。例如T1校验通过成功计数 1 T2数据库写入暂时成功 T3当前事务后续执行异常 T4事务整体回滚 T5内存中的成功数不会自动减回去最终用户看到的是“成功 1 条”数据库里却没有这条记录。所以导入统计不能只依赖内存变量。真正可信的成功数应以可追踪的持久化状态为准。二、先确定导入规则全量原子还是允许部分成功在写任何并行代码前先要回答一个业务问题这份文件里只要有一行失败整批是否都应该失败不同业务场景答案可能不同。导入类型更常见的处理策略财务结算、关键配置、权限规则通常要求全量成功或整体回滚商品标签、客户备注、运营名单往往允许部分成功并返回错误行历史数据迁移常用分批提交 可重试 完整审计大量主数据初始化常用暂存表校验后再统一入库外部系统同步文件常用可恢复批次 行级状态记录这一步不能交给并行框架决定。例如如果业务要求“全量原子”那么把数据拆成多个独立事务并行执行可能就不符合业务预期。即使 9999 行成功最后 1 行失败也不能简单地保留前 9999 行。反过来如果业务允许部分成功那么“任意一行失败就回滚整批”又会让大文件导入的重试成本过高。因此正确设计顺序应当是先定义业务成功标准 ↓ 再确定事务边界 ↓ 再决定是否需要并行 ↓ 最后才写处理代码三、不要直接把 Excel 行当成业务数据先建立导入批次一个更稳定的设计通常会把“文件导入”拆成批次和行状态。例如创建导入批次表CREATETABLEimport_batch(idBIGINTPRIMARYKEYAUTO_INCREMENT,batch_noVARCHAR(64)NOTNULL,file_hashVARCHAR(64)NOTNULL,total_rowsINTNOTNULLDEFAULT0,parsed_rowsINTNOTNULLDEFAULT0,valid_rowsINTNOTNULLDEFAULT0,success_rowsINTNOTNULLDEFAULT0,failed_rowsINTNOTNULLDEFAULT0,statusVARCHAR(32)NOTNULL,created_atDATETIMENOTNULL,finished_atDATETIMENULL,UNIQUEKEYuk_file_hash(file_hash));再建立行级暂存表CREATETABLEimport_row_stage(idBIGINTPRIMARYKEYAUTO_INCREMENT,batch_idBIGINTNOTNULL,row_noINTNOTNULL,row_hashVARCHAR(64)NOTNULL,payload_jsonTEXTNOTNULL,statusVARCHAR(32)NOTNULL,error_codeVARCHAR(64)NULL,error_messageVARCHAR(512)NULL,target_idBIGINTNULL,retry_countINTNOTNULLDEFAULT0,updated_atDATETIMENOTNULL,UNIQUEKEYuk_batch_row(batch_id,row_no));这样做以后导入过程不再只是读取一行 ↓ 写一行 ↓ 计数 1而变成创建导入批次 ↓ 解析文件并写入暂存表 ↓ 结构校验 ↓ 业务校验 ↓ 分片处理 ↓ 记录每行处理结果 ↓ 汇总统计 ↓ 最终对账这带来的最大价值是即使应用中途重启也能知道这个批次目前执行到了哪里。四、错误但常见的做法校验、写库、统计全部混在一个并行循环里很多导入代码会把所有动作塞进一起rows.parallelStream().forEach(row-{validateFormat(row);validateBusinessRule(row);repository.save(convert(row));successCount.incrementAndGet();});这种写法的问题是失败后的信息很难区分。比如一行数据失败到底是Excel 日期格式不合法必填字段为空业务唯一键重复关联对象不存在数据库连接超时死锁重试失败外部校验服务异常当前批次已经被重复提交这些错误的处理方式完全不同。更清晰的做法是把导入状态拆开状态含义下一步PARSED已读取原始行进入格式校验FORMAT_INVALID格式不合法返回错误报告BUSINESS_INVALID不符合业务规则返回错误报告或人工处理READY已通过校验待写入进入处理队列PROCESSING当前分片正在处理防止重复领取SUCCESS已确认提交成功计入成功统计FAILED_RETRYABLE临时失败可重试进入重试任务FAILED_FINAL不可重试需要人工确认留存错误证据状态一旦清晰统计也会更可靠SELECTstatus,COUNT(*)AStotalFROMimport_row_stageWHEREbatch_id:batchIdGROUPBYstatus;用户最终看到的结果就不再依赖某个 JVM 内存里的计数器而是来自数据库中的真实处理状态。五、分片并行可以做但每个分片必须有明确边界对允许部分成功的大文件导入可以按固定范围分片。例如每 500 行一个任务第 1 个分片第 1 - 500 行 第 2 个分片第 501 - 1000 行 第 3 个分片第 1001 - 1500 行每个分片处理时应该先领取自己负责的行TransactionalpublicListImportRowStageclaimRows(LongbatchId,intfromRow,inttoRow){returnimportRowStageRepository.findReadyRowsForUpdate(batchId,fromRow,toRow);}随后分片内用独立事务处理TransactionalpublicvoidprocessChunk(LongbatchId,intfromRow,inttoRow){ListImportRowStagerowsclaimRows(batchId,fromRow,toRow);for(ImportRowStagerow:rows){try{targetRepository.save(convert(row.getPayloadJson()));row.markSuccess();}catch(TransientDataAccessExceptione){row.markRetryableFailure(DATABASE_TEMPORARY_ERROR,e.getMessage());}catch(BusinessExceptione){row.markFinalFailure(BUSINESS_VALIDATION_FAILED,e.getMessage());}}}这里的重点不是“所有行都必须在一个事务里”。而是分片范围固定同一行不会被多个任务重复领取每一行都有明确状态事务提交后才允许计入最终成功数临时失败和业务失败能够分开处理任务中断后可以从未完成状态恢复。六、让 AI 先帮助拆分导入边界而不是直接生成并行代码如果只问 AI帮我把 Excel 导入改成多线程提高速度。它很可能会给出parallelStream()、线程池或CompletableFuture。这些工具本身没有错。但它们不会自动替你解决是否要求全量回滚哪些数据允许部分成功导入重试是否会重复写入如何保存错误行如何防止相同文件重复提交如何在应用重启后恢复任务如何解释“成功条数为什么对不上”。更有效的提问方式是你是后端批量导入方案评审助手。 场景 系统需要导入一个最多 5 万行的 Excel 文件。 部分格式错误允许返回错误行 数据库临时异常允许重试 重复上传同一个文件不能产生重复数据 应用重启后需要恢复未完成任务。 请不要直接给 parallelStream 或线程池代码。 请输出 1. 导入批次与行级状态设计 2. 全量回滚与部分成功分别适合哪些场景 3. 分片范围和事务边界如何划分 4. 文件重复提交和行重复写入如何控制 5. 失败后如何区分可重试与不可重试 6. 最终成功数应如何统计 7. 至少 8 个并发、重试和中断恢复测试场景。这类输入能让 AI 先参与“系统边界设计”而不是只负责把循环改成并发。对于已经把 ChatGPT Plus、GPT Plus 用在代码解释、导入方案评审、异常分类和测试清单整理中的开发者来说长期使用的价值不在于让工具更快生成并行代码而在于能否把数据边界、处理状态和验证规则沉淀成固定工作流。对已经确认有 AI 工具长期使用需求的开发者来说工具准备不只是模型能力还包括使用周期、说明理解、边界意识和异常处理路径相关信息可按实际需要参考gpt985com七、导入结束后必须做一次最终对账很多导入任务结束时只会返回成功 9856 条失败 144 条但一个真正可靠的导入结果至少应该满足总行数 解析失败数 格式失败数 业务失败数 成功数 待重试数 处理中数如果这个等式不成立说明至少有一种状态没有被正确记录。可以用下面的 SQL 做基础核对SELECTb.total_rows,SUM(CASEWHENr.statusSUCCESSTHEN1ELSE0END)ASsuccess_rows,SUM(CASEWHENr.statusIN(FORMAT_INVALID,BUSINESS_INVALID,FAILED_FINAL)THEN1ELSE0END)ASfinal_failed_rows,SUM(CASEWHENr.statusFAILED_RETRYABLETHEN1ELSE0END)ASretryable_rows,SUM(CASEWHENr.statusPROCESSINGTHEN1ELSE0END)ASprocessing_rowsFROMimport_batch bLEFTJOINimport_row_stage rONb.idr.batch_idWHEREb.id:batchIdGROUPBYb.id,b.total_rows;最终结果不应该只有“成功”和“失败”两种。还要能明确区分哪些行需要用户修正后重新上传哪些行正在等待重试哪些行需要人工确认哪些数据已经持久化成功哪些批次还没有真正结束。八、至少覆盖这些测试场景文件导入在开发环境中最容易“看起来没问题”。因为本地通常只有小文件、低并发、单线程和稳定数据库。上线前建议至少覆盖测试场景预期结果正常文件导入总数与成功数一致Excel 中存在格式错误行返回准确行号与错误原因同一文件重复上传不重复写入或明确提示重复批次单个分片数据库异常仅该分片进入重试不影响已提交分片处理过程中应用重启可从未完成状态恢复同一批次任务重复触发不重复领取同一行分片事务回滚成功统计不提前增加外部校验服务超时进入可重试状态并保留错误证据大文件导入内存、线程池、数据库连接保持可控最终对账总行数与各状态汇总一致还需要注意分片并行不是越多越快。线程数过高可能导致数据库连接池耗尽单表锁竞争加重错误日志短时间爆发同一业务对象被并发写入重试任务与正常任务互相挤占资源。所以分片数量、线程池大小和单批大小都应该在压测环境中根据实际数据库、连接池和业务规则确定。九、结语Excel 导入的难点从来不只是把一行数据写进数据库。真正容易出问题的是成功统计是否代表真实提交错误行是否能被定位和解释部分成功是否符合业务要求重试是否会重复写入应用重启后能否恢复同一文件是否会被重复处理最终统计是否能与数据库状态对上。AI 可以帮助你生成解析代码、拆分校验规则、补齐状态机和测试案例。但如果没有导入批次、行级状态、事务边界和最终对账再快的并行导入也只是“更快地制造无法解释的数据差异”。真正可靠的批量导入不是跑完一个循环。而是每一行数据最后都能被回答它去了哪里、为什么成功、为什么失败、是否还能继续处理。