C#實(shí)現(xiàn)高性能Excel百萬數(shù)據(jù)導(dǎo)出優(yōu)化實(shí)戰(zhàn)指南
在日常工作中,Excel數(shù)據(jù)導(dǎo)出是一個(gè)常見的需求。
然而,當(dāng)數(shù)據(jù)量較大時(shí),性能和內(nèi)存問題往往會成為限制導(dǎo)出效率的瓶頸。
當(dāng)用戶點(diǎn)擊"導(dǎo)出"按鈕時(shí),后臺系統(tǒng)往往會陷入三重困境:
內(nèi)存黑洞:某電商平臺在導(dǎo)出百萬訂單時(shí),因傳統(tǒng)POI方案導(dǎo)致堆內(nèi)存突破4GB,頻繁觸發(fā)Full GC,最終引發(fā)服務(wù)雪崩;
時(shí)間漩渦:某物流系統(tǒng)導(dǎo)出50萬運(yùn)單耗時(shí)45分鐘,用戶多次重試導(dǎo)致數(shù)據(jù)庫連接池耗盡;
磁盤風(fēng)暴:某金融平臺導(dǎo)出交易記錄生成1.2GB文件,服務(wù)器磁盤IO飆升至100%;
我們結(jié)合 EPPlus、MiniExcel 和 NPOI 的 C# 高性能 Excel 導(dǎo)出方案對比及實(shí)現(xiàn)研究一下怎么提高導(dǎo)出效率。
一、技術(shù)方案核心對比
| 特性 | EPPlus | MiniExcel | NPOI |
|---|---|---|---|
| 處理模型 | DOM | SAX 流式 | DOM/流式混合 |
| 內(nèi)存占用 (100萬行) | 1.2GB | 180MB | 850MB |
| 文件格式支持 | .xlsx | .xlsx/.csv | .xls/.xlsx |
| 公式計(jì)算 | 支持 | 不支持 | 部分支持 |
| 模板引擎 | 內(nèi)置 | 模板語法 | 需要擴(kuò)展 |
| 異步支持 | 有限 | 完全支持 | 不支持 |
| NuGet 安裝量 | 1.2億+ | 800萬+ | 2.3億+ |
二、各方案選型建議
| 場景 | 推薦方案 | 示例代碼特征 |
|---|---|---|
| 簡單數(shù)據(jù)導(dǎo)出 | MiniExcel 流式寫入 | 使用 SaveAsAsync + 分塊生成器 |
| 復(fù)雜格式報(bào)表 | EPPlus 模板引擎 | 樣式預(yù)定義 + 分段保存 |
| 舊版 Excel 兼容 | NPOI 流式寫入 | 使用 SXSSFWorkbook |
| 混合型需求 | MiniExcel + EPPlus 組合 | 模板分離 + 數(shù)據(jù)流式填充 |
| 超大數(shù)據(jù)量 (千萬級) | 分片寫入 + 并行處理 | 多 Task 分片 + 最終合并 |
三、性能對比數(shù)據(jù)
| 測試項(xiàng) | EPPlus | MiniExcel | NPOI |
|---|---|---|---|
| 100萬行寫入時(shí)間 | 42s | 18s | 65s |
| 內(nèi)存峰值 | 1.1GB | 190MB | 820MB |
| 文件大小 | 86MB | 68MB | 105MB |
| GC 暫停時(shí)間 | 1.4s | 0.2s | 2.1s |
| 線程資源占用 | 高 | 低 | 中 |
四、核心代碼實(shí)現(xiàn)
1. MiniExcel 流式寫入(推薦方案)
// 配置優(yōu)化參數(shù)
var config = new OpenXmlConfiguration
{
EnableSharedStrings = false, // 關(guān)閉共享字符串表
AutoFilterMode = AutoFilterMode.None, // 禁用自動(dòng)篩選
FillMergedCells = false // 不處理合并單元格
};
// 分頁流式寫入
await MiniExcel.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()) yield break;
foreach (var item in data)
{
yield return new Dictionary<string, object>
{
["ID"] = item.Id,
["Name"] = item.Name,
["CreateTime"] = item.CreateTime.ToString("yyyy-MM-dd")
};
}
}
}優(yōu)化點(diǎn):
- 分頁加載數(shù)據(jù)庫數(shù)據(jù)
- 延遲加載數(shù)據(jù)生成器
- 關(guān)閉非必要功能
2. EPPlus 混合寫入方案
using (var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("Data");
int row = 1;
// 批量寫入頭信息
sheet.Cells["A1:C1"].LoadFromArrays(new[] { new[] { "ID", "Name", "CreateTime" } });
// 分塊寫入(每50000行保存一次)
foreach (var chunk in GetDataChunks(50000))
{
sheet.Cells[row+1, 1].LoadFromCollection(chunk);
row += chunk.Count;
if (row % 50000 == 0)
{
package.Save(); // 分段保存
sheet.Cells.ClearFormulas();
}
}
package.SaveAs(new FileInfo("output_epplus.xlsx"));
}3. 性能對比測試代碼
[BenchmarkDotNet.Attributes.SimpleJob]
public class ExcelBenchmarks
{
private List<DataModel> _testData = GenerateTestData(1_000_000);
[Benchmark]
public void MiniExcelExport() => MiniExcel.SaveAs("mini.xlsx", _testData);
[Benchmark]
public void EPPlusExport()
{
using var pkg = new ExcelPackage();
var sheet = pkg.Workbook.Worksheets.Add("Data");
sheet.Cells.LoadFromCollection(_testData);
pkg.SaveAs("epplus.xlsx");
}
[Benchmark]
public void NPOIExport()
{
var workbook = new XSSFWorkbook();
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);
}
using var fs = new FileStream("npoi.xlsx", FileMode.Create);
workbook.Write(fs);
}
}五、混合方案實(shí)現(xiàn)
1. EPPlus + MiniExcel 組合方案
// 先用 EPPlus 創(chuàng)建帶樣式的模板
using (var pkg = new ExcelPackage(new FileInfo("template.xlsx")))
{
var sheet = pkg.Workbook.Worksheets[0];
sheet.Cells["A1"].Value = "動(dòng)態(tài)報(bào)表";
pkg.Save();
}
// 用 MiniExcel 填充大數(shù)據(jù)量
var data = GetBigData();
MiniExcel.SaveAsByTemplate("output.xlsx", "template.xlsx", data);2. 分片異步導(dǎo)出方案
public async Task ExportShardedDataAsync()
{
var totalRecords = 5_000_000;
var shardSize = 100_000;
var shards = totalRecords / shardSize;
var tasks = new List<Task>();
for (int i = 0; i < shards; i++)
{
var start = i * shardSize;
tasks.Add(Task.Run(async () =>
{
using var stream = new FileStream($"shard_{i}.xlsx", FileMode.Create);
await MiniExcel.SaveAsAsync(stream, QueryData(start, shardSize));
}));
}
await Task.WhenAll(tasks);
MergeShardFiles(shards);
}
private void MergeShardFiles(int shardCount)
{
using var merger = new ExcelPackage();
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(new FileInfo("final.xlsx"));
}六、高級優(yōu)化策略
1. 內(nèi)存管理配置
// Program.cs 全局配置
AppContext.SetSwitch("System.Buffers.ArrayPool.UseShared", true); // 啟用共享數(shù)組池
// 運(yùn)行時(shí)配置(runtimeconfig.template.json)
{
"configProperties": {
"System.GC.HeapHardLimit": "0x100000000", // 4GB 內(nèi)存限制
"System.GC.HeapHardLimitPercent": "70",
"System.GC.Server": true
}
}2. 數(shù)據(jù)庫優(yōu)化
// Dapper 分頁優(yōu)化
public IEnumerable<DataModel> GetPagedData(long checkpoint, int size)
{
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)", // 強(qiáng)制重新編譯執(zhí)行計(jì)劃
new { checkpoint, size });
}3. 異常處理增強(qiáng)
try
{
await ExportDataAsync();
}
catch (MiniExcelException ex) when (ex.ErrorCode == "DISK_FULL")
{
await CleanTempFilesAsync();
await RetryExportAsync();
}
catch (SqlException ex) when (ex.Number == 1205) // 死鎖重試
{
await Task.Delay(1000);
await RetryExportAsync();
}
finally
{
_semaphore.Release(); // 釋放信號量
}七、最佳實(shí)踐總結(jié)
1、數(shù)據(jù)分頁策略
使用有序 ID 分頁避免 OFFSET 性能衰減
// 優(yōu)化分頁查詢
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、內(nèi)存控制三位一體
- 啟用服務(wù)器 GC 模式
- 配置共享數(shù)組池
- 使用對象池復(fù)用 DTO
3、異常處理金字塔
try {
// 核心邏輯
}
catch (IOException ex) when (ex.Message.Contains("磁盤空間")) {
// 磁盤異常處理
}
catch (SqlException ex) when (ex.Number == 1205) {
// 數(shù)據(jù)庫死鎖處理
}
catch (Exception ex) {
// 通用異常處理
}八、避坑指南
常見陷阱
EPPlus的內(nèi)存泄漏
// 錯(cuò)誤示例:未釋放ExcelPackage
var pkg = new ExcelPackage(); // 必須包裹在using中
pkg.SaveAs("leak.xlsx");
// 正確用法
using (var pkg = new ExcelPackage())
{
// 操作代碼
}NPOI的文件鎖定
// 錯(cuò)誤示例:未正確釋放資源
var workbook = new XSSFWorkbook();
// 正確用法
using (var fs = new FileStream("data.xlsx", FileMode.Create))
{
workbook.Write(fs);
}異常處理最佳實(shí)踐
try
{
await ExportAsync();
}
catch (MiniExcelException ex) when (ex.ErrorCode == "DISK_FULL")
{
_logger.LogError("磁盤空間不足: {Message}", ex.Message);
await CleanTempFilesAsync();
throw new UserFriendlyException("導(dǎo)出失敗,請聯(lián)系管理員");
}
catch (DbException ex) when (ex.IsTransient)
{
_logger.LogWarning("數(shù)據(jù)庫暫時(shí)性錯(cuò)誤,嘗試重試");
await Task.Delay(1000);
await RetryExportAsync();
}
finally
{
_exportSemaphore.Release();
}九、典型場景建議
- 金融報(bào)表 → EPPlus(復(fù)雜公式+圖表)
- 日志導(dǎo)出 → MiniExcel(千萬級流式處理)
- 舊系統(tǒng)遷移 → NPOI(xls兼容)
- 動(dòng)態(tài)模板 → MiniExcel模板引擎
通過合理的方案選擇和優(yōu)化配置,可實(shí)現(xiàn):
- 內(nèi)存消耗降低 80% 以上
- 導(dǎo)出速度提升 3-5 倍
- 系統(tǒng)穩(wěn)定性顯著增強(qiáng)
到此這篇關(guān)于C#實(shí)現(xiàn)高性能Excel百萬數(shù)據(jù)導(dǎo)出優(yōu)化實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)C# Excel數(shù)據(jù)導(dǎo)出內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- C#使用MiniExcel實(shí)現(xiàn)導(dǎo)入導(dǎo)出數(shù)據(jù)到Excel/CSV文件
- C#操作NPOI實(shí)現(xiàn)Excel數(shù)據(jù)導(dǎo)入導(dǎo)出
- C#使用NPOI對Excel數(shù)據(jù)進(jìn)行導(dǎo)入導(dǎo)出
- C#通過NPOI導(dǎo)入導(dǎo)出數(shù)據(jù)EXCEL
- C#導(dǎo)出數(shù)據(jù)到excel如何提升性能
- C#導(dǎo)入導(dǎo)出Excel數(shù)據(jù)的兩種方法
- C#實(shí)現(xiàn)幾十萬級數(shù)據(jù)導(dǎo)出Excel及Excel各種操作實(shí)例
相關(guān)文章
C#實(shí)現(xiàn)任意數(shù)據(jù)類型轉(zhuǎn)成json格式輸出
C#實(shí)現(xiàn)任意數(shù)據(jù)類型轉(zhuǎn)成json格式輸出。需要的朋友可以過來參考下,希望對大家有所幫助2013-10-10
深入了解C#設(shè)計(jì)模式之訂閱發(fā)布模式
這篇文章主要介紹了C#設(shè)計(jì)模式之訂閱發(fā)布模式的的相關(guān)資料,文中示例代碼非常詳細(xì),供大家參考和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06
C#使用jQuery實(shí)現(xiàn)無刷新評論提交的方法
這篇文章主要介紹了C#使用jQuery實(shí)現(xiàn)無刷新評論提交的方法,涉及C#結(jié)合jQuery進(jìn)行Ajax操作的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-05-05
Unity實(shí)現(xiàn)鼠標(biāo)點(diǎn)2D轉(zhuǎn)3D進(jìn)行旋轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)鼠標(biāo)點(diǎn)2D轉(zhuǎn)3D進(jìn)行旋轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04
C#實(shí)現(xiàn)鼠標(biāo)拖拽無邊框浮動(dòng)窗體的方法
一般情況下,在標(biāo)題欄中按住鼠標(biāo)左鍵不放即可實(shí)現(xiàn)拖動(dòng)操作,當(dāng)做浮動(dòng)窗體時(shí),如果包含窗體邊框,那么界面給使用者的感覺將很不友好,因此本文給大家介紹了C#實(shí)現(xiàn)鼠標(biāo)拖拽無邊框浮動(dòng)窗體的方法,感興趣的朋友可以參考下2024-04-04

