65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 的开源存储引擎 TmStorage

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2014 年 11 月 21 日

CPOL

7分钟阅读

viewsIcon

31844

downloadIcon

788

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 日:初始版本
© . All rights reserved.