基于C#和EF Core高效读取.txt文件并存储到SQL数据库
大文本文件导入EF Core的内存优化方案
针对你处理500-800万行固定格式文本文件导入SQL数据库时内存占用过高的问题,以下是具体的优化建议和代码调整方案:
核心问题分析
当前代码的主要内存开销来源:
- 所有实体属性均存储为字符串,即使数字类型也不例外,字符串的内存占用远高于值类型
- EF Core上下文长期跟踪批量导入的实体,导致内存无法及时释放
- 批量大小设置过大(10万条),单次内存峰值过高
BatchSizeCounter类存在逻辑错误(Decrement方法实际是递增计数)
具体优化方案
1. 优化实体属性类型
将数字类型的属性从string改为对应的值类型(int/long/short等),这会大幅降低单条记录的内存占用。
修改BAS实体示例:
public class BAS { // 数字类型改为对应值类型 public int Prop1 { get; set; } public int Prop2 { get; set; } public short Prop3 { get; set; } public long Prop4 { get; set; } public byte Prop5 { get; set; } // ... 其余数字属性按实际类型调整 // 文本类型保留string public string Prop11 { get; set; } public string Prop27 { get; set; } public string Prop29 { get; set; } // ... 其余文本属性 }
2. 优化字符串解析逻辑
利用Span直接解析值类型,避免创建不必要的字符串,减少内存分配。
修改ParseRecord方法:
private static BAS ParseRecord(string line) { var span = line.AsSpan(); var record = new BAS(); // 数字属性直接用Span解析为值类型,无需ToString int.TryParse(span.Slice(0, 3), out record.Prop1); int.TryParse(span.Slice(3, 3), out record.Prop2); short.TryParse(span.Slice(6, 2), out record.Prop3); long.TryParse(span.Slice(8, 6), out record.Prop4); byte.TryParse(span.Slice(14, 1), out record.Prop5); // ... 其余数字属性同理 // 文本属性按需转换为字符串 record.Prop11 = span.Slice(26, 1).ToString(); record.Prop27 = span.Slice(81, 10).ToString(); record.Prop29 = span.Slice(99, 10).ToString(); // ... 其余文本属性 return record; }
3. 修复BatchSizeCounter逻辑错误
修正方法命名和计数逻辑:
public class BatchSizeCounter { private int _count; private readonly int _batchSize; public BatchSizeCounter(int batchSize) { _batchSize = batchSize; } public bool IsBatchComplete => _count >= _batchSize; // 方法名改为Increment,符合计数逻辑 public void Increment() { _count++; } public void Reset() { _count = 0; } }
在ProcessFileAsync中同步修改调用:
records.Add(record); batchSize.Increment(); // 替换原Decrement调用
4. 控制EF Core上下文内存占用
每次批量插入后,清除上下文的实体跟踪,或者使用短生命周期的临时上下文,避免内存积累:
方案A:清除上下文跟踪
private async Task ProcessBatchAsync(List<BAS> records) { await _context.BAS.AddRangeAsync(records); await _context.SaveChangesAsync(); // 清除已跟踪的实体,释放内存 _context.ChangeTracker.Clear(); }
方案B:使用临时上下文(更推荐)
private async Task ProcessBatchAsync(List<BAS> records) { // 创建临时上下文,用完即释放 using var tempContext = new YourDbContext(); // 替换为你的DbContext构造方式 await tempContext.BAS.AddRangeAsync(records); await tempContext.SaveChangesAsync(); }
5. 调整批量大小
将批量大小从10万降低到2万-5万,减少单次内存峰值:
private const int BatchSize = 20_000;
6. 并行处理优化:生产者-消费者模式
通过Channel实现解析和插入的并行执行,提升处理效率的同时平衡内存占用:
public async Task ProcessFileAsync(string filePath) { // 限制通道容量,避免生产者过快导致内存暴涨 var channel = Channel.CreateBounded<BAS>(new BoundedChannelOptions(10_000) { FullMode = BoundedChannelFullMode.Wait }); // 生产者:读取文件并解析记录 var producer = Task.Run(async () => { using var reader = new StreamReader(filePath); string line; while ((line = await reader.ReadLineAsync()) != null) { var record = ParseRecord(line); await channel.Writer.WriteAsync(record); } channel.Writer.Complete(); }); // 消费者:批量插入数据库 var consumer = Task.Run(async () => { var batch = new List<BAS>(BatchSize); await foreach (var record in channel.Reader.ReadAllAsync()) { batch.Add(record); if (batch.Count >= BatchSize) { await ProcessBatchAsync(batch); batch.Clear(); } } // 处理剩余记录 if (batch.Any()) { await ProcessBatchAsync(batch); } }); await Task.WhenAll(producer, consumer); }
7. 第三方批量插入扩展(可选)
使用EFCore.BulkExtensions库替代原生AddRangeAsync,该库支持高效批量插入,内存占用更低:
private async Task ProcessBatchAsync(List<BAS> records) { using var tempContext = new YourDbContext(); await tempContext.BulkInsertAsync(records); }
优化效果预期
- 实体属性类型优化可减少约60%-70%的单条记录内存占用
- 上下文跟踪控制可避免内存持续积累
- 生产者-消费者模式可提升整体处理速度,同时控制内存峰值
内容的提问来源于stack exchange,提问作者Andrés Quesada Cascante
相关产品推荐
相关产品推荐

