Laden...

EF Core: Behandlung von Parallelitätskonflikten (Concurrency)

Erstellt von schuppsl vor 6 Jahren Letzter Beitrag vor 6 Jahren 4.446 Views
S
schuppsl Themenstarter:in
789 Beiträge seit 2007
vor 6 Jahren
EF Core: Behandlung von Parallelitätskonflikten (Concurrency)

verwendetes Datenbanksystem: MSSQL

Ich nutze in meinem Projekt einige Tabellen, die ich mit Code-First erstellt habe und per Migration update.
Jede Tabelle hat eine ID als Primary Key und ein Feld Namens TimeStamp, mit gleichnamigem Attribut.

Auf eine Tabelle greifen mehrere Clients gleichzeitig zu und verändern in einer Zeile ein und denselben Wert.

Heißt also, dass wenn diese gleichzeitig diesen Wert verändern wollen, bekomme ich eine DbUpdateConcurrencyException.

Wenn ich mir optimistisches Sperren so anschaue, dann kann ich den Anwender bei einer ConcurrencyException entscheiden lassen, ob:

  1. Der aktuelle Wert
  2. Der ursprüngliche Wert oder
  3. Der Datenbank Wert

gespeichert werden soll.

Ich muss zugeben, das habe ich nicht ganz verstanden.

Das hilft mir aber auch nicht weiter, da die Speicherung nicht durch einen Anwender erfolgen solll, sondern automatisch im Hintergrund.
Quelle: Handling Concurrency

Ich habe einiges ausprobiert, aber leider ist das Ganze schwer zu Debuggen.

Daher hier ein Gedankenexperiment:

Eine einfache Klasse und die daraus erzeugte Tabelle:


public class TestClass
{
[Required]
public int Id {get;set;}

public int Zaehler {get;set;}

[TimeStamp]
public byte[] TimeStamp {get;set;}
}


Hier soll jetzt der Zaehler jeweils um X erhöht werden.
X soll hier =1 sein.

Zwei Clients greifen also gleichzeitig auf die Tabelle zu und erhöhen den Zaehler jeweils um 1.
Ziel: Es soll danach 2 drinstehen.

Also rufe ich SaveChanges() auf und fange die Exception ab:


try
{
SaveChanges();
}
catch(DbUpdateConcurrencyException ex)
{
...
}

Im catch-Block kann ich nun entscheiden, wie ich hier verfahren soll.
Siehe im Link oben.

Angenommen, der 1. Client erzeugt einen DbContext, liest den Zaehler mit Wert 0 aus und erhöht diesen um 1.

Client 2 macht gleichzeitig dasselbe.

Beide Clients haben also die 0 ausgelesen und sind nun der Meinung, dass er neue Wert von Zaehler = 1 sein soll.

Client 1 ist nun schneller und schreibt die 1 in die Datenbank, Client 2 bekommt nun die DbUpdateConcurrencyException.

Was kann Client 2 nun hier machen?
Nach der Auflistung oben,** so wie ich es verstanden habe:**

Verständnis-Fragen:

  1. Aktueller Wert:
    Ich kann nun den Aktuellen Wert des Client 2 reinschreiben. Also die 1.

  2. Originaler Wert:
    Der Wert aus der Datenbank, bevor **irgendwelche **Änderungen gemacht wurden.
    Also = 0.

  3. Datenbank-Wert:
    Der Wert, der aktuell in der Datenbank steht.
    Also 1, da Client 1 ja schon den Zaehler incrementiert hat

Stimmt das so?

In jedem der drei Fälle bekomme ich keine 2 in den Zaehler.

Ich muss jetzt aber sicherstellen, dass auf jeden Fall die 2 im Zaehler steht.
Welche Möglichkeiten gibt es hier?

Eigentlich müsste ich hingehen, den aktuellen Wert aus der Datenbank auslesen ( also 1) und diesen zu dem aktuellen Wert (also 1 bzw. X) addieren und erneut schreiben.

Was aber, wenn ich dann erneut eine DbUpdateConcurrencyException bekomme?

W
955 Beiträge seit 2010
vor 6 Jahren

Hi,
wenn die Eintragungen automatisch erfolgen sollen kannst Du
* den Speicherversuch bei einer UodateException erneut versuchen bis sie nicht mehr auftritt
* einen Service davorschalten der das Inkrementieren mit einer Sperrprimitive schützt (Semaphore o.ä.)

16.830 Beiträge seit 2008
vor 6 Jahren

Auf eine Tabelle greifen mehrere Clients gleichzeitig zu und verändern in einer Zeile ein und denselben Wert.

Das ist der (Konzept-)Fehler.

Schalte einen Service davor.

P
1.090 Beiträge seit 2011
vor 6 Jahren

Grundlegend muss du dir Überlegen, wie die Anwendung sich da verhalten soll.

Grundlegend kannst du das Lesen um 1 erhöhen und Speichern in eine Tranaction packen, die so lange den Lesenden zugriff auf den Datensatz blockiert.

MSDN:Verwenden von Transaktionen (EF Core)

*bitte beachte unten die angegebenen Einstellungen für EF Core. Es kann sein das es das noch nicht kann. Mein letzter Stand ist das EF Core noch nicht wirklich für den Produktive Betrieb bereit ist.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

S
schuppsl Themenstarter:in
789 Beiträge seit 2007
vor 6 Jahren

Ok, aber wie sieht so ein Service aus?
Gibt es hier Links/Empfehlungen?

16.830 Beiträge seit 2008
vor 6 Jahren

Client verbinden sich mit einer HTTP API (Json) und nicht direkt mit der Datenbank.
Stichworte hier sind WebAPI, HTTP API, REST, GraphQL.

Eine Direktverbindung zur Datenbank ist unsichert, schlecht wartbar und im Zeitgeist von 1970.

P
1.090 Beiträge seit 2011
vor 6 Jahren

Web Api löst hier aber das Problem mit dem gleichzeitigen Schreiben nicht.

Wenn ich in der Api lösen wollte, bräuchte ich da eine Statische Klasse, was bei einer Web Api sehr unschön ist.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

16.830 Beiträge seit 2008
vor 6 Jahren

Nein.
Auch eine statische Klasse hilft Dir hier nicht; wenn dann meinst Du eine Singleton-Verbindung auf die Datenbank.
Ansonsten wird in einer Web API einfach (sofern wir von .NET sprechen) jeder Request in einem eigenen Thread ausgeführt und damit parallel durchgeführt - egal ob statisch oder nicht.
In NodeJS sind alle Requests im gleichen Thread und dadurch gibt es hier gar keine parallelen Zugriffe.

Concurrency ist immer eine Datenbank-Implementierungssache:
Optimistic vs. Pessimistic.

P
1.090 Beiträge seit 2011
vor 6 Jahren

Bei einer statischen Klasse wird aber, die gleiche Klasse von allen Threads verwendet, dann klappt auch das mit dem von witte schon erwähntenv Semaphore. Bei der SingelTon implementierung hab ich eine statische Instance der Klasse (vereinfacht ausgedrückt).

Mir ging es auch eher darum kurz anzudeuten, das statische "Sachen" bei einer WebApi sehr unschön sind. Und das die Web API alleine nicht reicht das Problem zu lösen.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

S
schuppsl Themenstarter:in
789 Beiträge seit 2007
vor 6 Jahren

Grundlegend muss du dir Überlegen, wie die Anwendung sich da verhalten soll.

Grundlegend kannst du das Lesen um 1 erhöhen und Speichern in eine Tranaction packen, die so lange den Lesenden zugriff auf den Datensatz blockiert.


>

*bitte beachte unten die angegebenen Einstellungen für EF Core. Es kann sein das es das noch nicht kann. Mein letzter Stand ist das EF Core noch nicht wirklich für den Produktive Betrieb bereit ist.

Das scheint die Lösung zu sein. Im Test ist der neue Wert = 2.

Ich bin überzeugt, dass das in der Praxis sehr selten der Fall sein wird, trotzdem muss ich garantieren, dass das Endergebnis stimmt.

Vielen Dank mal. 👍

D
985 Beiträge seit 2014
vor 6 Jahren

Haben sich die (TimeStamp/RowVersion) Werte zwischen Lesen und Schreiben verändert, dann gibt es die Concurrency Exception.

Der einfachste Weg aus diesem Dilemma:

Nochmal Lesen und Schreiben - und zwar so lange, bis es eben keine Concurrency Exception mehr gibt.

16.830 Beiträge seit 2008
vor 6 Jahren

Bei einer statischen Klasse wird aber, die gleiche Klasse von allen Threads verwendet

Nochmal: eine statische Klasse alleine hilft hier nicht.
Es geht hier um die Singleton-Verbindung zur Datenbank - und nicht die statische Klasse!

P
1.090 Beiträge seit 2011
vor 6 Jahren

Auch eine Singelton Implementierung sollte, wenn möglich bei einer WebApi nicht verwendet werden.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

T
2.223 Beiträge seit 2008
vor 6 Jahren

@schuppsl
Schalte eine WebAPI vor deine Clients.
Umso mehr Clients du im Endeffekt hast, umso größer wird mit der Zeit dein Problem noch an an anderen Stellen, wenn die Clients auch auf andere Resourcen parallel zugreifen können.
Du solltest dann die Daten in der WebAPI dann in einer Transaktion lesen, den Zeitstempel neusetzen und den Zähler hoch zählen.

Mit der WebAPI hast du dann auch eine zentrale Schnittstelle für deine Clients und kannst auch die Performance besser skalieren.
So könntest du in deiner WebAPI die Daten auch einmal einlesen und die Werte dann im Cache halten und dort den Zähler hochzählen und den Zeistempel setzen.

Das Schreiben in die DB könntest du dann per Task/Skript periodisch triggern lassen.
Und natürlich wenn deine WebAPI beendet wird den aktuellen Stand in die DB schreiben.
Somit entfallen vermutlich einige unnötige Lese- und Schreiboperationen, was sich auch zukünftig bemerkbar machen dürfte.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
schuppsl Themenstarter:in
789 Beiträge seit 2007
vor 6 Jahren

Also das mit der Transaktion hilft mir leider doch auch nicht weiter, da das Lesen nicht exclusiv geschieht.
Im Debugger sehe ich, wie beides Mal die 0 ausgelesen wird.

Beim Commit gibt es nachher trotzdem eine DbConcurrency Exception und ich bin wieder gleich weit.

Habe das Lesen, incrementieren und Schreiben in die Transaktion gepackt.

T
2.223 Beiträge seit 2008
vor 6 Jahren

@schuppsl
Eigentlich sollte es doch auch reichen ein Update mit der ID und dem Timestamp gegen die DB zu schicken oder?

Also in etwa sowas:


UPDATE TestClass SET Zaehler = Zaehler + 1, TimeStamp=@TimeStamp WHERE ID=@ID

Dann musst du nur den aktuellen Zeitstempel + die Zeilen ID per RAW SQL mitgeben.
Dann sparst du dir das einladen der gesamten Zeile und musst nur die ID wissen und den aktuellen Zeitstempel mitgeben.
Wäre der Zeitstempel auch in der DB ein TimeStamp/DateTime könntest du auch über GETDATE() den Zeitstempel setzen lassen.
Dann bräuchstest du nur noch die ID mitgeben und deine Update Anweisung mitgeben.
Dies sollte reichen, da die DB dann jedes Update gegen die Zeile locken dürfte, da diese den gleichen Eintrag bearbeiten wollen.

Spart dir im Endeffekt alle zusätzlichen Lesevorgänge und du musst im Client nur die ID kennen.
Der Rest wird durch die DB erledigt.
Oder spricht noch etwas dagegen in deinem Code?

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

P
1.090 Beiträge seit 2011
vor 6 Jahren

Schau dir vielleicht noch mal die Isolationsstufen für Transaktionen an.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

S
schuppsl Themenstarter:in
789 Beiträge seit 2007
vor 6 Jahren

Oder spricht noch etwas dagegen in deinem Code?

T-Virus

Nein, das spricht gar nichts dagegen.
Ich dachte nur, das sich so etwas mit dem Entity Framework leicht bewerkstelligen ließe 😃

Habe noch einige Versuche mit dem EF 6 gemacht und

 using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
            

Es wird aber trotzdem das Lesen erlaubt, wenn schon gelesen wird.

Vielen Dank an alle.

T
2.223 Beiträge seit 2008
vor 6 Jahren

@schuppsl
ReadCommited erlaubt gleichzeitiges lesen auch.
Beim lesen der Daten muss auch nichts gesperrt werden, was auch katastrophal wäre bei vielen gleichzeitigen Lesevorgängen auf die gleiche Zeile.

Bei ReadComitted wird sichergestellt, dass du die Daten erst lesen kannst wenn der aktuelle Schreibvorgang fertig ist.
Du musst hier in deinem Client entweder das Lesen- und Schreiben der Daten in einer Transaktion durchführen oder eben mit meinem Ansatz nur einen Update mit der ID senden um den Zeitstempel und den aktuellen Zähler durch die DB setzen zu lassen.
Letztes dürfte deine Probleme erst einmal lösen.

Denoch solltest du deine Clients gegen eine WebAPI als zentrale Schnittstelle laufen lassen.
Somit kannst du zukünftige Probleme wie dieses besser lösen.
Aktuell musst du alle Clients updaten um das Problem zu lösen, was je nach Anzahl der Clients auch etwas Aufwand sein kann.
Da es auch Clients geben kann, die nicht zeitnah Updates fahren können, würdest du dir weitere Probleme durch alte und neue Clients einholen.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
schuppsl Themenstarter:in
789 Beiträge seit 2007
vor 6 Jahren

Hallo T.

Das mit dem Update per SQL funktioniert, nur bekomme ich bei jedem Update oder Delete folgende Meldung bzw. Exception:> Fehlermeldung:

The data reader is incompatible with the specified '....NlAuftrag'. A member of the type, 'Id', does not have a corresponding column in the data reader with the same name.

Datenbank und Model sehen aber identisch aus. Id ist in jeder Tabelle der PK.
Der Vorgang an sich (Update, Delete) ist aber erfolgreich.
Timestamp wird automatisch aktualisiert.

Eine WebApi stellt kein Problem dar, aber zuerst möchte ich das Grundproblem in den Griff bekommen.

Ich habe auch eine Tabelle, die komplett von verschiedenen Stellen geupdatet wird. Hier sollte der komplette Vorgang dann tatsächlich entsprechend gesperrt werden, was mich eigentlich wieder zum Ausgangsproblem bringt.

Weiter oben hieß es mal, dass man das Lesen und Schreiben sperren kann mit Transaktionen.
Das werde ich mir nochmal anschauen.

T
2.223 Beiträge seit 2008
vor 6 Jahren

@schuppsl
Wie sieht dein Code dazu aus?
Ein DataReader sollte an der stelle gar nicht zum Einsatz kommen, da du bei einem Update ja keine Daten liest sondern als NonQuery in der DB Verarbeiten lässt.

Dein Code sollte dann wie so aussehen.


using (var context = new DbContext()) 
{ 
    context.Database.ExecuteSqlCommand("UPDATE TestClass SET Zaehler = Zaehler + 1, TimeStamp=@TimeStamp WHERE ID=@ID",
            new SqlParameter("@TimeStamp ", timestamp),
            new SqlParameter("@ID", id));
}

Code ist nur zusammen kopiert als Beispiel.
Musst du dann entsprechend auf deinen Code umsetzen.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.