Excel、CSV 与导入导出:大数据文件怎样校验、分批和回滚
导入商品表时,编号 000123 应保留前导零,金额 1.234 是否允许舍入要由业务规则决定,备注里的换行仍属于同一条记录。文件能被打开,只解决了格式解析;数据怎样进入数据库,还取决于列定义、类型转换和提交方式。
导出则沿相反方向工作:先确定要读取哪些数据以及它们属于哪个时间视图,再选择单元格类型、文本编码和文件组织方式。文件大小、内存、临时磁盘和数据库事务会在这一过程中相互影响。
格式契约决定怎样解释每一列
CSV 的一条记录可以跨越多行
CSV 是文本记录格式,常见约定是逗号分隔字段、双引号包围包含分隔符或换行的字段,字段内的双引号写成两个双引号。下面只有一条数据记录,note 字段同时包含逗号、引号和换行:
sku,amount,note
001,1.20,"a,""b""
next"用 split(",") 会拆坏引号中的逗号,先按物理行切开又会拆坏多行字段。解析器必须持续跟踪引号状态,直到完整记录结束。错误报告中的“记录号”和“物理行号”应分别表达;用户按 Excel 行号查错时还要明确是否包含表头。RFC 4180
CSV 实际使用存在很多方言:分号或制表符分隔、CRLF 或 LF、是否有表头、是否允许空行、引号及转义规则都可能不同。不要根据操作系统区域设置静默猜测格式。模板下载和上传说明应固定 UTF-8、分隔符、表头版本及空值规则;不符合时明确报错或让用户选择导入格式。
UTF-8 BOM 可能出现在文件开始,未处理时,第一个列名会多出一个不可见字符。空字符串、只有空格、缺少一列与业务 NULL 各有含义,需要逐项约定。
文本处理也应按列决定:金额列是否 trim,备注是否保留首尾空格,都由该列契约指定。统一“清洗所有字符串”容易改掉本应保留的内容。
Apache Commons CSV 能处理这些格式选项。实验固定使用 1.14.1,并显式拒绝重复表头;在线 API 文档可能指向后续快照版本,使用新配置项前要核对所依赖的发行版。Commons CSV 下载与版本、Commons CSV API
XLSX 单元格不只有字符串
XLSX 是一个包含多个 XML 部件的 ZIP 容器;工作表、样式、共享字符串和关系信息分别保存。XLS 是较早的二进制格式,POI 使用 HSSF 处理;XLSX 的用户模型是 XSSF。不能把扩展名改成 xlsx 就得到有效工作簿。
同一个视觉内容可能有不同存储类型:显示为 000123 的单元格可能是字符串,也可能是数值 123 加上显示格式;显示为日期的单元格常以序列数值配合日期格式保存;公式单元格还有公式表达式和缓存结果。
| 业务字段 | 建议契约 | 需要处理的错误 |
|---|---|---|
| 商品编码、证件号、长编号 | 文本,保留前导零和全部字符 | 被 Excel 转为科学计数或丢失有效位后,服务端通常无法恢复原值 |
| 金额、数量 | 明确精度、范围、单位和舍入规则 | 千分位、货币符号、小数位超限、负数越界 |
| 日期或时间 | 明确日期格式、时区和是否允许空值 | 混用文本与序列值,1900/1904 日期系统差异,日期误当时间戳 |
| 枚举 | 使用固定编码或受控名称映射 | 拼写、空格、大小写与已停用选项 |
| 备注 | 文本,定义最大长度和换行规则 | 超长字段、控制字符、导出时触发公式 |
| 公式 | 明确拒绝、保留还是受限计算 | 缓存结果过期、不支持的函数、外部工作簿依赖 |
DataFormatter 适合取得展示文本,但财务导入不应只依靠展示结果推导精确值。已经以二进制浮点保存的数值,也不能通过 new BigDecimal(double) 假装找回用户最初输入的小数。CSV 金额或明确的文本金额可以从原始字符串构造 BigDecimal,再按约定精度校验。
公式处理需要单独选择。读取缓存结果很快,但可能不是最新计算值;POI 的 FormulaEvaluator 可以计算其支持的公式,并非完整 Excel 运行环境。只接收数据的业务模板通常拒绝公式列,错误中给出单元格坐标,让用户粘贴为值后重新上传。POI 公式计算
表头和数据规则要能定位错误
模板版本、必需列、可选列、重复列和额外列都应有规则。用列名映射可以允许列顺序调整;固定顺序模板则应直接核对完整表头。缺失必填列属于整文件错误,不必等到逐行导入才失败。
数据校验可分为三次:解析阶段识别语法和类型;记录级检查字段范围与同一行关系;批次或数据库检查唯一键、外键和业务状态。例如文件内重复 SKU 可以在进入数据库前报告,但数据库唯一约束仍需保留,防止与并发写入冲突。
错误结果至少包含源文件版本、工作表或记录号、列名、错误码和可修改的说明。原始单元格可能含个人信息,不应无差别写入集中日志。错误文件再次导出给用户时,同样要处理公式注入和下载权限。
用真实解析器写出文件并控制资源
生成一份可以读回的 XLSX
下载 表格处理实验工程,解压进入 file-tabular-lab。环境为 Linux、Bash 和 Docker,当前用户拥有 Docker 权限。依赖固定为 POI 5.5.1、Commons CSV 1.14.1、H2 2.5.250、JUnit 5.13.4;Java 源码按 17 编译,运行也可对照 Java 25。POI 发行版见 Apache POI 下载。
mkdir -p .m2
docker run --rm --user "$(id -u):$(id -g)" --entrypoint mvn \
-e MAVEN_CONFIG=/m2 -v "$PWD:/work" -v "$PWD/.m2:/m2" -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Dmaven.repo.local=/m2 clean verify
bash run.sh workbook work构建会运行六个测试;命令输出 xlsx rows=2000,并产生 work/export.xlsx。程序使用 SXSSF 写出真实工作簿,再通过 XSSFReader 和 SAX 解析实际工作表,统计 2,000 行。测试还以 XSSFWorkbook 打开结果,检查第一列为六位字符串,第三列的 =1+1 保持 STRING 类型。
生成部分的核心代码为:
try (var workbook = new SXSSFWorkbook(20)) {
workbook.setCompressTempFiles(true);
var sheet = workbook.createSheet("data");
for (int i = 0; i < rows; i++) {
var row = sheet.createRow(i);
row.createCell(0, CellType.STRING)
.setCellValue(String.format(Locale.ROOT, "%06d", i));
row.createCell(1, CellType.NUMERIC).setCellValue(i / 100.0);
row.createCell(2, CellType.STRING).setCellValue("=1+1");
}
try (var output = Files.newOutputStream(target)) {
workbook.write(output);
}
}这里的数值列用于展示类型选择;实际金额导出应先保证数据库精度和业务格式,决定以数值单元格还是精确文本交付。设置显示格式影响展示,不会增加 Excel 数值本身的有效精度。
XSSF、SAX 与 SXSSF 的内存来自不同地方
XSSF 用户模型把工作簿组织成可随机访问的对象,编程方便,适合规模受控的文件。大 XLSX 读取可以采用事件模型,只处理当前工作表的 XML 事件;写出可以用 SXSSF 保留有限行窗口,其余行刷到临时文件。POI Spreadsheet 使用指南
“流式”仍有需要计算的资源:SAX 可能读取样式和共享字符串表;SXSSF 的样式、合并区域及某些工作簿结构仍驻留内存,刷盘的 XML 也可能远大于最终压缩文件。每行创建一个新 CellStyle 会让样式数量不断增长,应复用少量样式对象。
行窗口中的旧行一旦刷出,不能再像普通 XSSF 那样任意回到旧行修改。需要跨行统计或合计时,可以先计算结果、分两遍读取,或者把汇总写在最终可写位置。大量自动列宽计算和公式计算也会消耗时间,不能只用“行数乘缓冲大小”估算整个任务。
关闭工作簿和输出流由 try-with-resources 执行。临时文件是否在 close 时处置,应按使用的 POI 版本核对;异常退出后还要观察任务专属临时目录。容器被强制终止时,Java 清理代码没有执行机会,持久化暂存盘上的残留需要后台扫描。SXSSFWorkbook API
SAX 回调提供列坐标和格式化值,但空白单元格可能没有对应事件,不能仅按“收到第几个回调”决定列号。示例只用它统计行数;实现业务导入还需处理单元格坐标、缺失列、原始类型、公式和日期规则。
CSV 转义与公式注入处理不同问题
正确加引号可以保证逗号和换行留在字段内,但电子表格仍可能把以等号等字符开头的字段解释为公式。来自用户输入的备注、错误说明和文件名,都可能进入导出内容。
交付给电子表格的 CSV 可以对风险字段增加文本前缀,并正确执行 CSV 引号转义;但不同软件、导入方式以及再次保存可能改变前缀处理,不能承诺通用的安全效果。需要保留任意原始文本时,使用明确 STRING 单元格的 XLSX 更容易表达类型;机器交换的原始 CSV 与供人工打开的安全展示文件可以分开提供。OWASP CSV Injection
实验用公式样式字符串测试 textForSpreadsheet 的基础前缀处理。XLSX 导出则直接使用 STRING,不把用户文本传给 setCellFormula。交付前仍需在目标办公软件中实际打开,检查它最终怎样解释这些字段。
批次事务与检查点怎样一起恢复
先决定失败时保留多少数据
导入通常有三种提交策略,业务语义要在开始执行前确定:
| 策略 | 发生错误时 | 适合的要求 |
|---|---|---|
| 全文件校验后一次提交 | 整个事务回滚,没有已提交部分 | 数据量受控且要求全有或全无 |
| 合法批次逐批提交 | 已提交批次保留,当前失败批次回滚 | 大文件、允许部分成功并需要续跑 |
| 先导入暂存表再发布 | 解析和检查先独立完成,发布另有规则 | 需要全量对账或审核后才对业务可见 |
已经提交的前几个批次,不能靠最后一次 rollback 撤销。产品如果提供“撤销本次导入”,需要记录该导入实际创建或修改的业务对象,并设计补偿规则;后续已有其他业务更新的数据不能被粗暴恢复成旧值。
事务大小同时影响锁持有时间、日志量、失败重做和吞吐。可以先按数百或数千行一批实验,再根据单行复杂度、数据库性能与可接受回滚量调整。调用 executeBatch 减少往返,commit 决定事务完成,两者不是同一个动作。实验每两行提交仅用于让中断结果容易观察。
在真实数据库中复现中断与续跑
使用新的工作子目录执行:
bash run.sh fixture work/import-a
if bash run.sh interrupt work/import-a > work/interruption.log 2>&1; then
printf '%s\n' '预期的批次中断没有发生' >&2
exit 1
fi
grep -q INJECTED_AFTER_BATCH_COMMIT work/interruption.log || exit 1
bash run.sh status work/import-a
bash run.sh resume work/import-a
bash run.sh resume work/import-afixture 生成四条数据,其中包含引号包围的逗号和多行备注。interrupt 解析真实 CSV,把前两条写入 H2 文件数据库,同时更新检查点,然后在 commit 之后抛出指定 IOException。
预期输出依次为:
import rows=2; checkpoint=2
import rows=4
import rows=4三个命令是独立 Java 进程,数据库文件保存在 work/import-a。第一次恢复重新解析相同输入,跳过已确认的两条记录,完成后两条;再次恢复仍然只有四条。测试还覆盖第二行金额非法时,尚未提交的第一行随当前批次回滚,以及修改源文件后恢复被 SOURCE_CHANGED 拒绝。
不要在已经完成的 import-a 上重复期待 interrupt 再次中断;改用新的目录,例如 work/import-b。源文件在计算摘要、解析及恢复期间保持不变,生产可固定对象版本或创建只读暂存副本。
检查点记录的是已经提交的业务进度
实验保存 source_hash 和 checkpoint,并把批次插入与检查点更新放进同一数据库事务。先更新检查点再写业务,崩溃后会跳过尚未写入的数据;先提交业务再单独更新检查点,崩溃后又可能重做已提交部分。
生产检查点还应关联 importId、租户、模板版本、解析器规则版本、工作表和源版本。只记录物理行号不适用于多行 CSV;若从文件头重新解析成本过高,可以采用稳定的记录分段索引,但不能从任意字节偏移直接进入引号字段中间。
示例每个数据库只保存一次导入,用 SKU 唯一键阻止重复业务行,恢复通过记录号跳过已提交前缀。多个进程共同处理同一导入时,还需要领取租约、版本条件更新和业务幂等键,避免两个执行者同时推进检查点。不能仅靠 checkpoint 数值碰巧相同判断某个批次已经由自己提交。
数据库事务也无法回滚已发送的邮件或第三方调用。批次需要触发外部通知时,可以在事务内保存待发送事件,再由独立消费者执行并去重。导入失败后重新运行时,仍以事件和业务操作的身份控制副作用;不要在逐行循环里直接发送通知。
导出读取视图和运行故障怎样判断
最大 ID 水位不能固定已有行的值
按 WHERE id > lastId ORDER BY id LIMIT ... 取下一页,通常比不断增大的 OFFSET 更适合大表。排序键需要稳定且足以唯一定位记录;复合排序要用完整游标,避免相同排序值的记录丢失或重复。
导出开始时记录最大 ID,可以限制后续新增高 ID 行进入结果。但已存在的行在导出期间仍可能修改或删除,补写较小 ID 也会影响结果。这个上界是枚举规则,不是某一时刻所有字段值的快照。
允许页间变化的导出,可以逐页读取最新已提交值,并在产品说明中标明“滚动读取”。需要固定数据时,可在数据库支持的隔离级别下保持事务视图,或先生成稳定的导出明细,再异步转换成文件。
长事务可能延长旧版本保留、占用连接并增加数据库清理压力。生成文件所需的十分钟,不必全部包含在业务数据库事务中;应按所选读取方式确定事务持续到哪个步骤。
运行双连接实验:
bash run.sh snapshot work连接 A 先读取金额 10,连接 B 更新为 20 并提交,再让 A 读取同一行。H2 2.5.250 的输出为:
READ_COMMITTED=10->20
REPEATABLE_READ=10->10这个结果只验证同一行的重复读取。H2 的 REPEATABLE_READ 仍可能发生幻读,不能据此声称它为任意范围查询提供完整稳定快照;H2 另有 SNAPSHOT 隔离语义。PostgreSQL、MySQL 对同名隔离级别及游标的实现也要分别验证。H2 高级主题:事务隔离
同样,设置 JDBC fetchSize 不会让所有驱动自动变成流式游标。H2 结果处理方式和生产数据库驱动不同。面向生产导出时,分别验证实际 SQL、驱动参数、事务保持方式和连接释放,观察堆内是否仍然保留完整结果集。
长任务需要可见进度和可取消的阶段
小文件可以同步生成并直接返回。大导出通常提交任务,保存申请者、过滤条件、数据视图、完成文件定位和有效期,再由用户查询状态。开始导出和下载结果时都检查权限,避免权限撤销后旧任务仍向用户交付敏感数据。
进度可以记录已扫描记录、有效记录、已提交批次和当前步骤;没有准确总量时显示已处理数量,不制造“卡在 99%”的精确百分比。取消发生在批次或输出检查点,先停止读取与新写入,再关闭工作簿和流,删除未发布结果。已被用户下载的副本无法通过取消任务收回。
输出文件应在私有位置完整写出、关闭并核对后登记可下载;把仍在写入的文件提前暴露给下载接口,会交付一个结构不完整的 ZIP 容器。解析 untrusted XLSX 还需限制压缩包展开、单元格/字符串规模及任务时间,相关归档处理见文件安全与生命周期。
把失败定位到具体处理阶段
| 现象 | 先检查什么 | 修复后的下一步 |
|---|---|---|
| 表头看起来相同却不匹配 | BOM、不可见空格、重复列、模板版本 | 显示明确列差异,重新提交正确模板 |
| 备注错列或记录数量异常 | 是否使用真正 CSV 解析器,分隔符及引号规则 | 对多行字段做单独回归,再重新解析原文件 |
| 编号变短或金额精度错误 | 原始单元格类型、Excel 转换、BigDecimal 规则 | 原值已丢失时要求重新提供源数据,不能猜测补全 |
| OOM、处理越来越慢 | Workbook 用户模型、共享字符串、样式数量、全量列表 | 改为受限事件处理或分批读取,验证实际内存曲线 |
| 临时盘满但堆不高 | SXSSF 临时 XML、压缩设置、任务并发、异常残留 | 停止新增任务,按任务归属回收,再缩小并发 |
| 失败后重复写入 | 检查点与业务是否同事务,输入版本是否固定 | 核对已提交批次,按幂等身份恢复 |
| 导出行数正确但金额混杂 | 页间是否读取不同版本,水位是否被误作快照 | 选择并验证稳定视图或明确滚动读取语义 |
实验容器随命令结束移除。work 中保留了可打开的 XLSX、原始 CSV 和记录批次进度的 H2 文件,重新运行恢复命令仍会使用这些数据。核对完文件与数据库后,再清理本次创建的目录。
