大文件排序需分而治之:按字节阈值(如1.2GB)流式切块并在换行符处截断;内存中用指定比较器排序;归并时用PriorityQueue控制多路读取,配合临时文件异常防护机制。

大文件排序不能靠一次性加载进内存硬排,核心是“分而治之”:先切块、再内存排序、最后归并。用集合作为缓冲区,本质是把 List、Queue 或 PriorityQueue 当作临时中转站,控制内存占用、避免 OOM,同时提升归并效率。关键不在集合类型本身,而在它怎么参与读、排、写、合四个环节。
按字节切块,别按行数硬分
20GB 日志里一行可能 1KB,也可能 100 字节——按固定行数切(如每 10 万行)会导致某些块远超内存限额。正确做法是估算可用内存(比如 1.5GB),预留 20% 给运行时开销,按字节阈值(如 1.2GB)流式读取,并在换行符 \n 处安全截断,确保不把一行劈成两半。可用 StreamReader.BaseStream.Read() 配合累计计数,比 ReadLine() 更可控。
排序阶段:集合只做暂存,比较逻辑要明确
读满一块后,数据进 List<string> 或 string[],但排序不能依赖默认 Sort()——它按 Unicode 码点比,中文、数字、符号顺序常错乱。应显式指定比较器:
- 纯 ASCII 时间戳前缀(如 2026-05-09T12:34:56)→ 用 StringComparer.Ordinal
- 含大小写混合的文件名(如 file10.txt 应排在 file2.txt 后)→ 用 StringComparer.OrdinalIgnoreCase 或自定义 IComparer<string>
- 数值型字段(如日志中的响应时间毫秒)→ 先 int.TryParse() 提取再比,别字符串直比
归并阶段:用 PriorityQueue 控制多路读取
假设有 300 个已排序的临时块(chunk_001.tmp … chunk_300.tmp),传统方式为每个开一个 StreamReader,极易句柄耗尽或 GC 崩溃。改用 PriorityQueue<(string line, int fileId), string>:
- 初始化时,从每个块读首行,入队(优先级按 line 值)
- 每次 Dequeue() 取最小行,写入输出流
- 再从该行来源的块读下一行,重新入队
- 缓冲区满时批量刷盘,而非逐行 Write
这样最多只维持 300 个 StreamReader 实例,但活跃读取的只有队列大小(通常 1~2 行/块),内存峰值稳定可控。
临时文件与异常防护必须到位
所有中间文件不是随便命名写完就完事:
- 用 Path.GetTempFileName() 创建,系统自动保证唯一性和权限安全
- 每个临时块写完立刻 Flush() + Close(),防止崩溃时残留损坏文件
- 归并失败时,主动遍历并删除所有 *.tmp 文件,避免磁盘被占满
- 原始编码(如 UTF-8 BOM、GBK)需全程保持一致,否则归并时乱码直接导致排序错位
不复杂但容易忽略

















