ObjectRepository-主项目的.NET内存中存储库模式

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


对于存储站点或后端数据,大多数理智的人将首先使用SQL数据库。


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


最糟糕的情况是当您在团队中工作时,而同事无法建立快速查询。 您花费了多少时间来解决N + 1问题并建立其他索引,以便在合理的时间内完成主页上的SELECT?


另一种流行的方法是NoSQL。 几年前,围绕该主题进行了大肆宣传-我们有任何机会部署了MongoDB,并以json-documents的形式享受了答案(顺便说一下,由于文档中的循环链接,必须插入多少拐杖?)


为什么不尝试将所有数据存储在应用程序的内存中,并定期将其保存到任意存储(文件,远程数据库)中?


内存已经变得便宜了,大多数中小型项目的所有可能数据都将适合1 GB内存。 (例如,我最喜欢的家庭项目-一个财务跟踪器 ,在一年半的时间内保持每日统计数据以及我的支出,余额和交易历史记录,仅消耗45 MB的内存。)


优点:


  • 数据访问变得越来越容易-无需担心查询,延迟加载,ORM功能,可与普通C#对象一起使用;
  • 来自不同线程的访问没有任何问题;
  • 非常快-无需网络请求,无需将代码转换为查询语言,无需对象的(序列化)序列;
  • 允许以任何形式存储数据-至少以磁盘上的XML形式,至少以SQL Server形式,至少以Azure表存储形式存储。

缺点:


  • 水平扩展丢失了,结果,零停机时间部署无法完成;
  • 如果应用程序崩溃,则可能会部分丢失数据。 (但是我们的应用程序永远不会崩溃,对吗?)

如何运作?


算法如下:


  • 首先,建立与数据仓库的连接,并下载数据。
  • 建立了对象模型,主索引和关系索引(1:1、1:1和许多);
  • 创建预订用于更改对象的属性(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());  }    //   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); 

在此调用中, ParentModel对象添加到本地缓存和数据库的写队列中。 因此,此操作的耗时为O(1),您可以立即使用该对象。


例如,要在存储库中找到此对象并确保返回的对象是同一实例:


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

这会发生什么? Set <ParentModel>()返回TableDictionary <ParentModel> ,其中包含ConcurrentDictionary <ParentModel,ParentModel>并为主索引和辅助索引提供其他功能。 这样,您就可以拥有按ID(或其他任意自定义索引)进行搜索的方法,而无需完全枚举所有对象。


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


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

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


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

这还将对象添加到删除队列中。


保护工作如何进行?


更改跟踪对象(添加或删除以及更改属性)时, ObjectRepository引发ModelChanged事件,该事件是IStorage所预订的 。 当发生ModelChanged事件时, IStorage的实现总结了3个队列中的更改-添加,更新和删除。


同样,在初始化期间, IStorage实现会创建一个计时器,该计时器每5秒钟导致更改保存一次。


此外,还有一个用于强制保存调用的API: ObjectRepository.Save()


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


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


还有什么?


  • 所有库均基于.NET Standard 2.0。 它可以在任何现代.NET项目中使用。
  • 该API是线程安全的。 内部集合基于ConcurrentDictionary ,事件处理程序具有锁或不需要锁。
    唯一要记住的是调用ObjectRepository.Save();。
  • 自定义索引(要求唯一性):

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

谁在使用它?


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


但是在过去,后来的创业公司EscapeTeams与团队合作时(他们以为他们是有钱的,但是再也没有经验 ),他们使用Azure Table Storage来存储数据。


未来计划


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


从技术角度来看,我认为可以采用以下方案:


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

还有一个困扰我的问题-级联删除,或检测删除从其他对象引用的对象的情况。


源代码


如果您读到这里-那么只有代码有待阅读,可以
在github上找到

Source: https://habr.com/ru/post/zh-CN452232/


All Articles