ObjectRepository — .NET iekŔējā atmiņas krātuves modelis jÅ«su mājas projektiem

Kāpēc uzglabāt visus datus atmiņā?

Kad runa ir par mājaslapas vai aizmugurējās puses datu uzglabāŔanu, vairums saprātÄ«gu cilvēku pirmā izvēle ir SQL datubāze.Ā 

Bet dažreiz prātā ienāk doma, ka datu modelis nav piemērots SQL: piemēram, veidojot meklēŔanu vai sociālo grafiku, nepiecieÅ”ama sarežģītu attiecÄ«bu starp objektiem meklēŔana.Ā 

Visgrūtāk ir strādājot komandā, kad kolēģis nezina, kā veidot ātrus vaicājumus. Cik daudz laika esat pavadījis, risinot N+1 problēmas un veidojot papildus indeksus, lai SELECT galvenajā lapā izpildītos saprātīgā laikā?

Cits populārs pieejas veids ir NoSQL. Pirms dažiem gadiem ap Å”o tēmu bija liela ažiotāža — jebkurā izdevÄ«gā gadÄ«jumā tika izvērsta MongoDB un priecājāmies par atbildēm json dokumentu veidā. (Starpcitu, cik daudz pagaidu risinājumu nācās ievietot dēļ cikliskām atsaucēm dokumentos?).

Es piedāvāju izmēģināt vēl vienu alternatÄ«vu pieeju — kāpēc gan neglabāt visus datus lietojumprogrammas atmiņā, regulāri saglabājot tos nejauŔā krātuvē (fails, attālināta datubāze)?Ā 

Atmiņa ir kļuvusi lēta, un jebkuri iespējamie dati vairumam mazo un vidējo projektu iekļaujas 1 GB atmiņā. (Piemēram, mans iecienÄ«tākais mājas projekts — finanÅ”u izsekotājs, kas veic ikdienas statistiku un manu izdevumu, bilances un darÄ«jumu vēsturi pusotru gadu, patērē tikai 45 MB atmiņas.)

PriekŔrocības:

  • Piekļuve datiem kļūst vienkārŔāka — nav jāuztraucas par vaicājumiem, slinkajām ielādēm, ORM Ä«patnÄ«bām, darbs notiek ar parastajiem C# objektiem;
  • Nav problēmu, kas saistÄ«tas ar piekļuvi no dažādiem pavedieniem;
  • Ä»oti ātri — nav tÄ«kla vaicājumu, nav koda translācijas vaicājumu valodā, nav nepiecieÅ”ama (de)serializācija objektu;
  • Ir pieņemami glabāt datus jebkurā veidā — gan XML diskā, gan SQL Server, gan Azure Table Storage.

Mīnusi:

  • Pazaudēta horizontālā mērogoÅ”ana, lÄ«dz ar to nav iespējams veikt zero downtime deployment;
  • Ja lietojumprogramma avarē — iespējams daļēji zaudēt datus. (Bet mÅ«su lietojumprogramma taču nekad neavarē, vai ne?)

Kā tas darbojas?

Algoritms ir Ŕāds:

  • Sākumā tiek izveidota savienojums ar datu krātuvi, un notiek datu ielāde;
  • Tiek veidota objektu modeļu struktÅ«ra, primārie indeksi un attiecÄ«bu indeksi (1:1, 1:Daudz);
  • Tiek izveidota abonēŔana uz objektu Ä«paŔību izmaiņām (INotifyPropertyChanged) un uz elementu pievienoÅ”anu vai dzēŔanu kolekcijā (INotifyCollectionChanged);
  • Kad abonēŔana tiek aktivizēta, mainÄ«tais objekts tiek pievienots datu veikala ierakstu rindai;
  • Periodiski (pēc taimeri) fona pavedienā tiek saglabātas izmaiņas datu veikalā;
  • Izliekot no programmas, arÄ« tiek saglabātas izmaiņas datu veikalā.

Koda piemērs

Pievienojam nepiecieŔamās atkarības

// ŠžŃŠ½Š¾Š²Š½Š°Ń библиотека
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

Aprakstām datu modeli, kas tiks saglabāts datu veikalā

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

Tad ir objektu modelis:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // 1:Daudz attiecību piemērs
    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;
    }
    
    // PieejamÄ«ba ar indeksu meklēŔanu
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Un visfinally pats datu piekļuves klase - repozitorijs:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // Testiem, ļauj nesaglabāt izmaiņas datu bāzē
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Ja tiek lietots Hangfire un nepiecieŔams saglabāt datu modeli Hangfire datu repozitorijā
        // this.RegisterHangfireScheme(); 
    
        Initialize();
    }
}

Izveidojam ObjectRepository instance:

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

Ja projektā tiks izmantots HangFire

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

Jauna objekta pievienoŔana:

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

Å ajā izsaukumā objekts ParentModel tiek pievienots gan lokālajam keÅ”atmiņai, gan ieraksta rindai datubāzē. Tāpēc Ŕī operācija aizņem O(1), un ar Å”o objektu var uzreiz strādāt.

Piemēram, lai atrastu Å”o objektu repozitorijā un pārliecinātos, ka atgrieztais objekts ir tas pats eksemplārs:

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

Kas notiek Å”ajā gadÄ«jumā? Set<ParentModel>() atgriež TableDictionary<ParentModel>, kas satur iekŔā ConcurrentDictionary<ParentModel, ParentModel> un piedāvā papildu funkcionalitāti primārajiem un sekundārajiem indeksi. Tas ļauj izmantot metodes meklēŔanai pēc Id (vai citiem lietotāja indeksiem) bez visu objektu pilnÄ«gas pārskatīŔanas.

Pievienojot objektus ObjectRepository tiek pievienota paraksts uz to Ä«paŔību izmaiņām, tāpēc jebkura Ä«paŔību izmaiņa arÄ« rezultē objekta pievienoÅ”anā ieraksta rindai.Ā 
Pārveidojot īpaŔības no ārpuses, tas izskatās tāpat kā strādājot ar POCO objektu:

myParent.Children.First().Property = "Atjaunotā vērtība";

Objektu var izdzēst Ŕādos veidos:

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

Å ajā gadÄ«jumā notiek arÄ« objekta pievienoÅ”ana izdzēŔanas rindai.

Kā darbojas saglabāŔana?

ObjectRepository ja izsekojamo objektu izmaiņas (kā pievienoÅ”ana vai izdzēŔana, tāpat kā Ä«paŔību izmaiņas) izsauc notikumu ModelChanged, uz kuru ir parakstÄ«jies IStorage. Realizācijas IStorage notikuma raÅ”anās gadÄ«jumā ModelChanged uzkrāj izmaiņas 3 rindās — pievienoÅ”anai, atjaunināŔanai un izdzēŔanai.

Tāpat realizācijas IStorage savas inicializācijas laikā izveido taimeri, kas ik pēc 5 sekundēm izsauc izmaiņu saglabāŔanu.Ā 

Turklāt pastāv API piespiedu saglabāŔanas izsaukŔanai: ObjectRepository.Save().

Pirms katras saglabāŔanas vispirms notiek bezjēdzÄ«gu operāciju izņemÅ”ana no rindām (piemēram, notikumu dublikāti — kad objekts tika mainÄ«ts divreiz vai ātra pievienoÅ”ana/izdzēŔana), un tikai pēc tam pati saglabāŔana.Ā 

Visos gadījumos tiek saglabāts aktuālais objekts pilnībā, tāpēc var rasties situācija, kad objekti tiek saglabāti citā secībā, nekā mainījās, tajā skaitā var tikt saglabātas jaunākas objektu versijas, nekā brīdī, kad tās tika pievienotas rindai.

Kas vēl ir?

  • Visas bibliotēkas balstās uz .NET Standard 2.0. To var izmantot jebkurā mÅ«sdienu .NET projektā.
  • API ir plÅ«stoÅ”s. IekŔējās kolekcijas ir balstÄ«tas uz ConcurrentDictionary, un notikumu apstrādātājiem ir vai nu bloķēŔana, vai arÄ« tas nav nepiecieÅ”ams.Ā 
    VienÄ«gais, kas jāatceras — pirms lietojuma beigÅ”anas ir jāvērÅ”as pie ObjectRepository.Save();
  • Patvaļīgie indeksi (prasa unikālumu):

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

Kas to lieto?

PersonÄ«gi es sāku izmantot Å”o pieeju visos hobija projektos, jo tas ir ērti un neprasa lielus izdevumus datu piekļuves slāņa rakstīŔanai vai smagas infrastruktÅ«ras izveidoÅ”anai. Man parasti pietiek ar datu glabāŔanu litedb vai failā.Ā 

Bet pagātnē, kad ar komandu veidojām tagad jau neeksistējoÅ”o startapu EscapeTeams (domāju, nu lÅ«k, nauda — bet nē, atkal pieredze) — izmantojām Azure Table Storage datu glabāŔanai.

Nākotnes plāni

Gribu novērst vienu no galvenajiem Ŕī pieejas trÅ«kumiem — horizontālo mērogoÅ”anu. Tam nepiecieÅ”amas vai nu izplatÄ«tas transakcijas (sic!), vai jāpieņem grÅ«ts lēmums, ka vienādi dati no dažādiem instancēm nedrÄ«kst mainÄ«ties, vai rezultātā pieļaut mainīŔanu pēc principa 'kurÅ” pēdējais, tas taisnÄ«ba'.

No tehniskā viedokļa redzu iespējamu sekojoÅ”o shēmu:

  • Glabāt nevis objektu modeli, bet gan EventLog un Snapshot
  • Atrast citas instances (iekļaut visās instance iestatÄ«jumos gala punktus? udp atklāŔana? master/slave?)
  • Repliku veikt starp instancēm ar jebkuru no konsensa algoritmiem, piemēram, RAFT.

Tāpat pastāv vēl viena problēma, kas mani uztrauc — tas ir kaskādes dzēŔana, vai objektu, uz kuriem ir atsauces no citiem objektiem, dzēŔanas gadÄ«jumu noteikÅ”ana.Ā 

Atslēgas kods

Ja jÅ«s esat izlasÄ«jis lÄ«dz Å”im — tad tālāk paliek tikai lasÄ«t kodu, to var atrast GitHub:
https://github.com/DiverOfDark/ObjectRepository

Avots: habr.com

Iegādājieties uzticamu tÄ«mekļa hostingu ar DDoS aizsardzÄ«bu, VPS VDS serveriem šŸ”„ Iegādājieties uzticamu tÄ«mekļa hostingu ar DDoS aizsardzÄ«bu, VPS VDS serveriem | ProHoster