在日常工作中,Excel数据导出是一个常见的需求。
然而,当数据量较大时,性能和内存问题往往会成为限制导出效率的瓶颈。
当用户点击"导出"按钮时,后台系统往往会陷入三重困境:
内存黑洞:某电商平台在导出百万订单时,因传统POI方案导致堆内存突破4GB,频繁触发Full GC,最终引发服务雪崩;
时间漩涡:某物流系统导出50万运单耗时45分钟,用户多次重试导致数据库连接池耗尽;
磁盘风暴:某金融平台导出交易记录生成1.2GB文件,服务器磁盘IO飙升至100%;
我们结合 EPPlus、MiniExcel 和 NPOI 的 C# 高性能 Excel 导出方案对比及实现研究一下怎么提高导出效率。
一、技术方案核心对比
特性 | EPPlus | MiniExcel | NPOI |
---|---|---|---|
处理模型 | DOM | SAX 流式 | DOM/流式混合 |
内存占用 (100万行) | 1.2GB | 180MB | 850MB |
文件格式支持 | .xlsx | .xlsx/.csv | .xls/.xlsx |
公式计算 | 支持 | 不支持 | 部分支持 |
模板引擎 | 内置 | 模板语法 | 需要扩展 |
异步支持 | 有限 | 完全支持 | 不支持 |
NuGet 安装量 | 1.2亿+ | 800万+ | 2.3亿+ |
二、各方案选型建议
场景 | 推荐方案 | 示例代码特征 |
---|---|---|
简单数据导出 | MiniExcel 流式写入 | 使用 SaveAsAsync + 分块生成器 |
复杂格式报表 | EPPlus 模板引擎 | 样式预定义 + 分段保存 |
旧版 Excel 兼容 | NPOI 流式写入 | 使用 SXSSFWorkbook |
混合型需求 | MiniExcel + EPPlus 组合 | 模板分离 + 数据流式填充 |
超大数据量 (千万级) | 分片写入 + 并行处理 | 多 Task 分片 + 最终合并 |
三、性能对比数据
测试项 | EPPlus | MiniExcel | NPOI |
---|---|---|---|
100万行写入时间 | 42s | 18s | 65s |
内存峰值 | 1.1GB | 190MB | 820MB |
文件大小 | 86MB | 68MB | 105MB |
GC 暂停时间 | 1.4s | 0.2s | 2.1s |
线程资源占用 | 高 | 低 | 中 |
四、核心代码实现
1. MiniExcel 流式写入(推荐方案)
//配置优化参数var config = new OpenXmlConfiguration{ EnableSharedStrings =false,//关闭共享字符串表 AutoFilterMode = AutoFilterMode.None, //禁用自动筛选 FillMergedCells = false//不处理合并单元格};//分页流式写入awaitMiniExcel.SaveAsAsync("output.xlsx", GetDataChunks(), configuration: config);IEnumerable<IDictionary<string,object>>GetDataChunks(){var pageSize = 50000;for(int page = 0; ; page++) { var data = QueryDatabase(page * pageSize, pageSize); if(!data.Any())yieldbreak;foreach(varitemin data) { yieldreturnnewDictionary<string,object> { ["ID"] = item.Id, ["Name"] = item.Name, ["CreateTime"] = item.CreateTime.ToString("yyyy-MM-dd") }; } }}
优化点:
- 分页加载数据库数据
- 延迟加载数据生成器
- 关闭非必要功能
2. EPPlus 混合写入方案
using(var package = newExcelPackage()){var sheet = package.Workbook.Worksheets.Add("Data");int row = 1;//批量写入头信息sheet.Cells["A1:C1"].LoadFromArrays(new[] { new[] { "ID","Name","CreateTime" } }); //分块写入(每50000行保存一次)foreach(varchunkinGetDataChunks(50000)) { sheet.Cells[row+1,1].LoadFromCollection(chunk); row +=chunk.Count;if (row % 50000==0) { package.Save(); //分段保存 sheet.Cells.ClearFormulas(); } } package.SaveAs(newFileInfo("output_epplus.xlsx"));}
3. 性能对比测试代码
[BenchmarkDotNet.Attributes.SimpleJob]publicclassExcelBenchmarks{private List<DataModel> _testData = GenerateTestData(1_000_000); [Benchmark] publicvoid MiniExcelExport() => MiniExcel.SaveAs("mini.xlsx", _testData); [Benchmark] publicvoid EPPlusExport() { usingvar pkg = newExcelPackage();var sheet = pkg.Workbook.Worksheets.Add("Data"); sheet.Cells.LoadFromCollection(_testData); pkg.SaveAs("epplus.xlsx"); } [Benchmark] publicvoid NPOIExport() { var workbook = newXSSFWorkbook();var sheet = workbook.CreateSheet("Data");for(int i = 0; i < _testData.Count; i++) { var row = sheet.CreateRow(i); row.CreateCell(0).SetCellValue(_testData[i].Id); row.CreateCell(1).SetCellValue(_testData[i].Name); } usingvar fs = newFileStream("npoi.xlsx", FileMode.Create); workbook.Write(fs); }}
五、混合方案实现
1. EPPlus + MiniExcel 组合方案
// 先用 EPPlus 创建带样式的模板using(var pkg = newExcelPackage(newFileInfo("template.xlsx"))){var sheet = pkg.Workbook.Worksheets[0]; sheet.Cells["A1"].Value = "动态报表"; pkg.Save();}// 用 MiniExcel 填充大数据量var data =GetBigData();MiniExcel.SaveAsByTemplate("output.xlsx","template.xlsx", data);
2. 分片异步导出方案
publicasync Task ExportShardedDataAsync(){ var totalRecords =5_000_000;var shardSize =100_000;var shards = totalRecords /shardSize;var tasks = newList<Task>();for(int i = 0; i < shards; i++) { var start = i * shardSize; tasks.Add(Task.Run(async () =>{usingvar stream = newFileStream($"shard_{i}.xlsx", FileMode.Create); await MiniExcel.SaveAsAsync(stream, QueryData(start, shardSize)); })); } await Task.WhenAll(tasks); MergeShardFiles(shards);}privatevoidMergeShardFiles(intshardCount){usingvar merger = newExcelPackage();var mergedSheet = merger.Workbook.Worksheets.Add("Data");int row = 1;for(int i = 0; i < shardCount; i++) { var shardData = MiniExcel.Query($"shard_{i}.xlsx"); mergedSheet.Cells[row, 1].LoadFromDictionaries(shardData); row += shardData.Count(); } merger.SaveAs(newFileInfo("final.xlsx"));}
六、高级优化策略
1. 内存管理配置
// Program.cs 全局配置AppContext.SetSwitch("System.Buffers.ArrayPool.UseShared",true);//启用共享数组池//运行时配置(runtimeconfig.template.json){"configProperties": { "System.GC.HeapHardLimit":"0x100000000",// 4GB 内存限制"System.GC.HeapHardLimitPercent":"70","System.GC.Server":true}}
2. 数据库优化
// Dapper 分页优化public IEnumerable<DataModel> GetPagedData(longcheckpoint,intsize){return_conn.Query<DataModel>(@"SELECT Id, Name, CreateTime FROM BigTable WHERE Id > @Checkpoint ORDER BY Id OFFSET 0 ROWS FETCH NEXT @Size ROWS ONLY OPTION (RECOMPILE)",//强制重新编译执行计划new { checkpoint, size });}
3. 异常处理增强
try{awaitExportDataAsync();}catch (MiniExcelException ex) when (ex.ErrorCode == "DISK_FULL"){awaitCleanTempFilesAsync();awaitRetryExportAsync();}catch (SqlException ex) when (ex.Number == 1205)//死锁重试{awaitTask.Delay(1000);awaitRetryExportAsync();}finally{ _semaphore.Release(); //释放信号量}
七、最佳实践总结
1、数据分页策略
- 使用有序 ID 分页避免 OFFSET 性能衰减
//优化分页查询var lastId = 0;while(true){var data = Query($"SELECT * FROM Table WHERE Id > {lastId} ORDER BY Id FETCH NEXT 50000 ROWS ONLY");if(!data.Any())break; lastId =data.Last().Id;}
2、内存控制三位一体
- 启用服务器 GC 模式
- 配置共享数组池
- 使用对象池复用 DTO
3、异常处理金字塔
try{//核心逻辑}catch (IOException ex) when (ex.Message.Contains("磁盘空间")) { //磁盘异常处理}catch (SqlException ex) when (ex.Number == 1205) { //数据库死锁处理}catch (Exception ex) { //通用异常处理}
八、避坑指南
常见陷阱
EPPlus的内存泄漏
//错误示例:未释放ExcelPackagevar pkg = newExcelPackage();//必须包裹在using中pkg.SaveAs("leak.xlsx");//正确用法using(var pkg = newExcelPackage()){//操作代码}
NPOI的文件锁定
//错误示例:未正确释放资源var workbook = newXSSFWorkbook();//正确用法using(var fs = newFileStream("data.xlsx", FileMode.Create)){ workbook.Write(fs);}
异常处理最佳实践
try{awaitExportAsync();}catch (MiniExcelException ex) when (ex.ErrorCode == "DISK_FULL"){ _logger.LogError("磁盘空间不足: {Message}", ex.Message); awaitCleanTempFilesAsync();thrownewUserFriendlyException("导出失败,请联系管理员");}catch (DbException ex) when (ex.IsTransient){ _logger.LogWarning("数据库暂时性错误,尝试重试");awaitTask.Delay(1000);awaitRetryExportAsync();}finally{ _exportSemaphore.Release();}
九、典型场景建议
- 金融报表 → EPPlus(复杂公式+图表)
- 日志导出 → MiniExcel(千万级流式处理)
- 旧系统迁移 → NPOI(xls兼容)
- 动态模板 → MiniExcel模板引擎
通过合理的方案选择和优化配置,可实现:
- 内存消耗降低 80% 以上
- 导出速度提升 3-5 倍
- 系统稳定性显著增强
欢迎关注订阅微信公众号【熊泽有话说】,更多好玩易学知识等你来取
作者:熊泽-学习中的苦与乐
公众号:熊泽有话说
QQ群:711838388
出处:https://www.cnblogs.com/xiongze520/p/18866690
您可以随意转载、摘录,但请在文章内注明作者和原文链接。