ObjectRepository — .NET minneslager-mönster för dina hembaserade projekt

Varför lagra all data i minnet?

För att lagra webbplatsens eller backenddata skulle de flesta rationella människor först välja en SQL-databas. 

Men ibland dyker tanken upp att datamodellen inte passar för SQL: till exempel, vid byggandet av sökningar eller sociala nätverk behövs sökningar över komplexa relationer mellan objekt. 

Det värsta scenariot är när du arbetar i ett team och en kollega inte kan bygga snabba frågor. Hur mycket tid har du spenderat på att lösa N+1-problem och bygga extra index för att SELECT på huvudsidan ska köras inom rimlig tid?

En annan populär metod är NoSQL. För några år sedan var det en stor hype kring detta ämne - för varje lämplig situation satte man upp MongoDB och var glad över svaren i form av JSON-dokument. (Förresten, hur många hack fick man stoppa in på grund av cykliska referenser i dokumenten?).

Jag föreslår att vi provar ett annat alternativ - varför inte lagra all data i applikationens minne, och periodiskt spara i något fritt lagringsutrymme (fil, fjärrdatabas)? 

Minne har blivit billigt, och alla möjliga data för de flesta små och medelstora projekt får plats i 1 GB minne. (Till exempel, mitt favorit hemarbete - en finansiell spårare, som håller daglig statistik och historia över mina utgifter, saldon och transaktioner under de senaste ett och ett halvt åren, använder bara 45 MB minne.)

Fördelar:

  • Åtkomst till data blir enklare - du behöver inte bekymra dig om frågor, lat inläsning, ORM-specifika egenskaper, arbetet sker med vanliga C#-objekt;
  • Inga problem relaterade till åtkomst från olika trådar;
  • Mycket snabbt - inga nätverksförfrågningar, ingen kodöversättning till frågespråk, ingen (de)serialisering av objekt;
  • Det går bra att lagra data i vilket format som helst - även i XML på disken, i SQL Server eller i Azure Table Storage.

Nackdelar:

  • Det horisontella skalbarheten går förlorad och som en följd kan du inte göra noll-downtime deployment;
  • Om applikationen kraschar kan det delvis leda till dataförlust. (Men vår applikation kraschar ju aldrig, eller hur?)

Hur fungerar det?

Algoritmen är följande:

  • Vid starten etableras en anslutning till lagringsutrymmet och data laddas;
  • En objektmodell byggs, primära index och relationsindex (1:1, 1:Många);
  • Ett abonnemang skapas för att övervaka ändringar av objekts egenskaper (INotifyPropertyChanged) och för att lägga till eller ta bort element i en samling (INotifyCollectionChanged);
  • Vid utlösning av abonnemanget läggs det förändrade objektet i kön för att skrivas till datalagringen;
  • Periodiskt (via en timer) sparas ändringarna bakgrunder i lagringen;
  • Ändringarna sparas också i lagringen när programmet stängs.

Kodexempel

Vi 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 ska sparas 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; }
}

Därefter objektmodellen:

public class ParentModel : ModelBase
{
    public ParentModel(ParentEntity entity)
    {
        Entity = entity;
    }
    
    public ParentModel()
    {
        Entity = new ParentEntity(Guid.NewGuid());
    }
    
    public Guid? NullableId => null;
    
    // Exempel på relation 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);
    }
    
    // Åtkomst med indexsökning
    public ParentModel Parent => Single(ParentId);
    
    protected override BaseEntity Entity => _childEntity;
}

Och slutligen själva klassrepositoryt för datatillgång:

public class MyObjectRepository : ObjectRepositoryBase
{
    public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance)
    {
        IsReadOnly = true; // För tester, tillåter att inte spara ändringar i databasen
    
        AddType((ParentEntity x) => new ParentModel(x));
        AddType((ChildEntity x) => new ChildModel(x));
    
        // Om Hangfire används och datamodellen ska lagras för Hangfire i ObjectRepository
        // this.RegisterHangfireScheme();
    
        Initialize();
    }
}

Skapa en instans av ObjectRepository:

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

Om projektet ska använda HangFire

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

Infogning av ett nytt objekt:

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

Vid detta anrop är objektet ParentModel 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 man kan omedelbart arbeta med detta objekt.

Till exempel, för att hitta detta objekt i lagret och säkerställa att det returnerade objektet är samma instans:

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

Vad händer då? Set() återvänder TableDictionary, som innehåller ConcurrentDictionary och erbjuder ytterligare funktionalitet för primära och sekundära index. Detta möjliggör metoder för att söka efter Id (eller andra godtyckliga användardefinierade index) utan att behöva genomsöka alla objekt.

Vid tillägg av objekt till ObjectRepository lägger man till en prenumeration på ändringar av deras egenskaper, så alla ändringar av egenskaper resulterar också i att detta objekt läggs till i kön för att skrivas. 
Uppdatering av egenskaper utifrån ser likadant ut som arbete med POCO-objekt:

myParent.Children.First().Property = "Uppdaterat värde";

Ett objekt kan tas bort på följande sätt:

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

Samtidigt läggs objektet även till i kön för borttagning.

Hur fungerar sparandet?

ObjectRepository Vid ändring av övervakade objekt (både tillägg eller borttagning, samt ändring av egenskaper) utlöses en händelse ModelChanged, som är prenumererad på IStorage. Implementeringar IStorage vid händelseutlösning ModelChanged lagrar ändringarna i 3 köer - för tillägg, för uppdatering och för borttagning.

Implementeringar IStorage skapar dessutom en timer vid initialisering som var 5:e sekund utlöser sparande av ändringar. 

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

Före varje sparande rensas först köerna från meningslösa operationer (till exempel dubbletter av händelser - när objektet ändrades två gånger eller snabb tillägg/borttagning av objekt), och först därefter sker själva sparandet. 

I alla fall sparas hela det aktuella objektet, så det kan hända att objekten sparas i en annan ordning än de ändrades, inklusive att nyare versioner av objekt kan sparas än vid tidpunkten för tillägget till kön.

Vad finns det mer?

  • Alla bibliotek baseras på .NET Standard 2.0. Kan användas i vilket modernt .NET-projekt som helst.
  • API är trådsäkert. De interna samlingarna är baserade på ConcurrentDictionary, händelsehanterare har antingen lås eller behöver inga. 
    Det enda att komma ihåg är att vid avslutning av applikationen anropa ObjectRepository.Save();
  • Belbit franser (kräver unika värden):

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

Vem använder detta?

Personligen började jag använda detta tillvägagångssätt i alla mina hobbyprojekt eftersom det är praktiskt och inte kräver stora investeringar för att skriva ett databasåtkomstlager eller sätta upp en tung infrastruktur. För mig räcker det oftast med att lagra data i litedb eller en fil. 

Men tidigare, när jag tillsammans med teamet skapade det numera nedlagda startup-företaget EscapeTeams (trodde att här är pengarna — men nej, bara erfarenhet) — använde vi Azure Table Storage för datalagring.

Framtidsplaner

Jag vill åtgärda en av de grundläggande nackdelarna med detta tillvägagångssätt — horisontell skalning. För detta krävs antingen distribuerade transaktioner (sic!), eller ett beslut att samma data från olika instanser inte ska ändras, eller så får de ändras enligt principen "den som är sist — den som har rätt".

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

  • Lagras i stället för objektmodellen EventLog och Snapshot
  • Hitta andra instanser (lägga till alla instansers slutpunkter i inställningarna? udp-upptäckts? master/slave?)
  • Replikera mellan instanser EventLog via någon av konsensusalgoritmerna, till exempel RAFT.

Det finns också ett annat problem som stör mig — det kaskaddelete, eller att upptäcka fall där objekt som har referenser från andra objekt tas bort. 

Källkod

Om du har läst hit så återstår det bara att läsa koden, som du kan hitta på GitHub:
https://github.com/DiverOfDark/ObjectRepository

Källa: habr.com

Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster