Der übliche Ratschlag, wenn es darum geht, Deadlocks zu vermeiden, besteht darin, Ressourcen immer in der gleichen Reihenfolge zu sperren. Aber wie würden Sie dies in Bezug auf Zeilensperren in einer sehr zufriedenen Oracle-Datenbank implementieren?
Um zu sehen, was ich meine, betrachte das folgende Beispiel. Eine sehr einfache DAO für Bankkonten:
%Vor% Um eine Übertragung zwischen zwei Konten auszuführen, gibt es eine Art InternalBankTransfer
-Klasse mit einer Übertragungsmethode:
Normalerweise funktioniert das gut. Aber lassen Sie uns sagen, dass wir zwei Leute haben, die gleichzeitig Übertragungen initiieren. Nehmen wir an, Anne möchte 100 Dollar an Bob überweisen, während Bob 50 zu Anne transferieren will. Also ruft Anne in einem Thread transfer("Anne", "Bob", 100)
und in einem anderen Bob transfer("Bob", "Anne", 50)
an. Dieser Code ist anfällig für Deadlocks, wenn die Ausführungsreihenfolge wie folgt lautet:
Ich gebe zu, dass ich überhaupt nicht darüber nachgedacht habe, bevor ich in einer realen Anwendung tote Verschlüsse sah. Meine naive Ansicht war, dass die Art der Transaktionsisolation sich automatisch darum kümmerte. Oracle sagt, dass dies auf ein schlechtes Anwendungsdesign zurückzuführen ist. Aber was ist ein gutes Design in diesem Fall? Muss ich select for update
alles was ich aktualisieren möchte? Was ist, wenn es sich um eine große Transaktion handelt, bei der mehrere Tabellen aktualisiert werden? Sollte ich so aussehen, dass tote Schlösser unmöglich sind, oder sie einfach minimieren und akzeptieren, dass sie es sind eine Tatsache des Lebens?
Ich denke, es ist eine Tatsache des Lebens (und eine, die wirklich nur mit hohen Nebenläufigkeit und Hotspot-Daten passieren sollte).
Wenn Sie die Sperranordnung implementieren möchten, dann müssen Sie Ihren Code neu schreiben, um die Konten in einer vorgegebenen Reihenfolge zu sperren oder zu aktualisieren (zuerst Anne, dann Bob). Aber das wird mit komplexen Transaktionen nicht machbar sein. Wenn dies nur bei einigen Hotspot-Zeilen der Fall ist, können Sie möglicherweise die Sperrreihenfolge nur für diese verwenden (und den Rest unverändert belassen) und damit fertig werden.
Oder verwenden Sie weniger granulare Sperren, aber das wird Ihre Nebenläufigkeit beenden.
In Ihrem Fall können Sie einfach die abgebrochene Transaktion wiederholen. Und wenn es zu oft passiert, scheint es, dass Sie ein Problem mit Ihrem Anwendungsdesign haben.
Hier ist ein Link für ein zweiphasiges Commit-Protokoll für Kontoübertragungen. Es stammt aus dem MongoDB-Wiki, d. H. Von Leuten, die nicht einmal den Luxus von Zeilensperren und Transaktionen haben, aber man könnte das auch auf einem RDBMS implementieren, um Sperrkonflikte zu vermeiden. Das wäre natürlich eine ziemlich radikale Neugestaltung der Anwendung. Ich würde alles andere zuerst versuchen (Wiederholungen, grobe Sperren, künstlich reduziertes Nebenläufigkeitsniveau, Stapelverarbeitung).
Es gibt einige Probleme mit dem obigen Entwurf.
Auch wenn Sie nach Deadlocks fragen, habe ich das Bedürfnis, auch über andere Probleme zu schreiben, die IMHO falsch sind und sie könnten Sie in der Zukunft vor einigen Schwierigkeiten bewahren.
In Ihrem Entwurf ist das erste Problem, das ich sehe, die Methodentrennung: Um eine Änderung des Guthabens vorzunehmen, haben Sie eine Methode zum Abheben und eine Methode zum einzahlen. In jedem verwenden Sie die gleiche Methode "modifyBalance", um die Aktion auszuführen. und es gibt einige Probleme in der Art, wie es gemacht wird:
1- Die Methode modifyBalance fordert bei jedem Aufruf eine Verbindung an 2- Die Verbindung wird höchstwahrscheinlich den automatischen Festschreibmodus aktiviert haben, da Sie auto commit nicht auf off gesetzt haben.
Warum ist das problematisch? Die Logik, die du tust, soll eine Einheit sein. Nehmen wir an, Sie ziehen 50 von Bob zurück und es gelingt ihm. Sie haben ein automatisches Commit und die Änderungen sind endgültig. jetzt versuchst du Anne zu hinterlegen und es scheitert. Laut dem obigen Code wird Anne die 50 nicht bekommen, aber Bob hat sie schon verloren !!! Also in diesem Fall müssen Sie die Einzahlung zu Bob erneut aufrufen und die 50 an ihn zurückgeben, in der Hoffnung, dass es nicht scheitert oder sonst ... unendliche Verarbeitung. Daher sollten diese Aktionen in der gleichen Transaktion sein. Entweder gelingt sowohl das Zurückziehen als auch das Ablegen, und sie werden verpflichtet, oder sie versagen und alles wird rückgängig gemacht.
es ist auch problematisch, da im automatischen Festschreibungsmodus das Festschreiben stattfindet, nachdem die Anweisung abgeschlossen ist oder die nächste Ausführung stattfindet. Wenn aus irgendeinem Grund das Commit nicht stattgefunden hat, dann, da Sie die Verbindung nicht schließen (und das ist noch ein Problem, da es nicht zurück in den Pool gelangt) und kein Commit passiert, kann zu einem Deadlock führen, wenn ein anderes Update auf dem Zeile in der ersten Transaktion gesperrt.
Daher schlage ich vor, dass Sie Folgendes tun: entweder fordern Sie die Verbindung in Ihrer Überweisungsmethode an, oder vereinigen Sie die Methoden zurückziehen und hinterlegen in der Methode, um das Guthaben selbst zu modifizieren.
Da es Ihnen scheint, dass Ihnen die Idee gefallen hat, die beiden Methoden so zu haben, wie sie sind, werde ich die Verwendung der ersten Option, die ich erwähnt habe, demonstrieren:)
%Vor%und die Übertragungsmethode wird:
%Vor%Jetzt sind beide Aktionen erfolgreich oder beide werden zurückgesetzt. Wenn ein Update für eine Zeile ausgegeben wird, warten andere Transaktionen, die versuchen, die Zeile zu aktualisieren, auf den Abschluss, bevor sie fortfahren können. Zurückrollen oder Festschreiben stellt die Freigabe der Sperre auf Zeilenebene sicher.
Nun ist das obige eine Erklärung für ein besseres Design, um die logischen Aktionen und die Korrektheit der Daten zu behalten. aber es wird nicht lösen Ihr Schloss Probleme !!!! Hier ist ein Beispiel, was passieren könnte:
Angenommen, Thread 1 versucht sich vom Bob zurückzuziehen.
status: row bob gesperrt durch t1
Zu diesem Zeitpunkt zieht Thread zwei von Anne zurück
status: Zeile anne gesperrt von Thread 2
Jetzt Thread 1 möchte anne
ablegenstatus: thread 1 sieht row anne ist gesperrt, so dass es sitzt und wartet auf die Sperre freigegeben werden, damit es das Update machen kann: thread 1 wartet tatsächlich auf Thread Tweo für das Ende der Aktualisierung und Festschreiben oder Rollback der Sperre wird veröffentlicht
jetzt thread two möchte auf bob
ablegenstatus: bob row ist gesperrt, so dass thread two auf seine Freigabe wartet
DEADLOCK !!!!!
zwei Threads warten aufeinander.
Wie lösen wir es? Bitte sehen Sie sich die Antworten an (ich habe sie beim Tippen gesehen) und akzeptieren Sie diese Antwort nicht, sondern akzeptieren Sie die, die Sie tatsächlich verwenden, um Deadlocks zu vermeiden. Ich wollte nur über die anderen Themen sprechen und tut mir leid, dass ich so lange war.
Sie könnten SELECT FOR UPDATE NOWAIT
in der Zeile verwenden, bevor Sie versuchen, sie zu aktualisieren. Wenn die Zeile bereits gesperrt ist, erhalten Sie einen Fehler (ORA-00054). Warten Sie ein wenig und wiederholen Sie (*) oder werfen Sie eine Ausnahme aus.
Sie sollten niemals in Deadlocks geraten, da sie so leicht zu verhindern sind.
(*) In diesem Fall müssten Sie die gesamte Transaktion (nach einem Rollback) wiederholen, um eine Deadlock-Situation zu vermeiden.
Unter der Annahme, dass die Auszahlung und die Einzahlung Teil einer einzigen Datenbanktransaktion sind, sollte es relativ einfach sein, Deadlocks zu vermeiden, indem einfach mit den Konten in der richtigen Reihenfolge gearbeitet wird. Wenn Ihre Anwendung Übertragungen durch Abbuchung oder Gutschrift der niedrigeren Kontonummer zuerst ausgeführt hat und dann die höhere Kontonummer belastet oder gutgeschrieben wurde, wären Sie niemals in der Lage, einen Deadlock durch mehrere gleichzeitige Übertragungen zu bewirken. Aus Gründen der Deadlock-Verhinderung ist es egal, welche Reihenfolge Sie erzwingen (obwohl dies für die Anwendungsleistung von Bedeutung sein kann), solange Sie konsistent sind, diese Reihenfolge durchzusetzen.