在C#中寻找简单的独立持久字典实现

时间:2020-03-06 14:25:00  来源:igfitidea点击:

对于一个开放源代码项目,我正在寻找一种良好且简单的由文件支持的Dictionary的实现。这意味着,如果应用程序崩溃或者重新启动,字典将保持其状态。我希望每次触摸字典时都更新基础文件。 (添加值或者删除值)。 FileWatcher不是必需的,但可能很有用。

class PersistentDictionary<T,V> : IDictionary<T,V>
{
    public PersistentDictionary(string filename)
    {

    } 
}

要求:

  • 开源,不依赖本机代码(没有sqlite)
  • 理想的情况是非常简短的实现
  • 设置或者清除值时,不应重写整个基础文件,而应查找文件中的位置并更新值。

类似问题

  • .Net中的持久性二叉树/哈希表
  • 磁盘支持的字典/ C#缓存
  • `PersistentDictionary <键,值>

解决方案

听起来很酷,但是我们如何解决存储值(如果是引用类型)本身的更改?如果它是不可变的,那么一切都很好,但是如果不是,那你就有点塞满了:-)

如果我们不处理不可变的值,我怀疑更好的方法是在值级别上处理持久性,并根据需要重新构建字典。

(编辑以添加说明)

只需使用序列化即可。查看BinaryFormatter类。

我什么都不知道能解决问题。它必须是固定大小的结构,以便可以满足无需重写整个文件即可重写记录的要求。

这意味着正常的字符串已用完。

就像道格拉斯所说的那样,我们需要知道类型的固定大小(T和V)。同样,这些实例中的任何实例引用的对象网格中的可变长度实例也将消失。

尽管如此,实现由文件支持的字典还是非常简单的,在继承或者封装了Dictionary <TKey,TValue>类之后,我们可以使用BinaryWriter类将类型写入磁盘。

  • bplustreedotnet bplusdotnet包是C#,java和Python中交叉兼容的数据结构实现的库,对于需要存储和检索持久性信息的应用程序很有用。 bplusdotnet数据结构使永久存储与值关联的字符串键变得容易。
  • ESENT托管接口不是100%托管代码,但值得一提的是,非托管库本身已经是每个Windows XP / 2003 / Vista / 7机器的一部分ESENT是Windows的一部分,是嵌入式数据库存储引擎(ISAM)。它通过行级锁定,预写日志记录和快照隔离提供可靠的事务处理并发高性能数据存储。这是ESENT Win32 API的托管包装。
  • Akavache * Akavache是​​一个异步的,持久性的键值缓存,用于在C#中编写本机桌面和移动应用程序。可以将其视为桌面应用程序的memcached。

C5通用收藏库

C5提供了标准.NetSystem.Collections.Generic命名空间未提供的功能和数据结构,例如持久树数据结构,基于堆的优先级队列,哈希索引数组列表和链接列表,以及集合更改事件。

一种方法是使用内置于窗口中的可扩展存储引擎来存储东西。这是一个本地的win数据库,支持索引编制,事务处理等。

考虑一个内存映射文件。我不确定.NET中是否有直接支持,但是我们可以调用Win32调用。

我没有实际使用过,但是该项目显然在C#中提供了类似mmap()的实现

地图

我不是一个程序员,但是不会创建一个非常简单的XML格式来存储数据吗?

<dico> 
   <dicEntry index="x">
     <key>MyKey</key>
     <val type="string">My val</val>
   </dicEntry>
   ...
</dico>

在这里,我们可以加载XML文件DOM,并根据需要填充字典,

XmlDocument xdocDico = new XmlDocument();
string sXMLfile;
public loadDico(string sXMLfile, [other args...])
{
   xdocDico.load(sXMLfile);
   // Gather whatever you need and load it into your dico
}
public flushDicInXML(string sXMLfile, dictionary dicWhatever)
{
   // Dump the dic in the XML doc & save
}
public updateXMLDOM(index, key, value)
{
   // Update a specific value of the XML DOM based on index or key
}

然后,我们可以随时更新DOM并将其保存在磁盘上。

xdocDico.save(sXMLfile);

如果我们有能力将DOM保留在内存性能方面,那么它很容易处理。根据要求,我们甚至可能根本不需要字典。

让我分析一下:

  • 通过密钥检索信息
  • 持久存储
  • 1值更改时,不想写回整个文件
  • 应该在崩溃中幸存

我认为我们想要一个数据库。

编辑:我认为我们正在寻找错误的东西。搜索符合我们要求的数据库。并更改一些要求,因为我认为很难满足所有要求。

我找到了此链接,听起来像我们在找什么。它是用Java编程的,但移植到C#并不难:

http://www.javaworld.com/javaworld/jw-01-1999/jw-01-step.html

我认为问题很可能是最后一点:

When setting or clearing a value it should not re-write the entire underlying file, instead it should seek to the position in the file and update the value.

这正是数据库基本上在描述基于文件的简单表结构的方式。

我们可以通过查看字符串来说明问题。

内存中的字符串是灵活的事情,我们在声明字符串类型时无需知道C中字符串的长度。

在数据存储字符串中,其他所有内容都是固定大小的。我们在磁盘上保存的字典按顺序只是字节的集合。

如果我们在中间替换一个值,则其大小必须完全相同,否则我们将不得不重写紧随其后的每个字节。

这就是为什么大多数数据库都将text和blob字段限制为固定大小的原因。 Sql 2005+中的varchar(max)/varbinary(max)等新功能实际上是对行的巧妙简化,仅实际存储了指向实际数据的指针。

我们不能在示例中使用固定大小,因为它是通用的,我们不知道将要存储什么类型,因此无法将值填充到最大大小。

我们可以这样做:

class PersistantDictionary<T,V> : Dictionary<T,V>
    where V:struct

...由于值类型的存储大小没有变化,尽管我们必须谨慎实施以为每种类型保存正确的存储量。

但是,如果我们查看SQL Server和Oracle如何处理表更改,它们不会像这样改变值,那么模型就不会非常有效。相反,他们将旧记录标记为幽灵,并使用新值添加新记录。当数据库不太繁忙时,将清除旧的幻影记录。

我认为我们正在尝试重新发明轮子:

  • 如果我们要处理大量数据,则确实需要使用功能完善的数据库进行签出。 MySql或者SqlLite都很好,但是我们不会找到一个好的,简单的,开源的和lite的实现。
  • 如果我们不处理大量数据,那么我将进行整个文件序列化,并且这里已经有很多关于如何执行此操作的好建议。

我建议使用SQL Server Express或者其他数据库。

  • 免费。
  • 它与C#(包括LINQ)很好地集成在一起。
  • 它比自制解决方案快。
  • 它比自制解决方案更可靠。
  • 它比基于磁盘的简单数据结构更强大,因此将来可以轻松完成更多工作。
  • SQL是一种行业标准,因此其他开发人员将更容易理解程序,并且我们将拥有将来有用的技能。

我根据一段时间前对另一个项目的非常相似(我认为相同)的要求,自己编写了一个实现。当我这样做时,我意识到的一件事是,在大多数情况下,我们将要进行写操作,而在程序崩溃或者关闭时,我们很少执行读操作。因此,其想法是使写入尽可能快。我所做的是创建一个非常简单的类,该类将在发生任何事情时将所有操作(添加和删除)的日志记录到字典中。因此,过了一会儿,我们会在键之间重复很多。因此,一旦对象检测到一定数量的重复,它将清除日志并将其重写,因此每个键及其值仅出现一次。

不幸的是,我们不能对Dictionary进行子类化,因为我们无法覆盖其中的任何内容。这是我的简单实现,对不起,我还没有测试过,但是我想我们可能想要这个主意。随意使用并根据需要进行更改。

class PersistentDictManager {
    const int SaveAllThreshold = 1000;

    PersistentDictManager(string logpath) {
        this.LogPath = logpath;
        this.mydictionary = new Dictionary<string, string>();
        this.LoadData();
    }

    public string LogPath { get; private set; }

    public string this[string key] {
        get{ return this.mydictionary[key]; }
        set{
            string existingvalue;
            if(!this.mydictionary.TryGetValue(key, out existingvalue)) { existingvalue = null; }
            if(string.Equals(value, existingvalue)) { return; }
            this[key] = value;

            // store in log
            if(existingvalue != null) { // was an update (not a create)
                if(this.IncrementSaveAll()) { return; } // because we're going to repeat a key the log
            }
            this.LogStore(key, value);
        }
    }

    public void Remove(string key) {
        if(!this.mydictionary.Remove(key)) { return; }
        if(this.IncrementSaveAll()) { return; } // because we're going to repeat a key in the log
        this.LogDelete(key);
    }

    private void CreateWriter() {
        if(this.writer == null) {
            this.writer = new BinaryWriter(File.Open(this.LogPath, FileMode.Open)); 
        }
    }

    private bool IncrementSaveAll() {
        ++this.saveallcount;
        if(this.saveallcount >= PersistentDictManager.SaveAllThreshold) {
            this.SaveAllData();
            return true;
        }
        else { return false; }
    }

    private void LoadData() {
        try{
            using(BinaryReader reader = new BinaryReader(File.Open(LogPath, FileMode.Open))) {
                while(reader.PeekChar() != -1) {
                    string key = reader.ReadString();
                    bool isdeleted = reader.ReadBoolean();
                    if(isdeleted) { this.mydictionary.Remove(key); }
                    else {
                        string value = reader.ReadString();
                        this.mydictionary[key] = value;
                    }
                }
            }
        }
        catch(FileNotFoundException) { }
    }

    private void LogDelete(string key) {
        this.CreateWriter();
        this.writer.Write(key);
        this.writer.Write(true); // yes, key was deleted
    }

    private void LogStore(string key, string value) {
        this.CreateWriter();
        this.writer.Write(key);
        this.writer.Write(false); // no, key was not deleted
        this.writer.Write(value);
    }

    private void SaveAllData() {
        if(this.writer != null) {
            this.writer.Close();
            this.writer = null;
        }
        using(BinaryWriter writer = new BinaryWriter(File.Open(this.LogPath, FileMode.Create))) {
            foreach(KeyValuePair<string, string> kv in this.mydictionary) {
                writer.Write(kv.Key);
                writer.Write(false); // is not deleted flag
                writer.Write(kv.Value);
            }
        }
    }

    private readonly Dictionary<string, string> mydictionary;
    private int saveallcount = 0;
    private BinaryWriter writer = null;
}

我正在将EHCache移植到.NET。看看这个项目

http://sourceforge.net/projects/thecache/

持久缓存是已经实现的核心功能。所有主要单元测试都通过了。我在分布式缓存方面有些困难,但是我们不需要那部分。

查看此博客:

http://ayende.com/Blog/archive/2009/01/17/rhino.dht-ndash-persistent-amp-distributed-storage.aspx

看起来正是我们想要的。

我已经实现了我们想要的PersistedDictionary。基础存储是内置在Windows中的ESENT数据库引擎。该代码在此处可用:

http://managedesent.codeplex.com/