ObjectRepository – .NET-i mälusiseste hoidlate muster teie koduprojektide jaoks

Miks salvestada kõik andmed mällu?

Veebisaidi või taustaandmete salvestamiseks on enamiku mõistlike inimeste esimene soov valida SQL-andmebaas. 

Vahel aga tuleb pähe mõte, et andmemudel ei sobi SQL-ile: näiteks otsingu- või sotsiaalgraafiku koostamisel tuleb otsida keerulisi seoseid objektide vahel. 

Kõige hullem on olukord, kui töötad meeskonnas ja kolleeg ei oska kiireid päringuid koostada. Kui palju aega kulutasite N+1 ülesande lahendamisele ja lisaindeksite loomisele, et pealehel olev SELECT mõistliku aja jooksul valmis saaks?

Teine populaarne lähenemisviis on NoSQL. Mitu aastat tagasi oli selle teema ümber palju kära – igaks mugavaks juhuks juurutasid nad MongoDB ja olid rahul vastustega json-dokumentide kujul. (muide, mitu karku pidite sisestama dokumentides olevate ringikujuliste linkide tõttu?).

Soovitan proovida teist alternatiivset meetodit – miks mitte proovida salvestada kõik andmed rakenduse mällu, salvestades neid perioodiliselt juhuslikku salvestusruumi (fail, kaugandmebaas)? 

Mälu on muutunud odavaks ja kõik võimalikud andmed enamiku väikeste ja keskmise suurusega projektide jaoks mahuvad 1 GB mälusse. (Näiteks minu lemmik koduprojekt on finantsjälgija, mis hoiab igapäevast statistikat ja minu kulude, saldode ja tehingute ajalugu poolteist aastat, kulutab vaid 45 MB mälu.)

plussid:

  • Juurdepääs andmetele muutub lihtsamaks – te ei pea muretsema päringute, laisa laadimise, ORM-i funktsioonide pärast, töötate tavaliste C# objektidega;
  • Erinevatest lõimedest juurdepääsuga ei kaasne probleeme;
  • Väga kiire – ei mingeid võrgupäringuid, ei tõlgi koodi päringukeelde, pole vaja objekte (de)serialiseerida;
  • Andmete salvestamine on vastuvõetav mis tahes kujul – olgu see siis XML-vormingus kettal või SQL Serveris või Azure'i tabelisalvestuses.

miinuseid:

  • Horisontaalne skaleerimine on kadunud ja selle tulemusel ei saa nullseisaku kasutuselevõttu teha;
  • Kui rakendus jookseb kokku, võite andmed osaliselt kaotada. (Aga meie rakendus ei jookse kunagi kokku, eks?)

Kuidas see toimib?

Algoritm on järgmine:

  • Alguses luuakse andmesalvestusega ühendus ja andmed laaditakse;
  • Ehitatakse objektimudel, esmased indeksid ja relatsiooniindeksid (1:1, 1:Paljud);
  • Tellimus luuakse objekti omaduste muutmiseks (INotifyPropertyChanged) ja kogusse elementide lisamiseks või eemaldamiseks (INotifyCollectionChanged);
  • Tellimuse käivitumisel lisatakse muudetud objekt andmemälule kirjutamise järjekorda;
  • Salvestusruumi muudatused salvestatakse perioodiliselt (taimerile) taustalõime;
  • Kui rakendusest väljute, salvestatakse muudatused ka salvestusruumi.

Koodi näide

Vajalike sõltuvuste lisamine

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

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;
    
    // Пример связи 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;
}

Ja lõpuks, hoidla klass ise andmetele juurdepääsuks:

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

Loo ObjectRepository eksemplar:

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

Kui projekt kasutab HangFire'i

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

Uue objekti sisestamine:

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

Selle kõnega objekt ParentModel lisatakse nii kohalikku vahemällu kui ka andmebaasi kirjutamise järjekorda. Seetõttu võtab see operatsioon O(1) ja selle objektiga saab kohe töötada.

Näiteks selle objekti leidmiseks hoidlast ja kontrollimiseks, kas tagastatud objekt on sama eksemplar:

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

Mis toimub? Määra () naaseb Tabelsõnastik, mis sisaldab ConcurrentDictionary ning pakub primaarsete ja sekundaarsete indeksite lisafunktsioone. See võimaldab teil kasutada meetodeid ID (või muude suvaliste kasutajaindeksite) järgi otsimiseks ilma kõiki objekte täielikult itereerimata.

Objektide lisamisel Objektihoidla nende atribuutide muutmiseks lisatakse tellimus, nii et iga atribuutide muudatus toob kaasa ka selle objekti lisamise kirjutusjärjekorda. 
Atribuutide värskendamine väljastpoolt näeb välja sama, mis POCO objektiga töötamine:

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

Objekti saab kustutada järgmistel viisidel.

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

See lisab objekti ka kustutamisjärjekorda.

Kuidas salvestamine käib?

Objektihoidla kui jälgitavad objektid muutuvad (kas lisades või kustutades või omaduste muutmisel), tekitab sündmuse Mudel muudetudtellitud ISaldusruum. Rakendused ISaldusruum kui sündmus toimub Mudel muudetud muudatused pannakse 3 järjekorda - lisamiseks, värskendamiseks ja kustutamiseks.

Samuti teostused ISaldusruum initsialiseerimisel loovad nad taimeri, mis paneb muudatused salvestama iga 5 sekundi järel. 

Lisaks on salvestamiskõne sundimiseks API: ObjectRepository.Save().

Enne iga salvestamist eemaldatakse järjekordadest esmalt mõttetud toimingud (näiteks dubleerivad sündmused – kui objekti muudeti kaks korda või lisati/eemaldati objekte kiiresti) ja alles seejärel salvestamine ise. 

Kõikidel juhtudel salvestatakse kogu praegune objekt, mistõttu on võimalik, et objektid salvestatakse teises järjekorras kui neid muudeti, sealhulgas objektide uuemad versioonid kui nende järjekorda lisamise ajal.

Mis seal veel on?

  • Kõik teegid põhinevad .NET Standard 2.0-l. Saab kasutada igas kaasaegses .NET projektis.
  • API on lõimekindel. Sisekogusid rakendatakse lähtuvalt ConcurrentDictionary, sündmuste käitlejatel on lukud või nad ei vaja neid. 
    Ainus asi, mida tasub meeles pidada, on helistada ObjectRepository.Save();
  • Suvalised indeksid (nõuavad kordumatust):

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 hobiprojektides, kuna see on mugav ja ei nõua suuri kulutusi andmete juurdepääsukihi kirjutamiseks või raske infrastruktuuri juurutamiseks. Mulle isiklikult piisab tavaliselt andmete salvestamisest litedb-s või failis. 

Kuid varem, kui nüüdseks tegevuse lõpetanud idufirma EscapeTeams (Arvasin, et siin see on, raha – aga ei, jälle kogemus) – kasutatakse andmete salvestamiseks Azure Table Storage'is.

Tuleviku plaanid

Tahaksin parandada selle lähenemisviisi ühe peamise puuduse - horisontaalse skaleerimise. Selleks on vaja kas hajutatud tehinguid (sic!) või teha tugeva tahtega otsus, et samad andmed erinevatest eksemplaridest ei tohiks muutuda, või lasta neil muutuda põhimõttel “kellel on õigus, sellel on viimane”.

Tehnilisest vaatenurgast näen võimalikuna järgmist skeemi:

  • Objektimudeli asemel salvestage EventLog ja Snapshot
  • Otsige teisi eksemplare (lisage seadetesse kõigi eksemplaride lõpp-punktid? udp discovery? master/slave?)
  • Kopeerige EventLogi eksemplaride vahel mis tahes konsensusalgoritmi (nt RAFT) abil.

Murelikuks teeb ka teine ​​probleem – kaskaadkustutamine ehk selliste objektide kustutamise juhtumite tuvastamine, millel on linke teistele objektidele. 

Lähtekood

Kui olete lugenud lõpuni siia, siis jääb üle vaid kood lugeda; selle leiate GitHubist:
https://github.com/DiverOfDark/ObjectRepository

Allikas: www.habr.com

Lisa kommentaar