Hvorfor gemme alle data i hukommelsen?
Til opbevaring af datanetværk eller backend vil de fleste fornuftige mennesker først vælge en SQL-database.
Men nogle gange kommer man til at tænke, at datamodellen ikke passer til SQL: for eksempel, når man bygger søgning eller sociale netværk, er der behov for at søge i komplekse relationer mellem objekterne.
Det værste er, når man arbejder i et team, og en kollega ikke kan lave hurtige forespørgsler. Hvor meget tid har du brugt på at løse N+1 problemer og på at bygge ekstra indekser, så SELECT på hovedsiden kørte på et rimeligt tid?
En anden populær tilgang er NoSQL. For flere år siden var der stor hype omkring dette emne — for enhver passende situation satte man MongoDB op og glædede sig over svarene i form af JSON-dokumenter. (Forresten, hvor mange 'workarounds' var nødvendige på grund af cykliske referencer i dokumenterne?).
Jeg foreslår at prøve en anden, alternativ metode — hvorfor ikke prøve at gemme alle data i applikationens hukommelse og periodisk gemme dem i en vilkårlig lagringsenhed (fil, fjerndatabase)?
Hukommelse er blevet billig, og alle potentielle data for de fleste små og mellemstore projekter kan nemt være inden for 1 GB hukommelse. (For eksempel, mit yndlingshjemmeprojekt — , som holder daglig statistik og historik over mine udgifter, saldi og transaktioner i halvandet år, bruger kun 45 MB hukommelse.)
Fordele:
- Adgang til data bliver lettere — man skal ikke bekymre sig om forespørgsler, lazy loading, ORM-specifikke detaljer, arbejdet foregår med almindelige C# objekter;
- Ingen problemer relateret til adgang fra forskellige tråde;
- Meget hurtigt — der er ingen netværksforespørgsler, ingen omkodning til forespørgselssprog, der er ikke behov for (de)serialisering af objekter;
- Det er acceptabelt at gemme data i enhver form — uanset om det er i XML på disken, i SQL Server eller i Azure Table Storage.
Ulemper:
- Horizontalt skalering går tabt, og som følge deraf kan man ikke lave zero downtime deployment;
- Hvis applikationen går ned — kan man delvist miste data. (Men vores applikation går jo aldrig ned, eller hvad?)
Hvordan fungerer det?
Algoritmen er som følger:
- Ved opstart etableres en forbindelse til datalageret, og data indlæses;
- En objektmodel, primære indekser og relationsindekser (1:1, 1:Many) opbygges;
- Der abonneres ændringer i objektets egenskaber (INotifyPropertyChanged) samt tilføjelse eller fjernelse af elementer fra samlingen (INotifyCollectionChanged);
- Når abonnementet aktiveres, tilføjes det ændrede objekt til en kø til opbevaring;
- Periodisk (via en timer) gemmes ændringerne i baggrunden i opbevaringen;
- Når applikationen lukkes, gemmes ændringerne også i opbevaringen.
Code example
Tilføjer nødvendige afhængigheder
// Основная библиотека
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.HangfireBeskriver datamodellen, der skal gemmes i opbevaringen
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; }
}Derefter objektmodellen:
public class ParentModel : ModelBase
{
public ParentModel(ParentEntity entity)
{
Entity = entity;
}
public ParentModel()
{
Entity = new ParentEntity(Guid.NewGuid());
}
public Guid? NullableId => null;
// Eksempel på 1:Many forhold
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);
}
// Adgang med søgning efter indeks
public ParentModel Parent => Single(ParentId);
protected override BaseEntity Entity => _childEntity;
}Og endelig selve klasse-repositoriet til dataadgang:
public class MyObjectRepository : ObjectRepositoryBase
{
public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
{
IsReadOnly = true; // Til test, tillader ikke, at ændringer gemmes i databasen
AddType((ParentEntity x) => new ParentModel(x));
AddType((ChildEntity x) => new ChildModel(x));
// Hvis Hangfire bruges, og det er nødvendigt at gemme datamodellen til Hangfire i ObjectRepository
// this.RegisterHangfireScheme();
Initialize();
}
}Opretter en instans af ObjectRepository:
var memory = new MemoryStream();
var db = new LiteDatabase(memory);
var dbStorage = new LiteDbStorage(db);
var repository = new MyObjectRepository(dbStorage);
await repository.WaitForInitialize();Hvis projektet vil bruge HangFire
public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository)
{
services.AddHangfire(s => s.UseHangfireStorage(objectRepository));
}Indsættelse af et nyt objekt:
var newParent = new ParentModel()
repository.Add(newParent);Ved dette kald vil objektet ParentModel Tilføjes både til den lokale cache og til køen til databaseopdatering. Derfor tager denne operation O(1), og man kan straks arbejde med dette objekt.
For eksempel, for at finde dette objekt i depotet og sikre, at det returnerede objekt er den samme instans:
var parents = repository.Set();
var myParent = parents.Find(newParent.Id);
Assert.IsTrue(ReferenceEquals(myParent, newParent));Hvad sker der her? Set() returnerer TableDictionary, som indeholder ConcurrentDictionary og tilbyder yderligere funktionalitet til primære og sekundære indekser. Dette gør det muligt at have metoder til søgning efter Id (eller andre vilkårlige brugerdefinerede indekser) uden at skulle gennemgå alle objekterne.
Når objekter tilføjes til ObjectRepository , addes der en abonnement på ændring af deres egenskaber, så enhver ændring i egenskaberne medfører, at dette objekt også tilføjes til køen til databaseopdatering.
Opdatering af egenskaber udefra ser ud på samme måde som at arbejde med et POCO-objekt:
myParent.Children.First().Property = "Opdateret værdi";Et objekt kan fjernes på følgende måder:
repository.Remove(myParent);
repository.RemoveRange(otherParents);
repository.Remove(x => !x.Children.Any());Dette medfører også, at objektet tilføjes til køen til fjernelse.
Hvordan fungerer gemmes?
ObjectRepository Når der sker ændringer i de overvågede objekter (både tilføjelse og fjernelse samt ændring af egenskaber), udløses en begivenhed ModelChanged, som er tilmeldt IStorage. Implementeringer IStorage ved opståen af begivenheden ModelChanged samler ændringerne i 3 køer — tilføjelse, opdatering og fjernelse.
Implementeringer IStorage opretter også en timer ved initialisering, som hver 5. sekund kalder til gemme ændringer.
Derudover findes der et API til at tvinge gemmeoperationer: ObjectRepository.Save().
Før hver gemmes operation fjernes først meningsløse operationer fra køerne (f.eks. dubletter af begivenheder — når et objekt er ændret to gange eller hurtig tilføjelse/fjernelse af objekter), og kun derefter finder selve gemmen sted.
I alle tilfælde gemmes den aktuelle objekt fuldstændigt, så der kan opstå situationer, hvor objekterne gemmes i en anden rækkefølge, end de blev ændret, herunder at nyere versioner af objekter kan gemmes end da de blev tilføjet til køen.
Hvad er der ellers?
- Alle biblioteker er baseret på .NET Standard 2.0. Kan bruges i ethvert moderne .NET projekt.
- API er trådsikkert. Interne samlinger er implementeret på basis af ConcurrentDictionary, begivenhedshåndterere har enten lås eller har ikke brug for dem.
Det eneste, du skal huske på, er at kalde ObjectRepository.Save(); - Tilfældige indekser (kræver unikke værdier):
repository.Set<ChildModel>().AddIndex(x => x.Value);
repository.Set<ChildModel>().Find(x => x.Value, "myValue");Hvem bruger dette?
Personligt begyndte jeg at bruge denne tilgang i alle mine hobbyprojekter, fordi det er praktisk og ikke kræver store omkostninger til at skrive et datatilgangslag eller implementere tung infrastruktur. For mig er det som regel tilstrækkeligt at gemme data i litedb eller i en fil.
Men tidligere, da vi med teamet lavede den nu nedlagte opstart EscapeTeams (tænkte jeg, her er pengene — men nej, mere erfaring) — brugte vi Azure Table Storage til datalagring.
Fremtidige planer
Jeg vil gerne løse en af de grundlæggende ulemper ved denne tilgang — horisontal skalering. For det kræves enten distribuerede transaktioner (sic!), eller at man træffer en beslutning om, at de samme data fra forskellige instanser ikke skal ændre sig, eller de må ændre sig efter princippet "den sidste har ret".
Fra et teknisk synspunkt ser jeg en mulig skema:
- Gemme i stedet for en objektmodel EventLog og Snapshot
- Finder andre instanser (tilføje endpoints for alle instanser i indstillingerne? udp discovery? master/slave?)
- Replikere mellem instanser EventLog gennem en hvilken som helst af konsensusalgoritmerne, fx RAFT.
Derudover er der et andet problem, der bekymrer mig — det er kaskadedeletion eller opdagelse af tilfælde af sletning af objekter, som der henvises til fra andre objekter.
Kildekode
Hvis du har læst så langt — er det kun tilbage at læse koden, som du kan finde på GitHub:
Kilde: habr.com
