为什么要将所有数据存储在内存中?
对于存储站点或后端数据,大多数理智的人将首先使用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);
- 触发订阅时-将更改后的对象添加到队列中以写入数据仓库;
- 定期(通过计时器)将对存储的更改保存在后台流中;
- 退出应用程序时,也会保存对存储库的更改。
代码示例
我们描述了将存储在存储库中的数据模型 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 class MyObjectRepository : ObjectRepositoryBase { public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance) { IsReadOnly = true;
创建一个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上找到 。