ObjectRepository - .NET in-memory repositorypatroon voor uw thuisprojecten

Waarom alle gegevens in het geheugen opslaan?

Om website- of backend-gegevens op te slaan, zal de eerste wens van de meeste weldenkende mensen het kiezen van een SQL-database zijn. 

Maar soms komt de gedachte bij me op dat het datamodel niet geschikt is voor SQL: bij het bouwen van een zoek- of sociale grafiek moet je bijvoorbeeld zoeken naar complexe relaties tussen objecten. 

De ergste situatie is wanneer je in een team werkt en een collega niet weet hoe hij snelle queries moet maken. Hoeveel tijd heb je besteed aan het oplossen van N+1-problemen en het bouwen van extra indexen, zodat de SELECT op de hoofdpagina binnen een redelijke tijd zou worden voltooid?

Een andere populaire aanpak is NoSQL. Een aantal jaren geleden was er veel hype rond dit onderwerp - voor elke geschikte gelegenheid zetten ze MongoDB in en waren blij met de antwoorden in de vorm van json-documenten (hoeveel krukken heb je trouwens moeten invoegen vanwege de cirkelvormige links in de documenten?).

Ik stel voor om een ​​andere, alternatieve methode te proberen: waarom probeer je niet alle gegevens op te slaan in het applicatiegeheugen, en deze periodiek op te slaan in willekeurige opslag (bestand, externe database)? 

Geheugen is goedkoop geworden en alle mogelijke gegevens voor de meeste kleine en middelgrote projecten passen in 1 GB geheugen. (Mijn favoriete thuisproject is bijvoorbeeld financiële tracker, dat anderhalf jaar lang dagelijkse statistieken en de geschiedenis van mijn uitgaven, saldi en transacties bijhoudt, verbruikt slechts 45 MB geheugen.)

Voors:

  • Toegang tot gegevens wordt eenvoudiger: u hoeft zich geen zorgen te maken over queries, lazyloading, ORM-functies, u werkt met gewone C#-objecten;
  • Er zijn geen problemen verbonden aan toegang vanuit verschillende threads;
  • Zeer snel - geen netwerkverzoeken, geen vertaling van code in een zoektaal, geen noodzaak voor (de)serialisatie van objecten;
  • Het is acceptabel om gegevens in welke vorm dan ook op te slaan: in XML op schijf, in SQL Server of in Azure Table Storage.

Tegens:

  • Horizontale schaalvergroting gaat verloren en als gevolg daarvan kan er geen sprake zijn van een zero-downtime-implementatie;
  • Als de applicatie crasht, kunt u gedeeltelijk gegevens kwijtraken. (Maar onze applicatie crasht nooit, toch?)

Hoe werkt het?

Het algoritme is als volgt:

  • In eerste instantie wordt er verbinding gemaakt met de dataopslag en worden data geladen;
  • Er worden een objectmodel, primaire indexen en relationele indexen (1:1, 1:Many) gebouwd;
  • Er wordt een abonnement aangemaakt voor wijzigingen in objecteigenschappen (INotifyPropertyChanged) en voor het toevoegen of verwijderen van elementen aan de collectie (INotifyCollectionChanged);
  • Wanneer het abonnement wordt geactiveerd, wordt het gewijzigde object toegevoegd aan de wachtrij om naar de gegevensopslag te schrijven;
  • Wijzigingen in de opslag worden periodiek (op een timer) opgeslagen in een achtergrondthread;
  • Wanneer u de applicatie afsluit, worden de wijzigingen ook in de opslag opgeslagen.

Codevoorbeeld

Het toevoegen van de nodige afhankelijkheden

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

We beschrijven het datamodel dat in de opslag wordt opgeslagen

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

Dan het objectmodel:

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

En tot slot de repositoryklasse zelf voor toegang tot gegevens:

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

Maak een ObjectRepository-instantie:

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

Als het project HangFire gaat gebruiken

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

Een nieuw object invoegen:

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

Met deze oproep wordt het object OuderModel wordt toegevoegd aan zowel de lokale cache als de wachtrij voor het schrijven naar de database. Daarom duurt deze bewerking O(1) en kan er onmiddellijk met dit object worden gewerkt.

Om dit object bijvoorbeeld in de repository te vinden en te verifiëren dat het geretourneerde object hetzelfde exemplaar is:

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

Wat gebeurt er? Set () geeft terug TabelWoordenboek, die bevat Concurrentwoordenboek en biedt extra functionaliteit van primaire en secundaire indexen. Hierdoor beschikt u over methoden voor het zoeken op ID (of andere willekeurige gebruikersindexen) zonder alle objecten volledig te doorlopen.

Bij het toevoegen van objecten aan ObjectRepository er wordt een abonnement toegevoegd om de eigenschappen ervan te wijzigen, dus elke wijziging in eigenschappen resulteert er ook in dat dit object aan de schrijfwachtrij wordt toegevoegd. 
Het bijwerken van eigenschappen van buitenaf ziet er hetzelfde uit als werken met een POCO-object:

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

U kunt een object op de volgende manieren verwijderen:

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

Hiermee wordt het object ook aan de verwijderwachtrij toegevoegd.

Hoe werkt sparen?

ObjectRepository wanneer bewaakte objecten veranderen (door toevoegen of verwijderen, of door eigenschappen te wijzigen), wordt er een gebeurtenis gegenereerd ModelGewijzigdgeabonneerd op IOpslag. Implementaties IOpslag wanneer er een gebeurtenis plaatsvindt ModelGewijzigd wijzigingen worden in 3 wachtrijen geplaatst: voor toevoegen, bijwerken en verwijderen.

Ook implementaties IOpslag bij initialisatie creëren ze een timer die ervoor zorgt dat wijzigingen elke 5 seconden worden opgeslagen. 

Daarnaast is er een API om een ​​save-call te forceren: ObjectRepository.Save().

Vóór elke opslag worden betekenisloze bewerkingen eerst uit de wachtrijen verwijderd (bijvoorbeeld dubbele gebeurtenissen - wanneer een object tweemaal is gewijzigd of snel objecten zijn toegevoegd/verwijderd), en pas daarna de opslag zelf. 

In alle gevallen wordt het gehele huidige object opgeslagen. Het is dus mogelijk dat objecten in een andere volgorde worden opgeslagen dan waarin ze zijn gewijzigd, inclusief nieuwere versies van objecten dan op het moment dat ze aan de wachtrij werden toegevoegd.

Wat is er nog meer?

  • Alle bibliotheken zijn gebaseerd op .NET Standard 2.0. Kan worden gebruikt in elk modern .NET-project.
  • De API is thread-safe. Interne collecties worden geïmplementeerd op basis van Concurrentwoordenboek, gebeurtenishandlers hebben vergrendelingen of hebben deze niet nodig. 
    Het enige dat de moeite waard is om te onthouden, is bellen ObjectRepository.Save();
  • Willekeurige indexen (vereisen uniciteit):

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

Wie gebruikt het?

Persoonlijk ben ik deze aanpak in alle hobbyprojecten gaan gebruiken omdat het handig is en geen grote kosten met zich meebrengt voor het schrijven van een datatoegangslaag of het inzetten van een zware infrastructuur. Persoonlijk is het voor mij meestal voldoende om gegevens op te slaan in litedb of een bestand. 

Maar in het verleden, toen de inmiddels ter ziele gegane startup EscapeTeams (Ik dacht: hier is het, geld - maar nee, opnieuw ervaring) - gebruikt om gegevens op te slaan in Azure Table Storage.

Plannen voor de toekomst

Ik zou graag een van de belangrijkste nadelen van deze aanpak willen oplossen: horizontale schaalvergroting. Om dit te doen heb je óf gedistribueerde transacties nodig (sic!), óf je moet een wilskrachtige beslissing nemen dat dezelfde gegevens uit verschillende instanties niet mogen veranderen, of je moet ze laten veranderen volgens het principe ‘wie het laatst heeft, heeft gelijk.’

Vanuit technisch oogpunt zie ik het volgende schema mogelijk:

  • Bewaar EventLog en Snapshot in plaats van het objectmodel
  • Vind andere instances (voeg eindpunten van alle instances toe aan de instellingen? udp-discovery? master/slave?)
  • Repliceer tussen EventLog-instanties via elk consensusalgoritme, zoals RAFT.

Er is ook nog een ander probleem dat mij zorgen baart: trapsgewijze verwijdering, of detectie van gevallen van verwijdering van objecten die links hebben van andere objecten. 

Bron

Als je helemaal tot hier hebt gelezen, hoef je alleen nog maar de code te lezen; deze is te vinden op GitHub:
https://github.com/DiverOfDark/ObjectRepository

Bron: www.habr.com

Voeg een reactie