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());

這也會將該物件新增至刪除佇列。

儲蓄如何運作?

對像庫 當監視的物件發生變更(新增或刪除或變更屬性)時,引發事件 型號已更改訂閱了 儲存。 Дализации 儲存 當事件發生時 型號已更改 更改被放入 3 個佇列中 - 用於新增、更新和刪除。

還有實現 儲存 初始化時,它們會建立一個計時器,每 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

來源: www.habr.com

添加評論