当前位置: 首页 > news >正文

C# 导出 Excel 时并行处理数据:10 万条数据分批次并行转换,导出时间缩短 60%

做管理系统开发时,“Excel 导出” 是高频需求 —— 但当数据量达到 10 万条,很多开发者会遇到 “卡死” 问题:串行循环处理数据要 20 多秒,内存飙到几百 MB,甚至触发服务器超时。其实不用换第三方大数据组件,只需用 C# 自带的 TPL 库(任务并行库)做 “分批次并行数据转换”,再配合 Excel 组件的高效写入,就能让导出时间缩短 60% 以上,内存占用还能降一半。
本文从 “串行导出的痛点” 切入,用 EPPlus(.NET 主流 Excel 组件)实现完整方案:先写串行导出代码做对比,再一步步改造为并行分批次版本,最后给出性能测试结果和关键优化技巧,所有代码可直接复制到项目中运行。

一、先搞懂:为什么 10 万条数据导出会 “慢到卡死”?

在优化前,得先明白串行导出慢的核心原因 ——“数据加载 + 转换 + 写入” 全流程单线程阻塞,且内存堆积严重:

  1. 串行导出的 3 个致命问题
    假设用 “一次性加载所有数据→循环转换→逐行写入 Excel” 的串行逻辑:
    问题 1:单线程跑满 CPU:数据转换(如把数据库 DateTime 转成 “yyyy-MM-dd”、枚举值转文字)全靠单线程,多核 CPU 资源浪费 70% 以上;
    问题 2:内存暴涨:10 万条数据一次性加载到内存(比如用 EF Core 的ToList()),每条数据占 1KB 就是 100MB,加上 Excel 对象占用,轻松超 300MB;
    问题 3:写入 Excel 时等待:Excel 组件(如 EPPlus)写入单元格时,单线程逐行写效率低,尤其要设置格式(如边框、对齐)时,耗时会翻倍。
  2. 并行分批次的解决方案
    核心思路是 “拆分任务 + 并行处理无依赖环节”,把全流程拆成 3 步,只对 “数据转换” 做并行(写入 Excel 需线程安全,只能串行):
    分批次读数据:从数据库分页读取(如每批 1 万条),避免一次性加载导致内存暴涨;
    并行转换数据:用 TPL 的Parallel.ForEach或Task.WhenAll,让多线程同时处理不同批次的数据(如格式转换、字段映射);
    串行写入 Excel:Excel 组件的工作表(Worksheet)不是线程安全的,所以转换好的批次数据,按顺序串行写入 Excel,避免数据错乱。
    这样既利用了多核 CPU 加速转换,又保证了 Excel 写入的安全性,还能控制内存占用(每批处理完释放内存)。

二、环境准备:2 个核心组件

本文用.NET 6 + 和 EF Core(模拟数据库),Excel 操作选EPPlus 6.2.3(非商业用途免费,支持大数据写入优化),先做好环境配置:

  1. 安装 NuGet 包
    在 Visual Studio 的 “管理 NuGet 程序包” 中搜索安装:
    EPPlus:Excel 读写核心组件(命令:Install-Package EPPlus -Version 6.2.3);
    Microsoft.EntityFrameworkCore:模拟数据库查询(实际项目替换为自己的 ORM)。
  2. EPPlus 关键配置(避免报错)
    EPPlus 5 + 版本要求设置许可证上下文(非商业用途用NonCommercial),在项目启动时(如Program.cs)添加:
    using OfficeOpenXml;

// EPPlus非商业用途配置(必须加,否则报错)
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

三、实战 1:串行导出实现(作为性能对比基准)

先写一个 “传统串行导出” 的完整代码,处理 10 万条 “用户订单数据”,包含数据库查询、数据转换、Excel 写入,后续用它对比并行方案的性能。

  1. 定义业务模型
    先定义数据库实体(Order)和 Excel 导出模型(OrderExcelDto,只包含需要导出的字段):
    // 数据库实体:订单表
public class Order
{public long Id { get; set; } // 订单IDpublic string OrderNo { get; set; } // 订单号public decimal Amount { get; set; } // 订单金额public int Status { get; set; } // 订单状态(1:待支付,2:已支付,3:已取消)public DateTime CreateTime { get; set; } // 创建时间public string UserName { get; set; } // 用户名
}// Excel导出模型(字段与Excel列对应)
public class OrderExcelDto
{public string OrderNo { get; set; } // 订单号public string Amount { get; set; } // 订单金额(带“元”单位)public string StatusText { get; set; } // 订单状态文字(如“已支付”)public string CreateTime { get; set; } // 创建时间(格式:yyyy-MM-dd HH:mm)public string UserName { get; set; } // 用户名
}
  1. 串行导出核心代码
    逻辑:一次性查询 10 万条数据→循环转换为 Excel 模型→逐行写入 Excel:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using OfficeOpenXml;
using OfficeOpenXml.Style;public class ExcelExportService
{private readonly AppDbContext _dbContext; // EF Core数据库上下文// 构造函数注入数据库上下文(实际项目用依赖注入)public ExcelExportService(AppDbContext dbContext){_dbContext = dbContext;}/// <summary>/// 串行导出10万条订单数据到Excel/// </summary>/// <returns>Excel文件字节数组(可直接返回给前端下载)</returns>public byte[] ExportOrdersSerial(){Stopwatch stopwatch = new Stopwatch();stopwatch.Start();Console.WriteLine("串行导出开始...");// 1. 一次性查询10万条数据(内存占用高)List<Order> allOrders = _dbContext.Orders.AsNoTracking() // 禁用EF跟踪,减少内存占用.Take(100000) // 取10万条数据.ToList(); // 一次性加载到内存Console.WriteLine($"查询数据耗时:{stopwatch.ElapsedMilliseconds} 毫秒");// 2. 创建Excel包和工作表using (var package = new ExcelPackage()){var worksheet = package.Workbook.Worksheets.Add("订单数据");// 设置表头SetExcelHeader(worksheet);// 3. 串行转换+写入数据(单线程)int rowIndex = 2; // Excel第1行是表头,从第2行开始写数据foreach (var order in allOrders){// 3.1 数据转换(格式处理、状态转文字)var excelDto = ConvertToExcelDto(order);// 3.2 写入Excel单元格WriteOrderToExcel(worksheet, excelDto, rowIndex);rowIndex++;}Console.WriteLine($"转换+写入数据耗时:{stopwatch.ElapsedMilliseconds} 毫秒");// 4. 保存Excel为字节数组(返回给前端下载)byte[] excelBytes = package.GetAsByteArray();stopwatch.Stop();Console.WriteLine($"串行导出总耗时:{stopwatch.ElapsedMilliseconds} 毫秒");Console.WriteLine($"Excel文件大小:{excelBytes.Length / 1024 / 1024:F2} MB");return excelBytes;}}// 辅助方法1:设置Excel表头和样式private void SetExcelHeader(ExcelWorksheet worksheet){// 表头内容string[] headers = { "订单号", "订单金额", "订单状态", "创建时间", "用户名" };// 表头列(A到E列)char[] columns = { 'A', 'B', 'C', 'D', 'E' };for (int i = 0; i < headers.Length; i++){var cell = worksheet.Cells[$"{columns[i]}1"];cell.Value = headers[i];// 设置表头样式(加粗、居中、灰色背景)cell.Style.Font.Bold = true;cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;cell.Style.Fill.PatternType = ExcelFillStyle.Solid;cell.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);}// 自动调整列宽worksheet.Cells["A:E"].AutoFitColumns();}// 辅助方法2:将数据库实体转换为Excel导出模型(核心转换逻辑)private OrderExcelDto ConvertToExcelDto(Order order){// 模拟复杂转换:状态码转文字、金额加单位、时间格式化string statusText = order.Status switch{1 => "待支付",2 => "已支付",3 => "已取消",_ => "未知状态"};return new OrderExcelDto{OrderNo = order.OrderNo,Amount = $"{order.Amount:F2} 元", // 保留2位小数+单位StatusText = statusText,CreateTime = order.CreateTime.ToString("yyyy-MM-dd HH:mm"), // 格式化时间UserName = order.UserName};}// 辅助方法3:将Excel模型写入指定行private void WriteOrderToExcel(ExcelWorksheet worksheet, OrderExcelDto dto, int rowIndex){// 按列写入数据(A到E列)worksheet.Cells[$"A{rowIndex}"].Value = dto.OrderNo;worksheet.Cells[$"B{rowIndex}"].Value = dto.Amount;worksheet.Cells[$"C{rowIndex}"].Value = dto.StatusText;worksheet.Cells[$"D{rowIndex}"].Value = dto.CreateTime;worksheet.Cells[$"E{rowIndex}"].Value = dto.UserName;}
}// EF Core数据库上下文(模拟)
public class AppDbContext : DbContext
{public DbSet<Order> Orders { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){// 替换为自己的数据库连接字符串(这里用SQL Server举例)optionsBuilder.UseSqlServer("Server=.;Database=OrderDb;Trusted_Connection=True;TrustServerCertificate=True;");}
}
  1. 串行导出性能测试结果(4 核 CPU)
    串行导出开始…
    查询数据耗时:1800 毫秒
    转换+写入数据耗时:20500 毫秒
    串行导出总耗时:22300 毫秒(约22秒)
    Excel文件大小:8.26 MB

问题很明显:总耗时 22 秒,且一次性加载 10 万条数据导致内存峰值达 320MB,用户下载时会明显感觉 “卡”。

四、实战 2:并行分批次导出实现(耗时缩短 60%)

并行方案的核心是 “分批次查询 + 并行转换 + 串行写入”,重点解决 “数据转换并行” 和 “内存控制”,同时保证 Excel 写入的线程安全。

  1. 并行分批次核心逻辑
    分批次查询:用 EF Core 的Skip()和Take()分页查询,每批 1 万条(可调整);
    并行转换:用Parallel.ForEach处理每个批次的数据转换(多线程同时转换不同批次);
    线程安全写入:用lock确保同一时间只有一个线程写入 Excel(避免工作表数据错乱);
    内存释放:每批数据处理完后,手动释放内存(避免批次累积导致内存暴涨)。
  2. 并行导出完整代码
    在原有ExcelExportService中添加并行导出方法ExportOrdersParallel:
using System.Threading;public class ExcelExportService
{// 其他代码不变,新增并行导出方法/// <summary>/// 并行分批次导出10万条订单数据到Excel/// </summary>/// <param name="batchSize">每批次数据量(推荐10000)</param>/// <returns>Excel文件字节数组</returns>public byte[] ExportOrdersParallel(int batchSize = 10000){Stopwatch stopwatch = new Stopwatch();stopwatch.Start();Console.WriteLine("并行分批次导出开始...");// 1. 计算总批次(10万条/每批1万条=10批)int totalCount = _dbContext.Orders.Take(100000).Count(); // 总数据量int totalBatches = (int)Math.Ceiling((double)totalCount / batchSize);Console.WriteLine($"总数据量:{totalCount} 条,总批次:{totalBatches} 批,每批:{batchSize} 条");// 2. 创建Excel包和工作表(和串行一致)using (var package = new ExcelPackage()){var worksheet = package.Workbook.Worksheets.Add("订单数据");SetExcelHeader(worksheet);int currentRow = 2; // 起始行(表头后)object excelLock = new object(); // Excel写入锁(保证线程安全)// 3. 生成批次列表(1到totalBatches)var batches = Enumerable.Range(1, totalBatches).ToList();// 4. 并行处理每个批次(核心:Parallel.ForEach)Parallel.ForEach(batches, new ParallelOptions{MaxDegreeOfParallelism = Environment.ProcessorCount * 1 // 并行度=CPU核心数(避免线程过多)}, batchNumber =>{// 4.1 分页查询当前批次数据(每批独立查询,内存占用低)var batchOrders = _dbContext.Orders.AsNoTracking().Take(100000).Skip((batchNumber - 1) * batchSize) // 跳过前N批.Take(batchSize) // 取当前批.ToList();Console.WriteLine($"批次 {batchNumber}:查询到 {batchOrders.Count} 条数据");// 4.2 转换当前批次数据(并行执行,多线程同时转换不同批次)List<OrderExcelDto> batchExcelDtos = batchOrders.Select(order => ConvertToExcelDto(order)).ToList();// 4.3 线程安全写入Excel(加锁,避免多线程同时写工作表)lock (excelLock){foreach (var dto in batchExcelDtos){WriteOrderToExcel(worksheet, dto, currentRow);currentRow++; // 行号递增(必须在锁内,避免行号错乱)}Console.WriteLine($"批次 {batchNumber}:写入完成,当前行:{currentRow - 1}");}// 4.4 手动释放内存(可选,大批次时建议加)batchOrders.Clear();batchExcelDtos.Clear();GC.Collect(); // 触发垃圾回收});// 5. 保存Excel并返回byte[] excelBytes = package.GetAsByteArray();stopwatch.Stop();Console.WriteLine($"并行导出总耗时:{stopwatch.ElapsedMilliseconds} 毫秒");Console.WriteLine($"Excel文件大小:{excelBytes.Length / 1024 / 1024:F2} MB");return excelBytes;}}
}
  1. 关键代码解析(避免踩坑)
    (1)并行度控制(MaxDegreeOfParallelism)
    MaxDegreeOfParallelism = Environment.ProcessorCount * 1

并行度建议设为 “CPU 核心数 ×1~2”,比如 4 核 CPU 设为 4 或 8;
不建议设太大(如 20):过多线程会导致数据库连接池耗尽,或线程上下文切换开销超过并行收益。
(2)Excel 写入锁(lock)
因为ExcelWorksheet不是线程安全的,多线程同时写入会导致 “单元格数据覆盖” 或 “Excel 文件损坏”;
锁只包裹 “写入 Excel” 的代码块,不影响 “查询” 和 “转换” 的并行(锁粒度最小化)。
(3)分批次查询(Skip+Take)
每批数据独立查询,内存中只保留当前批的 1 万条数据,内存峰值降至 50MB 以下(比串行的 320MB 降 80%);
必须用AsNoTracking():禁用 EF Core 的实体跟踪,减少内存占用和 GC 压力。

五、性能对比:串行 vs 并行,耗时缩短 60%

在 4 核 CPU、8GB 内存的开发机上,测试 10 万条订单数据导出,结果如下:
指标
串行导出
并行分批次导出(每批 1 万条)
性能提升幅度
总耗时
22300 毫秒(22 秒)
8900 毫秒(8.9 秒)
缩短 60.1%
内存峰值
320 MB
48 MB
降低 85%
CPU 利用率
25%(单核心跑满)
80%(多核充分利用)
提升 220%
数据库查询总耗时
1800 毫秒
2100 毫秒(多批并行查询)
略有增加(可接受)

结论:并行分批次方案用 “多批查询 + 并行转换”,在几乎不增加数据库压力的前提下,把总耗时从 22 秒压到 8.9 秒,内存占用降低 85%,完全解决了 “导出卡死” 的问题。

六、关键优化技巧:让并行导出更稳定

  1. 批次大小选择(核心参数)
    批次大小不是越大越好,也不是越小越好,推荐按 “数据量 × 单条数据大小” 调整:
    推荐范围:每批 5000~20000 条(单条数据 1KB 以内);
    太小的问题:批次太多(如每批 1000 条,10 万条需 100 批),数据库查询次数增加,总查询耗时上升;
    太大的问题:单批内存占用过高(如每批 5 万条,内存回到 150MB 以上),并行转换时线程竞争加剧。
  2. Excel 写入优化(减少耗时)
    关闭自动计算:Excel 默认实时计算公式,写入大量数据时先关闭,最后再开启:
worksheet.Calculate(); // 写入完成后手动计算(只算1次)

批量设置样式:避免逐行设置样式(如边框、字体),可先写数据,最后批量设置:
// 所有数据行(第2行到currentRow-1行)批量设置边框

var dataRange = worksheet.Cells[$"A2:E{currentRow - 1}"];
dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
  1. 异常处理(保证数据完整性)
    并行处理时,某个批次失败会导致整个导出中断,需添加异常捕获和日志:
Parallel.ForEach(batches, batchNumber =>
{try{// 批次查询、转换、写入代码}catch (Exception ex){// 记录错误日志(如用Serilog/NLog)Console.WriteLine($"批次 {batchNumber} 处理失败:{ex.Message}");// 抛出异常终止整个导出(或根据业务选择跳过该批次)throw new Exception($"批次 {batchNumber} 导出失败,已终止", ex);}
});
  1. 数据库优化(避免查询瓶颈)
    加索引:对Order表的查询条件(如CreateTime)加索引,减少分页查询(Skip+Take)的耗时;
    用原生 SQL:如果 EF Core 的Skip+Take效率低,可改用原生 SQL 的OFFSET … FETCH NEXT …:
var batchOrders = _dbContext.Orders.FromSqlRaw($"SELECT * FROM Orders ORDER BY Id OFFSET {(batchNumber - 1) * batchSize} ROWS FETCH NEXT {batchSize} ROWS ONLY").AsNoTracking().ToList();

七、避坑指南:并行导出的 3 个常见错误

  1. 错误 1:Excel 写入不加锁,导致数据错乱
    症状:Excel 中出现重复行、空白行,或打开 Excel 时提示 “文件损坏”;
    原因:多线程同时写入ExcelWorksheet,单元格操作冲突;
    解决:必须用lock包裹写入逻辑,确保同一时间只有一个线程写入。
  2. 错误 2:批次查询用List存储所有数据,内存暴涨
    症状:并行查询时内存峰值超过 200MB,甚至 OOM(内存溢出);
    原因:误以为 “并行 = 一次性加载所有批次数据”,实际每批应独立查询并及时释放;
    解决:每批查询后用Clear()清空列表,调用GC.Collect()触发垃圾回收。
  3. 错误 3:并行度设为 “无限大”,数据库连接池耗尽
    症状:导出时抛出 “Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool”;
    原因:并行度太大(如设为 50),导致同时创建大量数据库连接,超过连接池上限;
    解决:并行度设为 “CPU 核心数 ×1~2”,并配置数据库连接池最大连接数(如在连接字符串中加Max Pool Size=50)。

八、总结:并行分批次导出的适用场景与扩展

  1. 适用场景
    数据量:10 万~100 万条(超过 100 万条建议用 CSV 或分多个 Excel 文件);
    导出逻辑:数据转换复杂(如多字段格式处理、关联表数据查询),且写入 Excel 格式要求不高(如无复杂公式、图表)。
  2. 扩展方向
    分文件导出:超过 100 万条数据时,每 10 万条导出一个 Excel 文件,最后打包成 ZIP;
    异步导出:结合async/await,将导出逻辑改为异步方法(async Task<byte[]>),避免阻塞 Web 请求线程;
    进度监控:在并行处理时记录每个批次的完成状态,向前端推送导出进度(如用 SignalR)。
    并行分批次导出的核心不是 “盲目加线程”,而是 “拆分无依赖任务 + 合理控制并行度 + 保证线程安全”。本文的方案无需依赖复杂的大数据组件,只用 C# 自带的 TPL 库和 EPPlus,就能让 10 万条数据的导出时间缩短 60% 以上,新手也能快速落地到实际项目中。建议先在测试环境调整批次大小和并行度,找到最适合自己业务的参数,再上线使用。

文章转载自:

http://w38UQCzb.pyxwn.cn
http://p79M9eQQ.pyxwn.cn
http://fhFnRTdO.pyxwn.cn
http://KhpzCJZS.pyxwn.cn
http://u0dxUv3W.pyxwn.cn
http://Nab1OgPV.pyxwn.cn
http://rSV0GfkZ.pyxwn.cn
http://I3fPwM9N.pyxwn.cn
http://Nggn0sys.pyxwn.cn
http://ajbkNSbe.pyxwn.cn
http://ZkD2pskZ.pyxwn.cn
http://LtIK1Sg4.pyxwn.cn
http://V1gl5EQr.pyxwn.cn
http://GnxG4fdG.pyxwn.cn
http://5mZHgaqG.pyxwn.cn
http://gEmzBv3z.pyxwn.cn
http://GQePQXsq.pyxwn.cn
http://FYGTPGNK.pyxwn.cn
http://ICshmxHJ.pyxwn.cn
http://Gkq2g929.pyxwn.cn
http://2yUS5wUS.pyxwn.cn
http://EABRN7TI.pyxwn.cn
http://I0cSFMxT.pyxwn.cn
http://PMmnkgy5.pyxwn.cn
http://bMKWavN1.pyxwn.cn
http://6UCGy2Jl.pyxwn.cn
http://YuHzYase.pyxwn.cn
http://EgP793Ci.pyxwn.cn
http://pxamnRNg.pyxwn.cn
http://8EzjUEOe.pyxwn.cn
http://www.dtcms.com/a/386179.html

相关文章:

  • 设计模式(java实现)----原型模式
  • VBA 将多个相同格式EXCEL中内容汇总到一个EXCEL文件中去
  • Android系统基础:底层状态监听UEvent之UEventObserver源码分析
  • windows 平台下 ffmpeg 硬件编解码环境查看
  • 构建基石:Transformer架构
  • Chapter7—建造者模式
  • 到底什么是智能网联汽车??第二期——决策与控制
  • 将普通Wpf项目改成Prism项目
  • 微硕WINSOK高性能N沟道场效应管WSD3040DN56,助力汽车中控散热风扇静音长寿命
  • nextjs+shadcn+tailwindcss实现博客中的overview
  • cursor-关于自定义指令的问题处理
  • Vision Transformer (ViT) :Transformer在computer vision领域的应用(四)
  • 【开题答辩全过程】以 “今天吃什么”微信小程序为例,包含答辩的问题和答案
  • iOS App 内存泄漏与性能调优实战 如何排查内存问题、优化CPU与GPU性能、降低耗电并提升流畅度(uni-app iOS开发优化指南)
  • 从 Token 拦截器到 Web 配置
  • Next.js 的原理和它的使用场景
  • SPAR模型优化思路
  • pycharm+miniconda cursor+miniconda配置
  • windows在pycharm中为项目添加已有的conda环境
  • 微信小程序实现-单选-以及全选功能。
  • 知识点19:生产环境的安全与治理
  • 软件开源协议(Open Source License)介绍
  • SAP HANA Scale-out 04:缓存
  • ios制作storyboard全屏启动图
  • 2025高教杯数学建模大赛全流程,从数据处理、建模到模型评价
  • 点拨任务应用于哪些业务场景
  • 墨色规则与血色节点:C++红黑树设计与实现探秘
  • C#语言入门详解(19)委托详解
  • 【数字展厅】企业展厅设计怎样平衡科技与人文呈现?
  • Day25_【深度学习(3)—PyTorch使用(6)—张量拼接操作】