.NET 的开源存储引擎 TmStorage





5.00/5 (12投票s)
TmStorage 是一个无结构的虚拟文件系统,可以基于它构建复杂的存储或数据库。
引言
一段时间以来,我一直想编写一种文件系统,它能将许多不同类型的信息存储在单个文件中。我还希望它是一个具有扁平结构的通用文件系统。后来,我决定使用“存储”这个名字,而不是“文件系统”。
我将 TmStorage
添加到 NoSQL 类别中,因为尽管它是一个文件系统(并且可以这样使用),但它也旨在作为基础,在其之上构建自定义存储或数据库。通过构建自定义存储或数据库,可以根据要存储的数据结构来调整其结构。它还获得了 TmStorage
中已实现的所有功能(事务、缓存、快照以及未来更多功能)。
考虑到这一点,TmStorage
的设计目的是:
- 在单个文件中存储大量数据流(数百万个)。
TmStorage
会在流长度增加时自动分配空间,在流长度减少时释放空间。 - 支持完整事务,因此更改/创建/删除一个或多个流是一个原子操作。
- 通过 GUID 引用流,而不是通过名称 - 这样做的原因稍后会提到。
- 内部使用 64 位整数,因此限制非常高。
以上所有点都已完全实现。
TmStorage
包含创建、打开和删除流的方法。创建或打开流时,TmStorage
会返回一个派生自标准 .NET Stream
类的流,因此使用起来非常简单。
这是一个如何将图像保存在存储流中的示例。在存储构造函数中,会指定主文件和事务日志文件。
Image image = Image.FromFile("c:\\image.png");
Storage storage = new Storage("c:\\images.storage", "c:\\images.storagelog");
Guid streamId = Guid.NewGuid();
storage.StartTransaction();
try
{
Stream stream = storage.CreateStream(streamId);
image.Save(stream, ImageFormat.Png);
stream.Close();
storage.CommitTransaction();
}
catch
{
storage.RollbackTransaction();
}
开源项目
我将 TmStorage
作为开源项目发布,采用 MIT 许可证。任何人都可以免费用于所有目的(无论商业还是非商业)。如果有人能加入开发,我将不胜感激。
SourceForge: https://sourceforge.net/projects/tmstorage/
我最近也将其发布到了 GitHub: https://github.com/tomazk8/TmStorage
工作原理
TmStorage
使用一个主文件来存储所有流。主文件被分成可变长度的段,一个段只能属于一个流。每个流可以由零个或多个链在一起的段组成。标记空闲空间的段也链接到一个名为 free-space stream 的流中。
每个段的开头都有段元数据,并包含以下信息:
- 段的大小 (
Int64
) - 下一个段的位置(如果是最后一个则为
null
)(Int64
) - 元数据校验和 (
Int
)
为了防止高碎片化,段大小始终是块大小的倍数,块大小固定为 512 字节。
空间分配和释放
当创建一个新的空存储时,会创建一个 free-space stream,它由一个占据主文件中所有可用虚拟空间(264 字节)的段组成。主文件的大小永远不会像 free-space stream 那样大,因为 free-space stream 不包含任何数据,其大小只是虚拟的。
在为流分配或释放空间时,只能对段执行两种操作:分割或合并。当流增大时,会从 free-space stream 中获取整个段或分割段,并将其添加到流的段链中。
之前:有四个段代表空闲空间。
之后:在此空间分配中,两个段被移至新流,而第三个段被分割成两个。
当流缩小后,过程相同,只是流和 free-space stream 会被交换。
当段添加到段链中时,所有相邻的段都会合并成更大的段,以防止过度分段。
流表
所有流的元数据都存储在流表中。流表本身存储在一个具有硬编码流 ID 和硬编码第一个段位置的流中,以便在打开存储但流表尚未加载时能够找到它。
流元数据包含以下信息:
- 流 ID (
GUID
) - 流长度 (
Int64
) - 已初始化流长度 (
Int64
) - 链中第一个段的位置(空时为
null
)(Int64
) - 标签 (
Int
)
流 ID 为 GUID
类型的原因有一个。任何创建新流 ID 的人(例如使用 Guid.CreateNew()
)都可以在任何地方、任何时间独立于 TmStorage
创建它。因此,即使流 ID 在使用之前很久就被创建,您也可以确保它是唯一的。如果流 ID 是整数,则需要某种带有已存储 MaxId
值的流 ID 生成器。
已初始化长度是流包含有效数据的长度。这用于优化。当流大小增大时,新分配的空间包含随机(或剩余)数据,因为字节不会初始化为 0
。如果进行初始化,写入流将执行两次,从而降低性能。已初始化长度始终小于或等于流长度,并且在增大流时不会更改,但在写入流时会增长。读取到已初始化长度之上(直到流长度)的所有字节都会自动设置为 0
,而无需实际读取流。
标签是存储一些自定义信息的地方,例如流中数据的类型。
事务
TmStorage
支持完整事务。它们是在主文件级别实现的。在事务期间,对于主文件的每一次写入,TmStorage
会将即将被覆盖的数据复制到事务日志文件中。TmStorage
还会记录哪些部分已被备份,以避免重复操作。事务日志文件是位于主文件旁边的一个单独文件,在事务提交时会被清空。
当抛出异常时,事务可以被回滚。如果在事务期间计算机崩溃,下次打开存储时,存储会识别到事务未完成,并执行回滚。
性能与未来发展
TmStorage
目前不适合用于高性能多用户数据库,而更适合作为各种应用程序的数据存储,在这些应用程序中性能不是关键因素。
我在 TmStorage
中填充了百万个流,性能仍然出色。填充三个百万个流时仍然工作得很好。流表完全加载到内存中,因此访问速度非常快。
缺少的一个功能是缓存层。其想法是将主文件的块缓存在内存中以加快读取速度。如果存储不是太大,所有数据都可以被缓存。
我还想实现快照。其想法是能够创建当前存储的快照,这样对它的所有更改都只写入快照。最后,您可以选择是合并更改到主文件还是丢弃它们。当测试写入存储的软件时,这非常有用,这样您就不必每次开始测试时都对存储主文件进行完整备份。
可能的用法示例
使用 TmStorage
,任何人都可以创建自定义或专用存储,具有自定义数据结构,或者直接按原样使用它。
以下是一些基于 TmStorage
构建的存储/数据库示例:
分层文件系统
一个文件夹内的项目表将存储在一个流中,其中每个文件夹项都将指向一个包含子文件夹表的流,而每个文件项都将指向一个存储文件数据的流。只有包含根项的流的 ID 将是硬编码的,而其他 ID 将是生成的。表中的每个项还将存储文件/文件夹名称、创建日期等。
文档管理数据库
处理扫描文档的应用程序将为每个扫描文档首先存储文档元数据,然后将有关文档的不同信息存储在从元数据引用的各种流中。元数据将存储扫描图像的日期以及指向这些流的引用。
- 包含扫描图像的流(如果文档有多页,则可能包含多个流)
- 包含 OCR 图像的流(例如,用于全文搜索)
- 包含一种或多种语言文档翻译的流
- 包含文档(例如 Word 文档、PDF 或电子表格)的流(而不是扫描图像)
- 包含更改历史记录或旧版本元数据的流
数据库还将包含一个或多个存储索引数据的流,以便快速检索。
数据库可以使用两个 TmStorages
,一个用于当前数据,一个用于位于另一驱动器或计算机上的存档数据。
消息队列
每条消息都将存储在一个流中,并附带一个元数据,指定下一条消息存储所在流的 ID(如果是最后一条消息则为 null
)。因此,消息将构成一个链表。
文件包
如果您需要创建包含文件的包,例如用于应用程序部署,TmStorage
可以像 zip 文件一样使用。
如果您的应用程序使用大量文件(游戏中的图像、声音、纹理……),并且您不想在安装文件夹中有大量文件,则可以直接从 TmStorage
访问所有文件,而无需先将文件提取到临时文件夹。
历史
- 2014 年 11 月 21 日:初始版本