Warum alle Daten im Speicher speichern?
Für das Speichern von Site- oder Backend-Daten ist der erste Wunsch der meisten gesunden Menschen eine SQL-Datenbank.
Manchmal kommt man jedoch auf die Idee, dass das Datenmodell nicht für SQL geeignet ist: Wenn Sie beispielsweise eine Suche oder ein soziales Diagramm erstellen, müssen Sie nach komplexen Beziehungen zwischen Objekten suchen.
Die schlimmste Situation ist, wenn Sie in einem Team arbeiten und ein Kollege keine schnellen Abfragen erstellen kann. Wie viel Zeit haben Sie damit verbracht, N + 1-Probleme zu lösen und zusätzliche Indizes zu erstellen, damit SELECT auf der Hauptseite in angemessener Zeit funktioniert?
Ein weiterer beliebter Ansatz ist NoSQL. Vor einigen Jahren gab es einen großen Hype um dieses Thema - für jede Gelegenheit haben wir MongoDB bereitgestellt und die Antworten in Form von JSON-Dokumenten genossen (übrigens, wie viele Krücken mussten aufgrund von Zirkelverknüpfungen in den Dokumenten eingefügt werden?) .
Warum nicht versuchen, alle Daten im Speicher der Anwendung zu speichern und sie regelmäßig in einem beliebigen Speicher (Datei, entfernte Datenbank) zu speichern?
Der Speicher ist billig geworden, und alle möglichen Daten aus den meisten kleinen und mittleren Projekten passen in 1 GB Speicher. (Zum Beispiel verbraucht mein Lieblingsprojekt zu Hause - ein Finanz-Tracker , der anderthalb Jahre lang tägliche Statistiken und einen Verlauf meiner Ausgaben, Salden und Transaktionen speichert - nur 45 MB Speicher.)
Vorteile:
- Der Zugriff auf Daten wird immer einfacher - Sie müssen sich nicht um Abfragen, verzögertes Laden, ORM-Funktionen und die Arbeit mit normalen C # -Objekten kümmern.
- Mit dem Zugriff von verschiedenen Threads sind keine Probleme verbunden.
- Sehr schnell - keine Netzwerkanforderungen, keine Codeübersetzung in die Abfragesprache, keine (De-) Serialisierung von Objekten;
- Es ist zulässig, Daten in beliebiger Form zu speichern - zumindest in XML auf der Festplatte, mindestens in SQL Server, mindestens in Azure Table Storage.
Nachteile:
- Die horizontale Skalierung geht verloren. Daher können Sie keine Bereitstellung ohne Ausfallzeiten durchführen.
- Wenn die Anwendung abstürzt, können Sie Daten teilweise verlieren. (Aber unsere Anwendung stürzt nie ab, oder?)
Wie funktioniert es
Der Algorithmus ist wie folgt:
- Zu Beginn wird eine Verbindung zum Data Warehouse hergestellt und Daten werden heruntergeladen.
- Ein Objektmodell, Primärindizes und Beziehungsindizes (1: 1, 1: Viele) werden erstellt.
- Es wird ein Abonnement zum Ändern der Eigenschaften von Objekten (INotifyPropertyChanged) und zum Hinzufügen oder Entfernen von Elementen zur Auflistung (INotifyCollectionChanged) erstellt.
- Wenn das Abonnement ausgelöst wird, wird das geänderte Objekt der Warteschlange zum Schreiben in das Data Warehouse hinzugefügt.
- In regelmäßigen Abständen (nach Zeitgeber) werden Änderungen am Speicher im Hintergrundstrom gespeichert.
- Wenn Sie die Anwendung beenden, werden auch Änderungen am Repository gespeichert.
Codebeispiel
Fügen Sie die erforderlichen Abhängigkeiten hinzu Wir beschreiben das Datenmodell, das im Repository gespeichert wird 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; } }
Dann das Objektmodell: public class ParentModel : ModelBase { public ParentModel(ParentEntity entity) { Entity = entity; } public ParentModel() { Entity = new ParentEntity(Guid.NewGuid()); }
Und schließlich die Repository-Klasse selbst für den Zugriff auf Daten: public class MyObjectRepository : ObjectRepositoryBase { public MyObjectRepository(IStorage storage) : base(storage, NullLogger.Instance) { IsReadOnly = true;
Erstellen Sie eine Instanz von ObjectRepository:
var memory = new MemoryStream(); var db = new LiteDatabase(memory); var dbStorage = new LiteDbStorage(db); var repository = new MyObjectRepository(dbStorage); await repository.WaitForInitialize();
Wenn das Projekt HangFire verwendet public void ConfigureServices(IServiceCollection services, ObjectRepository objectRepository) { services.AddHangfire(s => s.UseHangfireStorage(objectRepository)); }
Neues Objekt einfügen:
var newParent = new ParentModel() repository.Add(newParent);
Bei diesem Aufruf wird das ParentModel- Objekt sowohl dem lokalen Cache als auch der Schreibwarteschlange zur Datenbank hinzugefügt. Daher benötigt diese Operation O (1) und Sie können sofort mit diesem Objekt arbeiten.
So finden Sie dieses Objekt beispielsweise im Repository und stellen sicher, dass das zurückgegebene Objekt dieselbe Instanz ist:
var parents = repository.Set<ParentModel>(); var myParent = parents.Find(newParent.Id); Assert.IsTrue(ReferenceEquals(myParent, newParent));
Was passiert damit? Set <ParentModel> () gibt ein TableDictionary <ParentModel> zurück , das ConcurrentDictionary <ParentModel, ParentModel> enthält und zusätzliche Funktionen für Primär- und Sekundärindizes bietet. Auf diese Weise können Sie Methoden zum Suchen nach ID (oder anderen beliebigen benutzerdefinierten Indizes) verwenden, ohne alle Objekte vollständig aufzulisten.
Wenn Objekte zum ObjectRepository hinzugefügt werden, wird ein Abonnement hinzugefügt, um ihre Eigenschaften zu ändern. Bei jeder Änderung der Eigenschaften wird dieses Objekt auch zur Schreibwarteschlange hinzugefügt.
Das Aktualisieren von Eigenschaften von außen sieht genauso aus wie das Arbeiten mit einem POCO-Objekt:
myParent.Children.First().Property = "Updated value";
Sie können ein Objekt folgendermaßen löschen:
repository.Remove(myParent); repository.RemoveRange(otherParents); repository.Remove<ParentModel>(x => !x.Children.Any());
Dadurch wird das Objekt auch zur Löschwarteschlange hinzugefügt.
Wie funktioniert Naturschutz?
ObjectRepository beim Ändern von verfolgten Objekten (sowohl Hinzufügen oder Entfernen als auch Ändern von Eigenschaften) löst das ModelChanged- Ereignis aus, das IStorage abonniert hat . Implementierungen von IStorage fassen beim Auftreten eines ModelChanged- Ereignisses die Änderungen in drei Warteschlangen zusammen: Hinzufügen, Aktualisieren und Löschen.
Außerdem erstellen IStorage- Implementierungen während der Initialisierung einen Timer, der alle 5 Sekunden bewirkt, dass die Änderungen gespeichert werden.
Darüber hinaus gibt es eine API, um einen Speicheraufruf zu erzwingen: ObjectRepository.Save () .
Vor jedem Speichern werden zuerst bedeutungslose Vorgänge aus den Warteschlangen entfernt (z. B. doppelte Ereignisse - wenn ein Objekt zweimal geändert wurde oder Objekte schnell hinzugefügt / entfernt werden) und erst dann das Speichern selbst.
In allen Fällen wird das gesamte Objekt gespeichert, sodass die Objekte möglicherweise in einer anderen Reihenfolge als geändert gespeichert werden, einschließlich neuerer Versionen der Objekte als zum Zeitpunkt des Hinzufügens zur Warteschlange.
Was gibt es sonst noch?
- Alle Bibliotheken basieren auf .NET Standard 2.0. Es kann in jedem modernen .NET-Projekt verwendet werden.
- Die API ist threadsicher. Interne Sammlungen basieren auf ConcurrentDictionary . Ereignishandler verfügen entweder über Sperren oder benötigen diese nicht.
Das einzige, woran Sie sich erinnern sollten, ist, ObjectRepository.Save () aufzurufen. - Benutzerdefinierte Indizes (erfordern Eindeutigkeit):
repository.Set<ChildModel>().AddIndex(x => x.Value); repository.Set<ChildModel>().Find(x => x.Value, "myValue");
Wer benutzt es?
Persönlich habe ich begonnen, diesen Ansatz in allen Hobbyprojekten zu verwenden, da er praktisch ist und keine großen Kosten für das Schreiben einer Datenzugriffsschicht oder die Bereitstellung einer umfangreichen Infrastruktur erfordert. Persönlich habe ich normalerweise genug Datenspeicher in litedb oder in einer Datei.
Aber in der Vergangenheit, als EscapeTeams, das späte Startup, mit dem Team durchgeführt wurde (sie dachten, sie wären Geld - aber nein, noch einmal Erfahrung ), verwendeten sie Azure Table Storage zum Speichern von Daten.
Zukunftspläne
Ich möchte einen der Hauptnachteile dieses Ansatzes beheben - die horizontale Skalierung. Dazu benötigen Sie entweder verteilte Transaktionen (sic!) Oder eine willensstarke Entscheidung, dass sich dieselben Daten aus verschiedenen Instanzen nicht ändern sollen, oder sie nach dem Prinzip "Wer ist der Letzte - das ist richtig" ändern lassen.
Aus technischer Sicht sehe ich folgendes Schema möglich:
- Speichern Sie EventLog und Snapshot anstelle des Objektmodells
- Andere Instanzen suchen (Endpunkte aller Instanzen hinzufügen? Udp-Erkennung? Master / Slave? Zu den Einstellungen)
- Replizieren Sie zwischen EventLog-Instanzen über einen der Konsensalgorithmen, z. B. RAFT.
Es gibt auch ein anderes Problem, das mich stört - das kaskadierende Löschen oder das Erkennen von Fällen des Löschens von Objekten, auf die von anderen Objekten verwiesen wird.
Quellcode
Wenn Sie bis hierher lesen - dann muss nur noch Code gelesen werden, es kann sein
auf Github gefunden .