ObjectRepository - .NET in-memory repository mönster för dina hemprojekt

Varför lagra all data i minnet?

För att lagra webbplats- eller backend-data kommer de flesta vettiga människors första önskan att välja en SQL-databas. 

Men ibland kommer tanken att tänka på att datamodellen inte är lämplig för SQL: till exempel när du bygger en sökning eller social graf måste du söka efter komplexa relationer mellan objekt. 

Den värsta situationen är när du arbetar i ett team och en kollega inte vet hur man bygger snabba frågor. Hur mycket tid lade du ner på att lösa N+1-problem och bygga ytterligare index så att SELECT på huvudsidan skulle slutföras inom rimlig tid?

En annan populär metod är NoSQL. För flera år sedan var det mycket hype kring detta ämne - för alla lämpliga tillfällen distribuerade de MongoDB och var nöjda med svaren i form av json-dokument (förresten, hur många kryckor var du tvungen att sätta in på grund av de cirkulära länkarna i dokumenten?).

Jag föreslår att du provar en annan, alternativ metod - varför inte försöka lagra all data i applikationsminnet och regelbundet spara den till slumpmässig lagring (fil, fjärrdatabas)? 

Minnet har blivit billigt och all möjlig data för de flesta små och medelstora projekt kommer att passa in i 1 GB minne. (Till exempel är mitt favorithemprojekt finansiell spårare, som sparar daglig statistik och historik över mina utgifter, saldon och transaktioner i ett och ett halvt år, förbrukar endast 45 MB minne.)

Fördelar:

  • Tillgång till data blir enklare - du behöver inte oroa dig för frågor, lat laddning, ORM-funktioner, du arbetar med vanliga C#-objekt;
  • Det finns inga problem förknippade med åtkomst från olika trådar;
  • Mycket snabbt - inga nätverksbegäranden, ingen översättning av kod till ett frågespråk, inget behov av (av)serialisering av objekt;
  • Det är acceptabelt att lagra data i vilken form som helst - vare sig det är i XML på disk, eller i SQL Server eller i Azure Table Storage.

Nackdelar:

  • Horisontell skalning går förlorad, och som ett resultat kan ingen driftsättning göras utan driftstopp;
  • Om programmet kraschar kan du delvis förlora data. (Men vår applikation kraschar aldrig, eller hur?)

Hur fungerar det?

Algoritmen är följande:

  • Vid starten upprättas en anslutning med datalagringen och data laddas;
  • En objektmodell, primära index och relationsindex (1:1, 1:Många) byggs;
  • En prenumeration skapas för ändringar i objektegenskaper (INotifyPropertyChanged) och för att lägga till eller ta bort element till samlingen (INotifyCollectionChanged);
  • När prenumerationen utlöses läggs det ändrade objektet till i kön för skrivning till datalagringen;
  • Ändringar av lagringen sparas med jämna mellanrum (på en timer) i en bakgrundstråd;
  • När du avslutar programmet sparas även ändringar i lagringen.

Kodexempel

Lägger 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

Vi beskriver datamodellen som kommer att lagras 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;
    
    // Пример связи 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;
}

Och slutligen, själva förvarsklassen för åtkomst till data:

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

Skapa en ObjectRepository-instans:

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

Om projektet kommer att använda HangFire

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

Infoga ett nytt objekt:

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

Med detta anrop, objektet Föräldramodell läggs till både i den lokala cachen och i kön för att skriva till databasen. Därför tar denna operation O(1), och detta objekt kan arbetas med omedelbart.

Till exempel, för att hitta det här objektet i arkivet och verifiera att det returnerade objektet är samma instans:

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

Vad händer? Uppsättning () returnerar Tabellordbok, vilket innehåller ConcurrentDictionary och ger ytterligare funktionalitet för primära och sekundära index. Detta gör att du kan ha metoder för att söka efter Id (eller andra godtyckliga användarindex) utan att fullständigt iterera över alla objekt.

När du lägger till objekt till ObjectRepository en prenumeration läggs till för att ändra deras egenskaper, så varje ändring av egenskaper resulterar också i att detta objekt läggs till i skrivkön. 
Att uppdatera egenskaper från utsidan ser likadant ut som att arbeta med ett POCO-objekt:

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

Du kan ta bort ett objekt på följande sätt:

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

Detta lägger också till objektet i raderingskön.

Hur fungerar sparandet?

ObjectRepository när övervakade objekt ändras (antingen lägger till eller tar bort eller ändrar egenskaper), höjer en händelse ModellÄndradprenumererade på ISlagring. Genomföranden ISlagring när en händelse inträffar ModellÄndrad ändringar läggs i 3 köer - för att lägga till, för att uppdatera och för att radera.

Även implementeringar ISlagring vid initiering skapar de en timer som gör att ändringarna sparas var 5:e sekund. 

Dessutom finns det ett API för att tvinga fram ett sparsamtal: ObjectRepository.Save().

Före varje sparning tas först meningslösa operationer bort från köerna (till exempel dubbletter av händelser - när ett objekt ändrades två gånger eller snabbt lades till/borttagna objekt), och först sedan själva sparandet. 

I samtliga fall sparas hela det aktuella objektet, så det är möjligt att objekt sparas i en annan ordning än de ändrades, inklusive nyare versioner av objekt än när de lades till i kön.

Vad finns det mer?

  • Alla bibliotek är baserade på .NET Standard 2.0. Kan användas i alla moderna .NET-projekt.
  • API:t är trådsäkert. Interna insamlingar genomförs utifrån ConcurrentDictionary, händelsehanterare har antingen lås eller behöver dem inte. 
    Det enda värt att komma ihåg är att ringa ObjectRepository.Save();
  • Godtyckliga index (kräver unikhet):

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

Vem använder det?

Personligen började jag använda detta tillvägagångssätt i alla hobbyprojekt eftersom det är bekvämt och inte kräver stora utgifter för att skriva ett dataåtkomstlager eller distribuera tung infrastruktur. Personligen räcker det för mig att lagra data i litedb eller en fil. 

Men tidigare, när den nu nedlagda startupen EscapeTeams (Jag trodde här är det, pengar - men nej, upplevelse igen) - används för att lagra data i Azure Table Storage.

Planer för framtiden

Jag skulle vilja fixa en av de största nackdelarna med detta tillvägagångssätt - horisontell skalning. För att göra detta behöver du antingen distribuerade transaktioner (sic!), eller fatta ett starkt beslut om att samma data från olika instanser inte ska ändras, eller låta dem ändras enligt principen "vem som är sist har rätt."

Ur teknisk synvinkel ser jag följande schema som möjligt:

  • Lagra EventLog och Snapshot istället för objektmodell
  • Hitta andra instanser (lägg till slutpunkter för alla instanser i inställningarna? UDP-upptäckt? Master/slav?)
  • Replikera mellan EventLog-instanser via valfri konsensusalgoritm, såsom RAFT.

Det finns också ett annat problem som oroar mig - kaskadradering, eller upptäckt av fall av radering av objekt som har länkar från andra objekt. 

Källkod

Om du har läst hela vägen hit så återstår bara att läsa koden; den finns på GitHub:
https://github.com/DiverOfDark/ObjectRepository

Källa: will.com

Lägg en kommentar