Was ist ein effizienter Weg, um Einfügungen von einzigartigen "unveränderlichen" Entitäten durch mehrere Produzenten mit optimistischem Concurrency-Ansatz zu handhaben?

8

Nehmen Sie ein System mit mehreren gleichzeitigen Produzenten an, die jeweils bestrebt sind, einen Graphen von Objekten mit den folgenden gemeinsamen Entitäten zu erhalten, die durch ihre Namen eindeutig identifizierbar sind:

%Vor%

Beispiel: Erzeuger A speichert einige CommonEntityMeeting s, während Erzeuger B CommonEntitySet s speichert. Jeder von ihnen muss CommonEntity s beibehalten, die sich auf die jeweiligen Elemente beziehen.

Grundsätzlich sind die wichtigsten Punkte:

  • Es gibt unabhängige Produzenten.
  • Sie arbeiten gleichzeitig.
  • Theoretisch (obwohl sich das ändern ändern kann und noch nicht genau wahr ist) werden sie über denselben Web Service (ASP.Net Web API) betrieben, nur mit ihren jeweiligen Endpunkten / "Ressourcen". Idealerweise sollte die vorgeschlagene Lösung daher nicht darauf angewiesen sein.
  • Sie sind bestrebt, verschiedene Graphen von Objekten zu erhalten, die möglicherweise noch nicht existierende CommonEntity / CommonEntityGroup-Objekte enthalten.
  • CommonEntity / CommonEntityGroup sind nach der Erstellung unveränderlich und werden danach nicht mehr geändert oder entfernt .
  • CommonEntity / CommonEntityGroup sind nach einigen ihrer Eigenschaften eindeutig ( Name und zugehörige gemeinsame Entität, falls vorhanden (z. B. CommonEntity ist eindeutig von CommonEntity.Name + CommonEntityGroup.Name )).
  • Hersteller kennen / interessieren sich nicht für IDs dieser CommonEntities - sie geben normalerweise nur DTOs mit Names (einmalig) von diesen CommonEntities und zugehörigen Informationen weiter. Also muss Common(Group)Entity von Name .
  • gefunden / erstellt werden
  • Da ist ein bestimmte Möglichkeit, dass die Produzenten versuchen werden, dasselbe zu schaffen CommonEntity / CommonEntityGroup zur gleichen Zeit.
  • Obwohl es viel ist wahrscheinlicher, dass solche CommonEntity / CommonEntityGroup-Objekte bereits vorhanden sind existieren in db.

Also, mit Entity Framework (Datenbank zuerst, obwohl es wahrscheinlich keine Rolle spielt) als DAL und SQL Server als Lagerung Was ist ein effizienter und zuverlässiger Weg, um sicherzustellen, dass alle diese Hersteller erfolgreich ihre überschneidenden Objekt Graphen gleichzeitig bestehen bleiben?

Wenn Sie berücksichtigen, dass UNIQUE INDEX bereits dafür sorgt, dass es keine doppelte CommonEntities gibt (Name, GroupName-Paar ist eindeutig), kann ich folgende Lösungen sehen:

  1. Stellen Sie sicher, dass jede CommonEntity / CommonGroupEntity gefunden / erstellt wurde + SaveChanged () , bevor Sie den Rest des Objektgraphen erstellen.

In einem solchen Fall, wenn SaveChanges für verwandte Entitäten aufgerufen wird, gibt es keine Indexverstöße aufgrund der Tatsache, dass andere Produzenten die gleichen Entitäten einen Moment vorher erstellen.

Um es zu erreichen, werde ich einige

haben %Vor%

Bei diesem Ansatz wird es mehrere Aufrufe von SaveChanges geben und jede CommonEntity wird ihre eigene Art von Repository haben, obwohl dies die zuverlässigste Lösung zu sein scheint.

  1. Erstellen Sie einfach das gesamte Diagramm und erstellen Sie es neu, wenn Indexverletzungen auftreten

Ein bisschen hässlich und ineffizient (mit 10 CommonEntities müssen wir es vielleicht 10 mal wiederholen), aber einfach und mehr oder weniger zuverlässig.

  1. Erstellen Sie einfach das gesamte Diagramm und ersetzen Sie doppelte Einträge, wenn Indexverletzungen auftreten

Nicht sicher, ob es eine einfache und zuverlässige Möglichkeit gibt, doppelte Einträge in mehr oder weniger komplexen Objektdiagrammen zu ersetzen, obwohl sowohl fallspezifische als auch allgemeinere reflexionsbasierte Lösungen implementiert werden können.

Dennoch, wie bei einer vorherigen Lösung, können mehrere Wiederholungen erforderlich sein.

  1. Versuchen Sie, diese Logik in die Datenbank (SP) zu verschieben

Bedenken Sie, dass es einfacher ist, innerhalb der gespeicherten Prozedur zu arbeiten. Es werden die gleichen optimistischen oder pessimistischen Ansätze sein, die gerade auf der Datenbankseite implementiert wurden.

Obwohl es möglicherweise eine bessere Leistung bietet (in diesem Fall kein Problem) und die Einfügelogik an einem gemeinsamen Ort ablegt.

  1. Verwendung von SERIALIZABLE Isolationsstufe / TABLOCKX + SERIALIZABLE Tabellenhinweis in Stored Procedure - es sollte definitiv funktionieren, aber ich würde es vorziehen, die Tabellen nicht mehr als nur wirklich zu sperren, da das tatsächliche Rennen ziemlich selten ist . Und wie bereits im Titel erwähnt, würde ich gerne einen optimistischen Parallelitätsansatz finden.

Ich würde wahrscheinlich die erste Lösung versuchen, aber vielleicht gibt es bessere Alternativen oder einige mögliche Fallstricke.

    
Eugene Podskal 26.12.2016, 20:48
quelle

4 Antworten

4

Tabelle bewertete Parameter

Eine Option besteht darin, table valued parameters anstelle von einzelnen Aufrufen für die Datenbank zu verwenden.

Beispielprozedur mit einem Tabellenwertparameter:

%Vor%

Tabelle Wert Parameterreferenz:

Ich empfehle merge nicht, es sei denn, es gibt ein überzeugendes Argument dafür. Diese Situation betrachtet nur das Einfügen, so scheint es wie Overkill.

Beispiel merge version mit Tabellenwertparameter:

%Vor%

merge reference:

ignore_dup_key Code-Kommentar:
  

// Überprüfen Sie, ob es sich um einen Namensindex-Verstoß handelte (vielleicht machen Sie die Indizes IGNORE_DUP_KEY)

ignore_dup_key wird serializable hinter dem die Szenen ; potenziell kostspieliger Overhead für nicht gruppierte Indizes ; und selbst wenn der Index gruppiert ist, kann kostet abhängig von der Anzahl der Duplikate .

Dies kann in den gespeicherten Prozeduren unter Verwendung von Sam Saffrons Upsert (Update / Einfügen) Muster , oder eines der hier gezeigten Muster: Leistungseinfluss verschiedener Fehlerbehandlungstechniken - Aaron Bertrand .

SqlZim 31.12.2016, 14:41
quelle
2

Die Wahl des Ansatzes hängt sicherlich von der Art der Funktionalität ab. Datenmenge, die beide Prozeduren verwenden.

Wenn wir mit dem ersten Ansatz fortfahren, wird das Entity Framework für jeden SaveChanges () -Aufruf eine Transaktion anlegen. Dies könnte die Leistung im Falle einer großen Anzahl von Datensätzen ein wenig reduzieren.

Wenn es eine beträchtliche Anzahl von Datensätzen gibt, die eingefügt / aktualisiert werden müssen, dann werde ich sicherlich mit dem Stored Procedure basierten Ansatz gehen. Mit diesem Ansatz haben Sie eine vollständige Kontrolle über die Datenbank & amp; Die Abfrage nach dem Datensatz, um zu prüfen, ob er existiert, wird sehr einfach sein (obwohl hier einige Feinabstimmungen erforderlich sein können). Ich sehe nicht, ob es irgendwelche Herausforderungen geben würde, dasselbe mit gespeicherten Prozeduren zu implementieren. Mit wenigen Implementierungsoptimierungen, wie dem Laden der Daten in temporäre Tabellen (nicht sql temporäre Tabellen, sondern physische Tabellen, die zum vorübergehenden Speichern von Daten verwendet werden können), kann dies weiter verbessert werden, um vollständige Informationen zu erhalten, die die gespeicherte Prozedur verarbeitet hat.

    
Mads... 29.12.2016 13:55
quelle
2

Basierend auf Ihrem letzten wichtigen Punkt besteht eine andere Lösung darin, Ihre Erstellungslogik auf einen zentralen Anwendungsserver / -dienst zu verschieben (siehe Update 2) , der eine Warteschlange zum Hinzufügen von Datensätzen verwenden kann .

Da die meisten Ihrer Datensätze bereits existieren, sollten Sie, wenn Sie eine Art Caching verwenden, dies sehr effizient machen können

Nun, über die Anzahl der Datensätze.
Man muss bedenken, dass die EF nicht dafür ausgelegt ist, "Massen" -Operationen zu unterstützen, daher wird die Erstellung von Tausenden von Datensätzen (wirklich, wirklich) langsam sein.

Ich habe 2 Lösungen verwendet, die Ihnen und einer großen Anzahl von Datensätzen sehr schnell helfen 1) EntityFramework.BulkInsert
2) SqlBulkCopy

Beide sind extrem einfach zu bedienen

Ich hoffe auch, dass Sie den schnellsten Weg zum Einfügen in Entity gesehen haben Framework

Aktualisieren
Unten ist eine andere Lösung, die ich kürzlich zweimal benutzt habe Speichern Sie Ihren Datensatz nicht, wenn ein Benutzer einen "Speichern" -Aufruf ausführt, sondern legen Sie fest, dass er X Sekunden später ausgeführt wird.
Wenn in der Zwischenzeit ein anderer Benutzer versucht, den gleichen Datensatz zu speichern, "schieben" Sie einfach das geplante Datum.

Unten sehen Sie einen Beispielcode, der versucht, den gleichen Datensatz 10 Mal (zur gleichen Zeit) zu speichern, aber die tatsächliche Speicherung geschieht nur einmal.

Das tatsächliche Ergebnis kann hier gesehen werden:

%Vor%

Update 2 Da Sie den "creation" -Prozess in Ihrer WebAPI-App steuern können, sollten Sie in der Lage sein, das Duplizieren mit einer Art Cache zu vermeiden, wie im folgenden Pseudocode

%Vor%     
George Vovos 30.12.2016 11:31
quelle
2
  

Hersteller wissen / interessieren sich nicht für IDs dieser CommonEntities - sie   in der Regel nur DTOs mit Namen (eindeutig) von diesen CommonEntities und   verwandte Informationen. Also muss jede gemeinsame (Gruppen-) Entität sein   gefunden / erstellt von Name.

Ich gehe davon aus, dass Tabellen, in denen Ihre Objekte gespeichert sind, auf CommonEntity mit ID und nicht auf Name verweisen.

Ich nehme an, dass die Tabellendefinition des Objekts ungefähr so ​​aussieht:

%Vor%

Gleichzeitig hat die Funktion SaveSomeObject auf hoher Ebene CommonEntity.Name und CommonEntityGroup.Name (nicht ID ) als Parameter. Es bedeutet, dass die Funktion irgendwo die Name der Entität nachschlagen und ihre entsprechende ID finden muss.

Somit kann die Funktion SaveSomeObject auf hoher Ebene mit den Parametern (ObjectName, CommonEntityName, CommonEntityGroupName) in zwei Schritten implementiert werden:

%Vor%

GetCommonEntityID ist eine Hilfsfunktion / gespeicherte Prozedur, die die Entität ID nach ihrer Name sucht und bei Bedarf eine Entität erstellt (generiert ID ).

Hier extrahieren wir diesen Schritt explizit in eine separate dedizierte Funktion. Nur diese Funktion muss mit Nebenläufigkeitsproblemen umgehen. Es kann unter Verwendung von optimistischem Concurrency-Ansatz oder pessimistisch implementiert werden. Dem Benutzer dieser Funktion ist es egal, welche Magie er verwendet, um eine gültige ID zurückzugeben, aber der Benutzer kann sicher sein, dass er die zurückgegebene ID verwenden kann, um den Rest des Objekts zu erhalten.

Pessimistischer Concurrency-Ansatz

Der pessimistische Nebenläufigkeitsansatz ist einfach. Stellen Sie sicher, dass nur eine Instanz von GetCommonEntityID kann ausgeführt werden. Ich würde sp_getapplock dafür verwenden (anstelle von SERIALIZABLE Transaktionsisolationslevel oder Tabellenhinweisen) . sp_getapplock ist im Wesentlichen ein Mutex und sobald eine Sperre erreicht ist, können wir sicher sein, dass keine andere Instanz dieser gespeicherten Prozedur parallel läuft. Dies macht die Logik einfach - versuchen Sie, die ID und INSERT die neue Zeile zu lesen, wenn sie nicht gefunden wird.

%Vor%

Optimistischer Concurrency-Ansatz

Versuchen Sie nicht, etwas zu verriegeln. Handeln Sie optimistisch und schauen Sie sich ID an. Wenn nicht gefunden, versuchen Sie INSERT den neuen Wert und versuchen Sie es erneut, wenn eine eindeutige Indexverletzung vorliegt.

%Vor%

Bei beiden Ansätzen sollten Sie die Logik wiederholen. Der optimistische Ansatz ist im Allgemeinen besser, wenn Sie erwarten, dass die Namen bereits in der Entitätentabelle vorhanden sind und die Wahrscheinlichkeit von Wiederholungen gering ist (wie in Ihrem Fall in der Frage beschrieben). Der pessimistische Ansatz ist im Allgemeinen besser, wenn Sie erwarten, dass viele konkurrierende Prozesse den gleichen Namen einfügen. Sie sind wahrscheinlich besser dran, wenn Sie Inserts serialisieren.

    
Vladimir Baranov 31.12.2016 15:35
quelle