ObjectRepository — .NET in-memory repositoriesmodell for dine hjemmeprosjekter

Hvorfor lagre all data i minnet?

For lagring av nettsteddata eller backend, er det første ønsket til de fleste fornuftige personer å velge en SQL-database. 

Men noen ganger slår tanken ned at datamodellen ikke passer for SQL: for eksempel, når man bygger søk eller en sosial graf, er det behov for søk på komplekse relasjoner mellom objekter. 

Det verste er situasjonen når man jobber i et team, og en kollega ikke kan lage effektive spørringer. Hvor mye tid har du brukt på å løse N+1-problemer og på å lage ekstra indekser, så SELECT på hovedsiden kan kjøres på en rimelig tid?

En annen populær tilnærming er NoSQL. For flere år siden var det stor hype rundt dette temaet – for enhver passende anledning ble MongoDB satt opp, og man gledet seg over svar i form av JSON-dokumenter. (Forresten, hvor mange løsninger måtte man lage på grunn av sirkulære referanser i dokumentene?).

Jeg foreslår å prøve en annen, alternativ metode – hvorfor ikke prøve å lagre all data i applikasjonens minne, periodisk lagre i en vilkårlig lagring (fil, ekstern database)? 

Minne har blitt billig, og alle potensielle data for flesteparten av små og mellomstore prosjekter vil passe inn i 1 GB minne. (For eksempel, mitt favoritt hjemmeprosjekt – en økonomisk tracker, som fører daglig statistikk og historikk over mine utgifter, saldi og transaksjoner over halvannet år, bruker bare 45 MB minne.)

Fordeler:

  • Tilgang til data blir enklere – man må ikke bekymre seg for spørringer, lat lasting, spesifikasjoner av ORM; arbeidet skjer med vanlige C# objekter;
  • Ingen problemer relatert til tilgang fra ulike tråder;
  • Veldig raskt – ingen nettverksforespørsel, ingen oversettelse av kode til spørringsspråk, ingen (de)serialisering av objekter;
  • Det er akseptabelt å lagre data i hvilken som helst form – enten i XML på disk, i SQL Server, eller i Azure Table Storage.

Ulemper:

  • Man mister horisontal skalering, og som en konsekvens kan man ikke gjøre null nedetid distribusjon;
  • Hvis applikasjonen krasjer, kan man delvis miste data. (Men, applikasjonen vår krasjer vel aldri, sant?)

Hvordan fungerer det?

Algoritmen er som følger:

  • Ved oppstart opprettes det en forbindelse til datalagring, og data lastes inn;
  • Objektmodellen, primære indekser, og relasjonsindekser (1:1, 1:mange) bygges.
  • En abonnemang på ändringar av objektens egenskaper (INotifyPropertyChanged) och på tillägg eller borttagning av element i samlingen (INotifyCollectionChanged) skapas;
  • Vid abonnemangets utlösning läggs det ändrade objektet i kö för att skrivas till datalagringen;
  • Ändringar sparas periodiskt (via timer) i bakgrundstråden till lagringssystemet;
  • Ändringar sparas också till lagringssystemet vid utloggning från applikationen.

Kodeeksempel

Lägg till nödvändiga beroenden

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

Beskriv datamodellen som ska sparas i lagringen

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

Sedan objektmodellen:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Exempel på relation 1: Många
    public IEnumerable Children => Multiple(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
    }
    
    // Åtkomst med sökning enligt index
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Och slutligen själva klass-repositoriet för åtkomst till data:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // För tester, tillåter att inte spara ändringar i databasen
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Om Hangfire används och databasen behöver lagra datamodellen för Hangfire i ObjectRepository
        // this.RegisterHangfireScheme();
    
        Initialize();
    }
}

Skapa en instans av ObjectRepository:

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

Om projektet ska använda HangFire

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

Infogning av ett nytt objekt:

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

Vid denna anrop läggs objektet ParentModel blir lagt til både i lokal hurtigminne og i køen for skriving til databasen. Derfor tar denne operasjonen O(1), og dette objektet kan straks brukes.

For eksempel, for å finne dette objektet i depotet og sikre at det returnerte objektet er den samme instansen:

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

Hva skjer her? Set() returnerer TableDictionary, som inneholder ConcurrentDictionary og gir ekstra funksjonalitet for primære og sekundære indekser. Dette gjør at det finnes metoder for å søke etter Id (eller andre vilkårlige brukerdefinerte indekser) uten å måtte iterere gjennom alle objektene.

Når objekter legges til i ObjectRepository legges det også til et abonnement på endringer i deres egenskaper, så enhver endring av egenskapene fører også til at dette objektet legges i køen for skriving. 
Oppdatering av egenskaper utenfra ser lik ut som å jobbe med et POCO-objekt:

myParent.Children.First().Property = "Oppdatert verdi";

Objektet kan fjernes på følgende måter:

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

Her legges også objektet til i køen for fjerning.

Hvordan fungerer lagringen?

ObjectRepository Når det skjer endringer på de sporene objektene (som tillegg eller fjerning, samt endring av egenskaper) utløses en hendelse ModelChanged, som er abonnert på av IStorage. Implementeringene IStorage ved hendelsen ModelChanged samler endringene i 3 køer – for tillegg, for oppdateringer og for fjerning.

Implementeringene IStorage oppretter også en timer ved initialisering som hver 5. sekund kaller lagring av endringene. 

I tillegg finnes det et API for å tvinge frem lagring: ObjectRepository.Save().

Før hver lagring skjer det først en fjerning av meningsløse operasjoner fra køene (for eksempel duplikater av hendelser – når objektet har blitt endret to ganger, eller rask tillegg/fjerning av objekter), og deretter selve lagringen. 

I alle tilfeller lagres den aktuelle objektet i sin helhet, så det kan oppstå situasjoner der objekter lagres i en annen rekkefølge enn de endret seg, inkludert at nyere versjoner av objekter kan lagres enn på tidspunktet for legging i kø.

Hva mer er det?

  • Alle bibliotekene er basert på .NET Standard 2.0. Kan brukes i ethvert moderne .NET-prosjekt.
  • API-en er tråd-sikker. Interne samlinger er implementert på basis av ConcurrentDictionary, hendelsesbehandlere har enten låser eller trenger dem ikke. 
    Det eneste du bør huske på, er å kalle ObjectRepository.Save();
  • Vilkårlige indekser (krever unikhet):

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

Hvem bruker dette?

Personlig har jeg begynt å bruke denne tilnærmingen i alle hobbyprosjekter, fordi det er praktisk, og krever ikke store kostnader for å skrive et datatilgangslag eller sette opp tung infrastruktur. For meg, er det vanligvis nok å lagre data i litedb eller i en fil. 

Men tidligere, da teamet jobbet med den nå nedlagte oppstarten EscapeTeams (jeg tenkte, her er pengene – men nei, igjen er det erfaring) – brukte vi Azure Table Storage for datalagring.

Fremtidsplaner

Det er ønskelig å fikse en av de største ulempene ved denne tilnærmingen – horisontal skalering. For dette er det nødvendig med enten distribuerte transaksjoner (sic!), eller ta en beslutning om at de samme dataene fra ulike instanser ikke skal endres, eller la dem endres etter prinsippet "den siste som endrer, har rett".

Fra et teknisk ståsted ser jeg en mulig løsning:

  • Lagre i stedet for objektmodellen EventLog og Snapshot
  • Finne andre instanser (legge til sluttpunktene for alle instanser i innstillingene? UDP oppdagelse? master/slave?)
  • Replisere mellom instanser EventLog via en hvilken som helst konsensusalgoritme, for eksempel RAFT.

Det finnes også et annet problem som bekymrer meg – dette med kaskade-sletting, eller å oppdage tilfeller av sletting av objekter som det er lenker til fra andre objekter. 

Kildekoden

Hvis du har lest hit – er det bare å lese koden videre, som du kan finne på GitHub:
https://github.com/DiverOfDark/ObjectRepository

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster