C#中低内存高效实现超大CSV文件分组多文件追加写入
低内存高效CSV分组写入方案
核心问题诊断
你内存占用过高的根本原因不是StreamWriter的缓冲区,而是同时打开的StreamWriter数量过多——每个流本身会占用固定内存,若分组数达到上千上万级,内存会直接堆涨。Flush仅清空写入缓冲区,不会释放流实例的内存。
具体解决方案
1. 限制并发打开的流数量(LRU缓存策略)
当分组数较多时,不可能长期保持所有分组的流打开。通过最近最少使用(LRU)缓存维护固定数量的StreamWriter,超过阈值就关闭最久未使用的流,需要时再重新打开追加。既控制内存占用,又避免频繁开关所有流的IO损耗。
2. 优化读取逻辑,减少内存分配
File.ReadLines会生成大量字符串,大文件处理时GC压力极大,这也是内存居高不下的隐性原因。改用FileStream配合Span<char>读取,减少不必要的字符串分配:
using var reader = new FileStream("input.csv", FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 65536, useAsync: false); var buffer = new byte[65536]; var charBuffer = new char[65536]; var decoder = Encoding.UTF8.GetDecoder(); int bytesRead; while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0) { int charsDecoded = decoder.GetChars(buffer, 0, bytesRead, charBuffer, 0); int lineStart = 0; for (int i = 0; i < charsDecoded; i++) { if (charBuffer[i] == '\n') { ProcessLine(charBuffer.AsSpan(lineStart, i - lineStart)); lineStart = i + 1; } } // 处理文件末尾未换行的剩余内容 if (lineStart < charsDecoded) { ProcessLine(charBuffer.AsSpan(lineStart, charsDecoded - lineStart)); } }
3. 实现LRU缓存管理StreamWriter
设置缓存大小(建议50-100个,可根据内存情况调整),每次获取分组Writer时:
- 缓存存在则移至最近使用位置
- 缓存不存在则打开新流;缓存满时关闭最久未使用的流再加入新流
public class StreamWriterCache : IDisposable { private readonly int _maxCacheSize; private readonly Dictionary<string, (StreamWriter Writer, LinkedListNode<string> Node)> _cache; private readonly LinkedList<string> _lruList; private readonly string _outputDir; public StreamWriterCache(int maxCacheSize, string outputDir) { _maxCacheSize = maxCacheSize; _outputDir = outputDir; _cache = new Dictionary<string, (StreamWriter, LinkedListNode<string>)>(); _lruList = new LinkedList<string>(); Directory.CreateDirectory(outputDir); } public StreamWriter GetWriter(string groupKey) { if (_cache.TryGetValue(groupKey, out var entry)) { _lruList.Remove(entry.Node); _lruList.AddFirst(entry.Node); return entry.Writer; } // 缓存满时清理最久未使用的流 while (_cache.Count >= _maxCacheSize) { var leastUsedKey = _lruList.Last.Value; _cache[leastUsedKey].Writer.Dispose(); _cache.Remove(leastUsedKey); _lruList.RemoveLast(); } // 创建带大缓冲区的StreamWriter,减少IO次数 var filePath = Path.Combine(_outputDir, $"{groupKey}.csv"); var writer = new StreamWriter(filePath, true, Encoding.UTF8, bufferSize: 65536); var node = _lruList.AddFirst(groupKey); _cache.Add(groupKey, (writer, node)); return writer; } public void Dispose() { foreach (var entry in _cache.Values) { entry.Writer.Dispose(); } _cache.Clear(); _lruList.Clear(); } }
4. 行处理与分组写入
在ProcessLine方法中解析分组标识,从缓存获取Writer写入:
private static readonly StreamWriterCache _writerCache = new StreamWriterCache(100, "./output"); private static void ProcessLine(ReadOnlySpan<char> line) { // 按CSV格式解析分组标识(示例为取第一个逗号前的内容) int separatorIdx = line.IndexOf(','); if (separatorIdx == -1) return; var groupKey = line.Slice(0, separatorIdx).ToString(); var writer = _writerCache.GetWriter(groupKey); writer.WriteLine(line.ToString()); }
额外优化建议
- 调整缓冲区大小:读取和写入缓冲区设为64KB或128KB(根据磁盘性能调整),适配大文件IO场景。
- 禁用手动Flush:保留StreamWriter默认的缓冲策略,满缓冲区再写入,减少IO次数。
- 使用同步IO:大文件处理时同步IO比异步更稳定,避免线程切换开销。
- 预分配文件空间:若预先知晓所有分组,可先创建空文件并预分配磁盘空间,减少碎片化带来的IO延迟。
- 监控GC:用性能工具查看内存分配,尽量用
Span处理文本,减少临时字符串的生成。
效果预期
该方案处理5GB文件时,内存占用可控制在500MB以内(取决于缓存大小),IO效率能满足1小时内完成的要求。若分组数极多(上万级),可适当调小缓存大小进一步降低内存占用。
内容的提问来源于stack exchange,提问作者iProgram
相关产品推荐
相关产品推荐

