ObjectRepository - 适用于您的家庭项目的 .NET 内存存储库模式

为什么要将所有数据存储在内存中?

要存储网站或后端数据,大多数理智的人的第一个愿望是选择 SQL 数据库。 

但有时我会想到数据模型不适合 SQL:例如,在构建搜索或社交图时,您需要搜索对象之间的复杂关系。 

最糟糕的情况是当您在团队中工作并且同事不知道如何构建快速查询时。 您花费了多少时间来解决 N+1 问题并构建额外的索引,以便主页上的 SELECT 可以在合理的时间内完成?

另一种流行的方法是 NoSQL。 几年前,围绕这个主题进行了很多炒作 - 在任何方便的场合,他们都部署了 MongoDB,并对 json 文档形式的答案感到满意 (顺便问一下,因为文档中的循环链接,你得插多少拐杖?).

我建议尝试另一种替代方法 - 为什么不尝试将所有数据存储在应用程序内存中,定期将其保存到随机存储(文件、远程数据库)? 

内存变得越来越便宜,大多数中小型项目的任何可能的数据都可以放入 1 GB 内存中。 (例如,我最喜欢的家庭项目是 金融追踪器,它保存了我一年半的支出、余额和交易的每日统计数据和历史记录,仅消耗 45 MB 内存。)

优点:

  • 访问数据变得更容易 - 您无需担心查询、延迟加载、ORM 功能,您可以使用普通的 C# 对象;
  • 不存在与不同线程访问相关的问题;
  • 非常快——没有网络请求,不需要将代码翻译成查询语言,不需要对象的序列化(反序列化);
  • 以任何形式存储数据都是可以接受的 - 无论是磁盘上的 XML、SQL Server 还是 Azure 表存储。

缺点:

  • 失去横向扩展能力,无法做到零宕机部署;
  • 如果应用程序崩溃,您可能会丢失部分数据。 (但是我们的应用程序永远不会崩溃,对吧?)

它是如何工作的呢?

算法如下:

  • 启动时,与数据存储建立连接,并加载数据;
  • 建立对象模型、主索引、关系索引(1:1、1:Many);
  • 为对象属性的更改 (INotifyPropertyChanged) 以及向集合添加或删除元素 (INotifyCollectionChanged) 创建订阅;
  • 当订阅被触发时,变化的对象被添加到队列中以写入数据存储;
  • 对存储的更改会定期(在计时器上)保存在后台线程中;
  • 当您退出应用程序时,更改也会保存到存储中。

代码示例

添加必要的依赖项

// Основная библиотека
Install-Package OutCode.EscapeTeams.ObjectRepository
    
// Хранилище данных, в котором будут сохраняться изменения
// Используйте то, которым будете пользоваться.
Install-Package OutCode.EscapeTeams.ObjectRepository.File
Install-Package OutCode.EscapeTeams.ObjectRepository.LiteDb
Install-Package OutCode.EscapeTeams.ObjectRepository.AzureTableStorage
    
// Опционально - если нужно хранить модель данных для Hangfire
// Install-Package OutCode.EscapeTeams.ObjectRepository.Hangfire

我们描述将存储在存储中的数据模型

public class ParentEntity : BaseEntity
{
    public ParentEntity(Guid id) => Id = id;
}
    
public class ChildEntity : BaseEntity
{
    public ChildEntity(Guid id) => Id = id;
    public Guid ParentId { get; set; }
    public string Value { get; set; }
}

然后是对象模型:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Пример связи 1:Many
    public IEnumerable<ChildModel> Children => Multiple<ChildModel>(x => x.ParentId);
    
    protected override BaseEntity Entity { get; }
}
    
public class ChildModel : ModelBase
{
    private ChildEntity _childEntity;
    
    public ChildModel(ChildEntity entity)
    {
        _childEntity = entity;
    }
    
    public ChildModel() 
    {
        _childEntity = new ChildEntity(Guid.NewGuid());
    }
    
    public Guid ParentId
    {
        get => _childEntity.ParentId;
        set => UpdateProperty(() => _childEntity.ParentId, value);
    }
    
    public string Value
    {
        get => _childEntity.Value;
        set => UpdateProperty(() => _childEntity.Value, value
    }
    
    // Доступ с поиском по индексу
    public ParentModel Parent => Single<ParentModel>(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

最后,用于访问数据的存储库类本身:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Для тестов, позволяет не сохранять изменения в базу
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Если используется Hangfire и необходимо хранить модель данных для Hangfire в ObjectRepository
        // this.RegisterHangfireScheme(); 
    
        Initialize();
    }
}

创建一个 ObjectRepository 实例:

var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
    
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();

如果项目将使用 HangFire

public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
    services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}

插入一个新对象:

var newParent = new ParentModel()
repository.Add(newParent);

通过这个调用,对象 父模型 被添加到本地缓存和写入数据库的队列中。 因此,该操作需要 O(1),并且可以立即使用该对象。

例如,要在存储库中查找此对象并验证返回的对象是否是同一个实例:

var parents = repository.Set<ParentModel>();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));

怎么了? 放() 回报 表格字典, 其中包含 并发词典 并提供主索引和辅助索引的附加功能。 这允许您使用 Id(或其他任意用户索引)进行搜索的方法,而无需完全迭代所有对象。

添加对象时 对象库 添加订阅来更改其属性,因此属性的任何更改也会导致该对象被添加到写入队列中。 
从外部更新属性看起来与使用 POCO 对象相同:

myParent.Children.First().Property = "Updated value";

您可以通过以下方式删除对象:

repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove<ParentModel>(x => !x.Children.Any());

这也会将该对象添加到删除队列中。

储蓄如何发挥作用?

对象库 当监视的对象发生更改(添加或删除或更改属性)时,引发事件 型号已更改订阅了 的IStorage。 Дализации 的IStorage 当事件发生时 型号已更改 更改被放入 3 个队列中 - 用于添加、更新和删除。

还有实现 的IStorage 初始化时,它们会创建一个计时器,每 5 秒保存一次更改。 

此外,还有一个 API 可以强制保存调用: 对象存储库.Save().

每次保存之前,首先从队列中删除无意义的操作(例如,重复事件 - 当对象更改两次或快速添加/删除对象时),然后才保存本身。 

在所有情况下,都会保存整个当前对象,因此对象的保存顺序可能与更改顺序不同,包括比添加到队列时更新的对象版本。

那里还有什么?

  • 所有库均基于 .NET Standard 2.0。 可用于任何现代 .NET 项目。
  • 该 API 是线程安全的。 内部集合的实现基于 并发字典,事件处理程序要么有锁,要么不需要它们。 
    唯一值得记住的是打电话 ObjectRepository.Save();
  • 任意索引(要求唯一性):

repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");

谁使用它?

就我个人而言,我开始在所有业余爱好项目中使用这种方法,因为它很方便,并且不需要大量费用来编写数据访问层或部署重型基础设施。 就我个人而言,将数据存储在 litedb 或文件中通常对我来说就足够了。 

但在过去,当现已解散的初创公司 EscapeTeams (我以为就在这里,钱——但不,再次体验) - 用于将数据存储在 Azure 表存储中。

未来的计划

我想解决这种方法的主要缺点之一 - 水平缩放。 为此,您要么需要分布式事务(原文如此!),要么做出坚定的决定,来自不同实例的相同数据不应更改,或者让它们按照“谁最后才是正确的”原则进行更改。

从技术角度来看,我认为以下方案是可行的:

  • 存储事件日志和快照而不是对象模型
  • 查找其他实例(将所有实例的端点添加到设置中?udp发现?主/从?)
  • 通过任何共识算法(例如 RAFT)在 EventLog 实例之间进行复制。

还有另一个让我担心的问题——级联删除,或者检测删除与其他对象有链接的对象的情况。 

源代码

如果你已经读到这里,那么剩下的就是阅读代码了;它可以在 GitHub 上找到:
https://github.com/DiverOfDark/ObjectRepository

来源: habr.com

添加评论