复杂的 .Net 对象图的快速读写

4
我有自己用C#编写的数据结构(该结构相当复杂),需要进行序列化和反序列化。磁盘上序列化文件的大小可能会很大(接近1 GB),但也可能很小(根据存储的记录数)。我有以下要求:
  1. 序列化和反序列化应该非常快
  2. 我应该能够部分反序列化一个大文件(即仅访问一些相关记录),因为如果我从磁盘反序列化整个文件,内存使用量将会过高。
  3. 应该是线程安全的,因为多个进程可以从文件中读取/写入记录
我知道这听起来像我需要一个数据库,但由于多种原因,我无法使用数据库。我尝试通过实现ISerializable来实现要求1,这使它比使用.net内置的二进制/XML序列化器要快得多,但速度还不够快。要求2完全困扰着我。
有没有人有关于如何解决此问题的想法?我认为任何必须保存自己的大型文件格式的人都必须处理类似的问题。
谢谢, Sam

为什么不能使用数据库 - 即使是像SQLite这样的小型轻量级数据库? - Tobias Langner
你能提供一些相关的代码吗? - fretje
应用程序分析调查响应(类似于SPSS)。数据文件是响应。该应用程序在分析方面很快,因为它依赖于该数据结构。为什么我不能使用数据库:数据文件可以通过电子邮件发送,而且数据结构对应用程序的工作非常重要。简化的数据结构如下: private RcCollection<CaseVar> _data; 其中CaseVar是: private string _rId; // 响应者ID private string _vName; // 变量名称 private ArrayList _Answer; // 答案。也可以包含CaseVar - Sam
简化的结构因空间不足而混乱。下面是更好格式化的示例:这是我的容器集合,正在进行序列化。 private RcCollection<CaseVar> _data;其中 CaseVar 是: private string _rId; //受访者 ID private string _vName; //变量名称 private ArrayList _Answer; //答案。可以包含 CaseVar - Sam
跳过示例。 - Marc Gravell
4个回答

2

这是一个数据树,还是一个完整的图形 - 即是否存在循环引用?如果没有,protobuf-net 是一种高性能的二进制树序列化程序。它支持可枚举项的流式传输(因此您可以跳过记录等内容,而不是缓冲所有内容),但为了有效地查找随机元素,您可能需要某种类型的索引。

对于单个文件来说,读/写非常困难;特别是写入可能需要移动比您预期更多的磁盘...读取也很棘手,可能需要同步。使用单独的文件会更容易...


跳过早期项目的示例;我可能可以添加一个帮助器方法,但 TryDeserializeWithLengthPrefix 方法将起作用...关键是要注意,在序列化和反序列化之间,我们只创建了一个额外的对象。

using System;
using System.IO;
using System.Threading;
using ProtoBuf;

[ProtoContract]
class Foo {
    static int count;
    public static int ObjectCount { get { return count; } }
    public Foo() { // track how many objects have been created...
        Interlocked.Increment(ref count);
    }
    [ProtoMember(1)]
    public int Id { get; set; }
    [ProtoMember(2)]
    public double Bar { get; set; }    
}
static class Program {
    static void Main() {
        MemoryStream ms = new MemoryStream();
        Random rand = new Random();
        for (int i = 1; i <= 5000; i++) {
            Foo foo = new Foo { Bar = rand.NextDouble(), Id = i };
            Serializer.SerializeWithLengthPrefix(ms, foo,PrefixStyle.Base128, 1);
        }
        ms.Position = 0;
        // skip 1000
        int index = 0;
        object obj;
        Console.WriteLine(Foo.ObjectCount);
        Serializer.NonGeneric.TryDeserializeWithLengthPrefix(
            ms, PrefixStyle.Base128,
            tag => ++index == 1000 ? typeof(Foo) : null, out obj);
        Console.WriteLine(Foo.ObjectCount);
        Console.WriteLine(((Foo)obj).Id);
    }
}

现在它是一个完整的图(带有循环引用)。但是您的建议很有趣。当您说“流式处理”时,您是否意味着,当我枚举集合时,它会按需从序列化文件中分页记录(即不会一次性加载所有内容到内存中)? - Sam
正确;主要可以将项作为顶级对象的序列挑选出来(假设数据实质上是同类记录的序列)- 但您还可以使用一些有趣的集合类别来删除记录 - 或按序列处理它们,然后将它们删除(而不是缓冲它们)。 - Marc Gravell
关于循环引用;你可以添加属性来表示“不序列化此关系”,并且(如果需要)使用序列化回调进行父级修复(即,假设父级会有一个回调告诉子记录其父级是谁)。 - Marc Gravell
此外 - 它并不明显,但是您可以让系统完全跳过早期的项(而不是反序列化它们并将其丢弃); 如果需要,我可以提供一个例子...或者添加一个辅助方法(可能更有用)。 - Marc Gravell
嘿,感谢您的大力帮助。我刚刚下载了protobuf-net。我将在接下来的几天里尝试它。如果这对我的情况有用,那将为我节省很多痛苦!(我差点要写一个基于块的算法!!)。如果我有任何疑问,我会联系您的。提前跳过示例(在反序列化之前跳过)将非常好... - Sam
显示剩余4条评论

2

我没有像您这样的情景经验。然而,过去我曾讨论过类似的问题,以下是我们讨论的结果(虽然我承认我从未见过实现)。另外,恐怕没有简单明了的解决方案。

假设:

i. 要写入的数据已排序。

解决方案:

i. 将数据存储库分成多个文件。将一定范围内的排序值分配给每个文件。例如,将记录1-10000存储在文件1中,将记录100001-20000存储在文件2中,以此类推。

ii. 当您写入/读取数据时,您事先知道范围,因此可以满足第二点。

iii. 只要两个或更多进程请求相同的数据的机会较小,它也将解决第3点。

为了能够提供更准确的解决方案,我们需要更多关于您尝试实现的内容的信息。


我同意,使用多文件方案几乎肯定是最好的。 - C. Ross
是的,多文件解决方案可以起作用,但不会使数据文件具有可移植性。我在原帖上发表了评论,说明了我想要实现什么。 - Sam
请问如何发送一个1GB的数据文件?为什么多文件解决方案会使数据文件不可移植,除非有人计划以某种方式修改数据文件,从而导致数据因修改而变得无序。 - rAm
抱歉,我所说的“可移植”是指用户很难通过电子邮件发送200个甚至20个单独的文件。他们习惯于使用其他软件中的数据文件类型概念。用户通常通过FTP或内部SAN共享大型数据文件,并通过电子邮件共享较小的文件。 - Sam
是的,现在我明白了。请告诉我们你最终选择了什么?很多时候并没有完美的解决方案,符合核心条件的才是胜者。 - rAm

0

我认为我们需要更多关于文件实际样式的信息...

你不能只从文件中读取sizeof(yourstruct)大小的片段,然后分别处理它们,而不是将所有记录都读入内存吗?


0

对于部分(或拆分)反序列化(我自己一直在研究,例如游戏关卡中的动态和静态部分),我认为您将不得不编写自己的序列化引擎。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接