导出XLSX数据时遇OutOfMemoryException问题求助(超6万条数据)
解决OpenXML导出大量数据时的OutOfMemoryException问题
我完全懂你现在的困扰——用OpenXML SDK的常规方式处理6万+条数据时,内存溢出真的是大概率事件。你当前的代码是把所有行、单元格对象都攥在内存里,直到整个工作表构建完才写入磁盘,这种方式对付小数据量没问题,但数据一上来内存肯定扛不住。咱们来一步步解决这个问题:
问题根源
你现在用的是OpenXML的**DOM(文档对象模型)**模式,这种模式会把Excel文档的所有元素(每一行、每一个单元格)都作为对象存在内存里。当数据量冲到6万条时,这些对象的内存占用会急剧膨胀,最终触发OutOfMemoryException。而CSV导出正常,是因为CSV是纯文本流式输出,不需要维护复杂的对象结构。
解决方案:用SAX模式流式写入
OpenXML SDK支持**SAX(Simple API for XML)**模式,也就是流式写入逻辑。这种模式下,我们会逐行生成XML内容并直接写入磁盘,内存里只会保留当前处理的那一行数据,不会把整个文档都存着。再搭配共享字符串表,还能进一步降低内存占用和最终的文件大小。
修改后的示例代码
下面是适配你场景的优化代码,用OpenXmlWriter实现流式写入:
// 先创建共享字符串表(可选但强烈推荐,减少重复字符串的内存浪费) var sharedStringPart = workbook.WorkbookPart.AddNewPart<SharedStringTablePart>(); var sharedStringTable = new SharedStringTable(); sharedStringPart.SharedStringTable = sharedStringTable; // 创建工作表部件,处理Sheet节点(和你原来的逻辑一致) var worksheetPart = workbook.WorkbookPart.AddNewPart<WorksheetPart>(); var relationshipId = workbook.WorkbookPart.GetIdOfPart(worksheetPart); uint sheetId = 1; if (sheets.Elements<DocumentFormat.OpenXml.Spreadsheet.Sheet>().Count() > 0) { sheetId = sheets.Elements<DocumentFormat.OpenXml.Spreadsheet.Sheet>().Select(s => s.SheetId.Value).Max() + 1; } DocumentFormat.OpenXml.Spreadsheet.Sheet sheet = new DocumentFormat.OpenXml.Spreadsheet.Sheet() { Id = relationshipId, SheetId = sheetId, Name = table.TableName }; sheets.Append(sheet); // 用OpenXmlWriter流式写入工作表内容 using (var writer = OpenXmlWriter.Create(worksheetPart)) { writer.WriteStartElement(new Worksheet()); writer.WriteStartElement(new SheetData()); // 写入表头行 writer.WriteStartElement(new Row()); int iCol = 1; List<string> columns = new List<string>(); foreach (System.Data.DataColumn column in table.Columns) { columns.Add(column.ColumnName); // 将表头字符串加入共享字符串表 var sharedStringItem = new SharedStringItem(new Text(column.ColumnName)); sharedStringTable.AppendChild(sharedStringItem); sharedStringTable.Save(); // 写入表头单元格 writer.WriteStartElement(new Cell()); writer.WriteAttributeString("t", "s"); // 标记为共享字符串类型 writer.WriteElement(new CellValue((sharedStringTable.Count() - 1).ToString())); // 引用共享字符串索引 writer.WriteAttributeString("r", $"{ExcelColumnLetter(iCol).Trim()}1"); writer.WriteEndElement(); // 闭合Cell标签 iCol++; } writer.WriteEndElement(); // 闭合Row标签 // 流式写入数据行 int iRow = 2; foreach (System.Data.DataRow dsrow in table.Rows) { writer.WriteStartElement(new Row()); iCol = 1; foreach (string col in columns) { string cellValue = dsrow[col].ToString(); // 先检查共享字符串表中是否已有该字符串,避免重复存储 var existingItem = sharedStringTable.Elements<SharedStringItem>() .FirstOrDefault(item => item.Text?.Text == cellValue); int stringIndex; if (existingItem != null) { stringIndex = sharedStringTable.IndexOf(existingItem); } else { var newItem = new SharedStringItem(new Text(cellValue)); sharedStringTable.AppendChild(newItem); sharedStringTable.Save(); // 及时写入磁盘,释放内存 stringIndex = sharedStringTable.Count() - 1; } // 写入数据单元格 writer.WriteStartElement(new Cell()); writer.WriteAttributeString("t", "s"); writer.WriteElement(new CellValue(stringIndex.ToString())); // writer.WriteAttributeString("r", $"{ExcelColumnLetter(iCol).Trim()}{iRow}"); // 可选,Excel会自动处理单元格位置 writer.WriteEndElement(); // 闭合Cell标签 iCol++; } writer.WriteEndElement(); // 闭合Row标签 iRow++; // 可选优化:每处理1000行就刷新一次,进一步释放内存 if (iRow % 1000 == 0) { sharedStringTable.Save(); writer.Flush(); } } writer.WriteEndElement(); // 闭合SheetData标签 writer.WriteEndElement(); // 闭合Worksheet标签 } workbook.Close();
关键优化点说明
- 流式写入(OpenXmlWriter):不需要把所有行对象都存在内存里,写完一行直接输出到磁盘,内存占用能降到极低。
- 共享字符串表(SharedStringTable):重复的字符串只存一次,通过索引引用,既减少内存占用,也能缩小最终的Excel文件体积。
- 定期刷新:每处理一定数量的行就调用
Flush()和Save(),及时把内存中的数据写入磁盘,避免内存堆积。
额外建议
- 如果你的程序是32位编译的,建议改成64位,这样程序能调用更多系统内存,进一步降低溢出概率。
- 检查
ExcelColumnLetter方法的实现,尽量避免在循环里做不必要的字符串拼接操作,保持高效。
内容的提问来源于stack exchange,提问作者K.Reed
相关产品推荐
相关产品推荐

