大文件索引不能用sorteddictionary/sortedset,因其内存受限、开销大且不支持持久化;应自研磁盘b-tree,按需加载页、固定字节序、预留空间防分裂,并确保二进制布局长期兼容。

为什么不用 SortedDictionary 或 SortedSet 做大文件索引
它们是内存结构,数据一多就 OOM。你读一个 50GB 日志文件,建个内存索引,光键值对就吃掉 10GB+ 内存 —— 不是慢,是根本跑不起来。SortedDictionary 的红黑树节点带指针、装箱开销大;SortedSet 同理,且不支持重复键和自定义序列化。真正的大文件索引必须落地到磁盘,靠 B-tree 的分页局部性减少 I/O。
实操建议:
- 别试图把整个 B-tree 加载进内存,按需加载节点页(比如 4KB/页),用
FileStream定位读取 - 键必须可比较且固定长度优先(如
long时间戳、intID),变长键(如string)要加前缀哈希或定长截断,否则页分裂逻辑爆炸 - 避免在叶子节点存完整记录——只存偏移量(
long)和长度(int),查时再跳转到原文件读,节省索引体积
BTreeIndex<tkey tvalue></tkey> 要自己写还是用现成库
没有靠谱的通用开源 C# B-tree 文件索引库。NuGet 上标“B-tree”的基本是内存版或玩具实现,不支持持久化、无崩溃恢复、不处理页缓存淘汰。你搜 LiteDB 或 RocksDB.NET?它们底层是 LSM 或自研结构,不是标准 B-tree,API 也不暴露索引操作细节。
实操建议:
- 从最小可行版本起步:只支持单字段
long键 +long值(即“偏移量”),用BinaryWriter/BinaryReader直接写页结构 - 每个节点页开头留 4 字节标记页类型(根/内/叶)、4 字节键数、8 字节子页地址(内节点)或 0(叶节点)
- 用
MemoryMappedFile替代反复Seek,尤其在随机查找频繁时,能省掉大量FileStream.Position设置开销
插入时页分裂导致性能断崖怎么办
典型现象:索引写到 200 万条后,单次 Insert 从 0.1ms 涨到 15ms,日志里看到连续多次 SplitNode 调用。这是因为满页分裂触发递归向上调整,最差情况一路撕到根节点,还可能引发磁盘碎片。
实操建议:
- 预留页空间:节点只填到 75%(比如 128 个键位只放 96 个),用
Array.Resize预分配但不立即填满 - 分裂时不全拷贝:内节点分裂后,把中位键提到父节点,左右两半分别写新页,旧页直接标记为“已废弃”,靠后台线程异步清理(避免阻塞写)
- 批量导入走特殊路径:先排序所有
(key, offset)对,再按 B-tree 层级顺序构造页,绕过逐条插入的分裂风暴
跨平台文件路径和字节序怎么不出错
Windows 上测试好好的索引文件,扔到 Linux 容器里一打开就读出 InvalidDataException —— 很大概率是用了 BitConverter.GetBytes(int) 写整数,而没统一用 BitConverter.IsLittleEndian 校验。B-tree 文件本质是二进制协议,必须约定死字节序。
实操建议:
- 所有数值写入一律用
BinaryWriter.Write(Int32)/Write(Int64),它内部按当前平台序,但你要在索引头写明endianness: 1 for little, 0 for big - 文件路径不硬编码
"\data.idx",用Path.Combine,且索引文件和数据文件放在同一目录下,避免相对路径解析歧义 - 首次打开索引时强制校验 magic number 和头结构 CRC32,不匹配立刻报错,别等到查第 10000 条才崩
真正难的不是写通 B-tree,是让每一页的二进制布局在十年后、换三台机器、升级两次 .NET 版本后,还能被正确解析出来。字节序、magic、页大小、键比较逻辑——这些地方一旦定死,改起来比重写还疼。









