ObjectRepository - vzor úložiště .NET v paměti pro vaše domácí projekty

Proč ukládat všechna data do paměti?

Chcete-li uložit data webových stránek nebo backendu, první touhou většiny rozumných lidí bude vybrat si databázi SQL. 

Někdy ale přichází na mysl myšlenka, že datový model není vhodný pro SQL: například při sestavování vyhledávacího nebo sociálního grafu musíte hledat složité vztahy mezi objekty. 

Nejhorší je, když pracujete v týmu a kolega neví, jak sestavit rychlé dotazy. Kolik času jste strávili řešením problémů N+1 a budováním dalších indexů, aby se SELECT na hlavní stránce dokončil v rozumném čase?

Dalším oblíbeným přístupem je NoSQL. Před několika lety byl kolem tohoto tématu velký humbuk - pro každou vhodnou příležitost nasadili MongoDB a byli spokojeni s odpověďmi ve formě dokumentů json (mimochodem, kolik berlí jste museli vložit kvůli kruhovým odkazům v dokumentech?).

Navrhuji zkusit jinou, alternativní metodu – proč nezkusit všechna data uložit do paměti aplikace a pravidelně je ukládat do náhodného úložiště (soubor, vzdálená databáze)? 

Paměť zlevnila a veškerá možná data pro většinu malých a středních projektů se vejdou do 1 GB paměti. (Například můj oblíbený domácí projekt je finanční sledovač, která uchovává denní statistiky a historii mých výdajů, zůstatků a transakcí po dobu jednoho roku a půl, spotřebuje pouze 45 MB paměti.)

výhody:

  • Přístup k datům se zjednoduší – nemusíte se starat o dotazy, líné načítání, funkce ORM, pracujete s běžnými objekty C#;
  • Nejsou žádné problémy spojené s přístupem z různých vláken;
  • Velmi rychlé - žádné síťové požadavky, žádný překlad kódu do dotazovacího jazyka, žádná potřeba (de)serializace objektů;
  • Je přijatelné ukládat data v jakékoli formě – ať už v XML na disku, na SQL Serveru nebo v Azure Table Storage.

nevýhody:

  • Horizontální škálování je ztraceno a v důsledku toho nelze provést nasazení s nulovými prostoji;
  • Pokud aplikace spadne, můžete částečně přijít o data. (Ale naše aplikace nikdy nespadne, že?)

Jak to funguje?

Algoritmus je následující:

  • Na začátku se vytvoří spojení s datovým úložištěm a data se načtou;
  • Je vytvořen objektový model, primární indexy a relační indexy (1:1, 1:Many);
  • Předplatné je vytvořeno pro změny vlastností objektu (INotifyPropertyChanged) a pro přidávání nebo odebírání prvků do kolekce (INotifyCollectionChanged);
  • Když je předplatné spuštěno, změněný objekt je přidán do fronty pro zápis do datového úložiště;
  • Změny v úložišti jsou pravidelně ukládány (na časovači) do vlákna na pozadí;
  • Když aplikaci ukončíte, změny se také uloží do úložiště.

Příklad kódu

Přidání potřebných závislostí

// Основная библиотека
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

Popisujeme datový model, který bude v úložišti uložen

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; }
}

Pak objektový model:

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;
}

A nakonec samotná třída úložiště pro přístup k datům:

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

Vytvořte instanci ObjectRepository:

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

Pokud projekt bude používat HangFire

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

Vložení nového objektu:

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

S tímto voláním objekt Rodičovský model se přidá do místní mezipaměti i do fronty pro zápis do databáze. Proto tato operace trvá O(1) a s tímto objektem lze okamžitě pracovat.

Chcete-li například najít tento objekt v úložišti a ověřit, že vrácený objekt je stejná instance:

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

Co se s tím stane? Soubor () se vrací TableDictionary, který obsahuje Souběžný slovník a poskytuje další funkce primárních a sekundárních indexů. To vám umožňuje mít metody pro vyhledávání podle Id (nebo jiných libovolných uživatelských indexů) bez úplného opakování všech objektů.

Při přidávání objektů do Úložiště objektů je přidáno předplatné, aby se změnily jejich vlastnosti, takže jakákoli změna vlastností má také za následek přidání tohoto objektu do fronty zápisu. 
Aktualizace vlastností zvenčí vypadá stejně jako práce s objektem POCO:

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

Objekt můžete odstranit následujícími způsoby:

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

To také přidá objekt do fronty pro odstranění.

Jak funguje ukládání?

Úložiště objektů když se monitorované objekty změní (buď přidání nebo odstranění nebo změna vlastností), vyvolá událost ModelZměněnpřihlášen k odběru Úložiště. Implementace Úložiště když dojde k události ModelZměněn změny jsou zařazeny do 3 front - pro přidávání, pro aktualizaci a pro mazání.

Také implementace Úložiště při inicializaci vytvoří časovač, který způsobí uložení změn každých 5 sekund. 

Kromě toho existuje rozhraní API pro vynucení volání uložení: ObjectRepository.Save().

Před každým uložením jsou z front nejprve odstraněny nesmyslné operace (například duplicitní události - kdy byl objekt dvakrát změněn nebo rychle přidány/odebrány objekty) a teprve poté samotné uložení. 

Ve všech případech se uloží celý aktuální objekt, takže je možné, že se objekty uloží v jiném pořadí, než byly změněny, včetně novějších verzí objektů, než v době, kdy byly přidány do fronty.

Co je tam dalšího?

  • Všechny knihovny jsou založeny na .NET Standard 2.0. Může být použit v jakémkoli moderním .NET projektu.
  • Rozhraní API je bezpečné pro vlákna. Interní kolekce jsou realizovány na základě Souběžný slovník, obsluhy událostí buď zámky mají, nebo je nepotřebují. 
    Jediné, co stojí za zapamatování, je zavolat ObjectRepository.Save();
  • Libovolné indexy (vyžadují jedinečnost):

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

Kdo to používá?

Osobně jsem tento přístup začal používat ve všech hobby projektech, protože je pohodlný a nevyžaduje velké náklady na psaní datové vrstvy nebo nasazování těžké infrastruktury. Osobně mi většinou stačí ukládání dat do litedb nebo souboru. 

Ale v minulosti, když dnes již neexistující startup EscapeTeams (Myslel jsem, že tady to je, peníze - ale ne, zase zkušenost) – používá se k ukládání dat v Azure Table Storage.

Plány do budoucna

Rád bych napravil jednu z hlavních nevýhod tohoto přístupu – horizontální škálování. K tomu potřebujete buď distribuované transakce (sic!), nebo se rázně rozhodnout, že by se stejná data z různých instancí neměla měnit, nebo je nechat změnit podle zásady „kdo je poslední, má pravdu“.

Z technického hlediska vidím jako možné následující schéma:

  • Místo objektového modelu ukládejte EventLog a Snapshot
  • Najít další instance (přidat koncové body všech instancí do nastavení? udp discovery? master/slave?)
  • Replikujte mezi instancemi EventLog pomocí libovolného konsensuálního algoritmu, jako je RAFT.

Znepokojuje mě i další problém – kaskádové mazání neboli detekce případů mazání objektů, které mají odkazy z jiných objektů. 

Zdrojový kód

Pokud jste dočetli až sem, pak už zbývá jen přečíst kód; najdete ho na GitHubu:
https://github.com/DiverOfDark/ObjectRepository

Zdroj: www.habr.com

Přidat komentář