用 Parallel.ForEach
处理大批量数据导入时为什么反而变慢?
不是所有“并发”都加速,
Parallel.ForEach在 I/O 密集型场景(如数据库插入、文件读写)中常因线程争抢连接或锁而拖慢整体吞吐。它默认按 CPU 核心数分配线程,但数据库连接池、磁盘 IO 或网络带宽才是真实瓶颈。 避免直接包裹
DbContext.SaveChanges()或
SqlCommand.ExecuteNonQuery()—— 每次调用都可能触发独立事务和连接获取 改用批量操作:如 EF Core 的
ExecuteSqlRaw+ 参数化 SQL 批量插入,或 Dapper 的
connection.Execute(sql, list)手动控制并行度:
Parallel.ForEach(list, new ParallelOptions { MaxDegreeOfParallelism = 4 }, item => { ... }),值设为数据库连接池大小(如 SQL Server 默认 100,实际建议 4–8)
如何让 SqlBulkCopy
真正跑满带宽?
SqlBulkCopy是 .NET 原生最快的数据导入方式,但默认配置下常只用单线程、小缓冲、无索引优化,导致吞吐远低于理论值。 必须设置
BatchSize(如 10000),避免单次提交过大内存溢出或过小频繁往返 开启
EnableStreaming = true,配合
DataTable或
IDataReader流式供数,减少内存峰值 导入前执行
ALTER TABLE ... DISABLE TRIGGER和
DROP INDEX(完事后重建),尤其对有唯一约束或外键的表影响巨大 确保目标表有
WITH (TABLOCK)提示(通过
SqlBulkCopy.SqlRowsCopied事件无法控制,需在 SQL 层显式加)
EF Core 批量插入时 SaveChanges
卡住的三个常见原因
EF Core 的变更跟踪机制在海量数据下会吃光内存、拖慢性能,
SaveChanges不是“越快越好”,而是“越少调用越好”。 禁用自动检测变更:
context.ChangeTracker.AutoDetectChangesEnabled = false,手动
context.Entry(entity).State = EntityState.Added分批提交:每 500–2000 条调用一次
SaveChanges(),而非全量后一次提交(否则事务日志暴涨、锁表时间过长) 不用
AddRange直接传大集合——它仍会逐个标记状态;改用
context.AddRange(entities.Take(batchSize))+ 循环
异步 + 并发组合使用时容易忽略的连接池耗尽问题
用
await context.SaveChangesAsync()配合
Task.WhenAll看似高效,实则极易触发
Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool错误。
Task.WhenAll同时发起 100 个 SaveChanges —— 就等于向连接池申请 100 个连接,远超默认上限(通常 100,但受服务器资源限制) 正确做法:用
SemaphoreSlim限流,例如
var semaphore = new SemaphoreSlim(8);<br>await semaphore.WaitAsync();<br>try { await context.SaveChangesAsync(); }<br>finally { semaphore.Release(); }
连接字符串中显式加大 Max Pool Size=200不解决问题,只是掩盖争抢——应优先降低并发请求数,再调高池大小作为补充 真正卡住海量导入的,往往不是 CPU 或代码逻辑,而是数据库连接、事务日志增长、索引维护和锁等待。先确认瓶颈在哪,再选
SqlBulkCopy、分批
SaveChanges还是纯原生 ADO.NET,比盲目加并发更有效。
