ObjectRepository — .NET mäluandmehoidja mudel teie koduste projektide jaoks

Miks hoida kõiki andmeid mälus?

Veebisaidi või serveri andmete salvestamiseks valib enamik mõistlikke inimesi kõigepealt SQL andmebaasi. 

Kuid mõnikord tuleb mõte, et andmemudel ei sobi SQL-iga: näiteks, kui ehitada otsingut või sotsiaalset graafi, on vaja otsida keeruliste seoste kaudu objektide vahel. 

Halvimal juhul juhtub, et töötate meeskonnas ja kolleeg ei oska ehitada kiireid päringuid. Kui palju aega olete kulutanud N+1 probleemide lahendamiseks ja täiendavate indeksite loomiseks, et SELECT pealdival tööle hakkaks mõistliku ajaga?

Teine populaarne lähenemine on NoSQL. Mõned aastad tagasi oli selle teema ümber suur müra — igaks juhuks käivitati MongoDB ja rõõmustati JSON-dokumentide vastuste üle. (Muide, kui palju tugitooteid tuli tsükliliste viidete tõttu dokumentidesse lisada?).

Pakun proovida veel üht alternatiivset viisi — miks mitte hoida kõiki andmeid rakenduse mälus, perioodiliselt salvestades need juhuslikku hoidlisse (fail, kaugandmebaas)? 

Mälu on muutunud odavaks ja enamik väikeste ja keskmiste projektide andmeid mahub 1 GB mällu. (Näiteks, minu lemmik koduprojekt — finantsijälgija, mis hoiab päevast statistikat ja ajalugu minu kulutuste, saldode ja tehingute kohta poolteise aasta jooksul, tarbib vaid 45 MB mälu.)

Plussid:

  • Andmetele juurdepääs on lihtsam — ei ole vaja muretseda päringute, laiskade laadimiste, ORM-i omaduste üle, töö toimub tavaliste C# objektidega;
  • Ei ole probleeme, mis on seotud juurdepääsuga erinevatest niitidest;
  • Väga kiire — ei mingeid võrgupäringuid, puudub koodi tõlkimine päringukeelde, ei ole vaja (de)serialiseerida objekte;
  • Lubatud on hoida andmeid igas vormis — olgu need XML diskil, SQL Serveris või Azure Table Storage'is.

Miinused:

  • Kaduda võib horisontaalne skaleeritavus ja seega ei saa teha nullkatkestust juurutust;
  • Kui rakendus kukub, võib andmeid osaliselt kaduda. (Aga meie rakendus ju ei kuku kunagi, eks?)

Kuidas see töötab?

Algoritm on järgmine:

  • Alguses luuakse ühendus andmehoidla, ja toimub andmete laadimine;
  • Ehitatakse objekti mudel, esmased indeksid ja seoseindeksid (1:1, 1:palju);
  • Loodakse tellimus objektide omaduste (INotifyPropertyChanged) ja elementide lisamise või eemaldamise kohta kogus (INotifyCollectionChanged);
  • Tellimuse aktiveerimisel lisatakse muudetud objekt andmehoidla kirjutamise järjekorda;
  • Perioodiliselt (ajastiga) salvestatakse taustprotsessis muudatused andmehoidlas;
  • Rakendusest lahkumisel salvestatakse samuti muudatused andmehoidlas.

Koodinäide

Lisame vajalikud sõltuvused

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

Kirjeldame andmemudelit, mis salvestatakse andmehoidlas

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

Seejärel objekti mudel:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Näide seosest 1:Many
    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
    }
    
    // Juhtimine indeksi põhjal
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Ja lõpuks, klass-repositorium andmete ligipääsuks:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Testimiseks, võimaldab mitte salvestada muudatusi andmebaasi
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Kui kasutatakse Hangfire'i ja on vaja salvestada andmemudelit Hangfire'i jaoks ObjectRepository's
        // this.RegisterHangfireScheme();  
    
        Initialize();
    }
}

Loome ObjectRepository eksemplari:

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

Kui projektis kasutatakse HangFire'i

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

Uue objekti lisamine:

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

Selle kutsumise korral objekt ParentModel lisatakse nii kohaliku vahemälu kui ka kirjutamise järjekorda andmebaasi. Seetõttu võtab see operatsioon aega O(1) ja selle objekti kallal saab kohe töötada.

Näiteks, et leida see objekt repository's ja kinnitada, et tagastatud objekt on sama eksemplar:

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

Mida sel ajal toimub? Set() toob tagasi TableDictionary, mis sisaldab endas ConcurrentDictionary ja pakub lisafunktsioone esmase ja teisejärgulise indekseerimise jaoks. See võimaldab otsimise meetodeid Id (või muude suvaliste kasutajaindeksite) alusel ilma kõigi objektide täieliku läbivaatamiseta.

Objektide lisamisel ObjectRepository lisatakse tellimus nende omaduste muutmise jaoks, seega toob iga omaduste muutmine kaasa selle objekti lisamise kirjutamise järjekorda. 
Omaduste värskendamine väljastpoolt näeb välja sarnaselt POCO-objektiga töötamisega:

myParent.Children.First().Property = "Uuendatud väärtus";

Objekti saab eemaldada järgmistel viisidel:

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

Sellega toimub ka objekti lisamine kustutamise järjekorda.

Kuidas salvestamine töötab?

ObjectRepository kui jälgitavate objektide muutmine (kas lisamine, eemaldamine või omaduste muutmine) kutsub esile sündmuse ModelChanged, millele on tellinud IStorage. Teostused IStorage selle sündmuse toimumisel ModelChanged koguvad muudatused 3 järjekorda – lisamine, uuendamine ja eemaldamine.

Samuti loovad teostused algatamisel taimeri, mis iga 5 sekundi järel kutsub esile muudatuste salvestamise. IStorage Lisaks on olemas API sundsalvestamiseks: 

ObjectRepository.Save() Enne iga salvestamist eemaldatakse kõigepealt järjekordadest mõttetud operatsioonid (näiteks sündmuste dubleerimine — kui objekti muudeti kaks korda või objekti kiire lisamine/eemaldamine) ja alles seejärel toimub salvestamine..

Kõigis juhtudel salvestatakse aktuaalne objekt täielikult, seega võib juhtuda olukord, kus objekte salvestatakse teises järjestuses kui need muutusid, sealhulgas võivad salvestuda uuemad versioonid objektidest kui need lisati järjekorda. 

Mis veel on olemas?

Kõik raamatukogud põhinevad .NET Standard 2.0-l. Saab kasutada mis tahes kaasaegses .NET projektis.

  • Kõik raamatukogud põhinevad .NET Standard 2.0-l. Saab kasutada mis tahes kaasaegses .NET projektis.
  • API on veelkasutatav. Sised kolektsioonid on rakendatud ConcurrentDictionary, sündmuste käsitlejad kas omavad lukustusi või ei vaja neid. 
    Ainus, mille üle tasub meeles pidada, on see, et rakenduse lõpetamisel tuleks kutsuda ObjectRepository.Save();
  • Meelevaldne indeks (nõuab unikaalsust):

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

Kes seda kasutab?

Isiklikult hakkasin seda lähenemist kasutama kõigis oma hobi projektides, sest see on mugav ja ei nõua suurt vaeva andmejuurdepääsu kihi kirjutamisel või raske infrastruktuuri käivitamisel. Üldiselt piisab mulle andmete salvestamisest litedb-s või failis. 

Kuid minevikus, kui tegime meeskonnaga praegu kustutatud idufirma EscapeTeams (mõtlesin, et nüüd on raha — aga ei, jälle kogemus) — kasutasime andmete salvestamiseks Azure Table Storage'i.

Tulevikuplaanid

Tahaksin parandada ühe selle lähenemise peamise puuduse — horisontaalne skaleerimine. Selleks on vaja kas jaotatud tehinguid (sic!), või teha otsus, et samad andmed erinevates instantsides ei tohi muutuda, või lasta neil muutuda „kes viimane, see parem” põhimõtte kohaselt.

Tehnilisest vaatepunktist näen võimalikku järgmist skeemi:

  • Salvestada objektide mudeli asemel EventLog ja Snapshot
  • Leida teised instantsid (lisada seadistustesse kõigi instantside lõpp-punktid? udp avastamine? master/slave?)
  • Replitseerida instantside vahel EventLog'i läbi mõne konsensuse algoritmi, näiteks RAFT.

Samuti on olemas veel üks probleem, mis mind häirib — see on kaskaadne kustutamine või avastamine olukordadest, kus objektide kustutamist, millele viidatakse teistest objektidest. 

Allika kood

Kui lugesite siia, siis edasi jääb vaid koodi lugemine, seda saab leida GitHubist:
https://github.com/DiverOfDark/ObjectRepository

Allikas: habr.com

Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster