Verlorenes Update
Verlorenes Update (auch englisch lost update) bezeichnet in der Informatik einen Fehler, der bei mehreren parallelen Schreibzugriffen auf eine gemeinsam genutzte Information auftreten kann. Wenn zwei Transaktionen dieselbe Information verändern, dann können die Änderungen der ersten sofort durch die Änderungen der zweiten überschrieben werden.
Dabei spielt es keine Rolle, ob die „gemeinsam genutzte Information“ in einer Datei, in einer Datenbanktabelle oder im Arbeitsspeicher steht.
Lesen und Schreiben ohne Interaktion mit einem Benutzer
Beispiel
Eine Kette von Vorverkaufsstellen speichert für jede Veranstaltung die Anzahl der verkauften Karten. Es wurden bereits 100 Karten verkauft, als an einer Kasse fünf Karten zurückgegeben werden. Zur gleichen Zeit werden an einer zweiten Kasse drei Karten gekauft. Das System der ersten Kasse zieht die 5 zurückgegebenen Karten von den 100 ab und schreibt den neuen Wert (95) wieder in die Datenbank. Das zweite Kassensystem addiert die drei soeben verkauften Karten zu der 100 dazu und schreibt diesen Wert (103) ebenfalls in die Datenbank. Der zuerst geschriebene Wert geht dabei verloren, das Endergebnis ist falsch (103 verkaufte Karten, obwohl es tatsächlich nur 98 sind).
Zeitpunkt | Programm 1
5 Karten zurücknehmen |
Gespeicherte Anzahl
verkaufter Karten |
Programm 2
3 Karten verkaufen |
---|---|---|---|
0 | 100 | ||
1 | Anzahl der verkauften Karten lesen
Ergebnis: 100 |
100 | |
2 | 100 | Anzahl der verkauften Karten lesen
Ergebnis: 100 | |
3 | 5 Karten werden zurückgenommen
Neuen Wert berechnen: 100-5=95 Neuen Wert (95) schreiben |
95 | |
4 | 103 | 3 Karten werden verkauft
Neuen Wert berechnen: 100+3=103 Neuen Wert (103) schreiben |
Methoden zur Umgehung des Problems
Bei der Ausführung des Lesezugriffs wird die gemeinsam genutzte Information gesperrt, damit eine zwischenzeitliche Änderung durch ein anderes Programm nicht möglich ist.
Die dafür benötigten Sperrmechanismen werden von den verschiedenen Datenverwaltungssystemen bereitgestellt:
- der Share-Lock ermöglicht beliebig vielen Transaktionen einen Lesezugriff.
- der Exclusive-Lock ermöglicht nur einer einzigen Transaktion einen schreibenden Zugriff. In dieser Zeit darf keine andere Transaktion die gesperrten Daten lesen.
Diese Sperrmechanismen werden sowohl von den meisten Betriebssystemen und Datenbanken, als auch von Buffer-Managern verwendet, um konkurrierende Zugriffe zu handhaben.
Der Isolationlevel RR
Oft wird der Isolationslevel RR (Repeatable Read) als Lösung des Lost-Update-Problems genannt.
Die meisten RDBMS bieten verschiedene Isolationlevel an. Repeatable Read bedeutet, dass ein Share-Lock bis zum Ende einer Transaktion bestehen bleibt und nicht direkt nach dem Lesezugriff wieder verschwindet.
Wenn man den Isolationlevel RR verwendet, so muss man darauf achten, dass keine Deadlocks entstehen.
Das erste Programm schreibt einen Share-Lock, der nicht wieder entfernt wird. Das zweite Programm schreibt auch einen Share-Lock. Nun will das erste Programm den Share-Lock in einen Exclusive-Lock umwandeln, doch das geht nicht, solange das zweite Programm noch seinen Share-Lock aufrechterhält. Etwas später will das zweite Programm ebenfalls seinen Share-Lock in einen Exclusive-Lock umwandeln. Nun wartet jeder auf den anderen. Das ist die klassische Deadlock-Situation.
Zeitpunkt | Programm 1
5 Karten zurücknehmen |
Locks von
Prog. 1 |
Gespeicherte Anzahl
verkaufter Karten |
Locks von
Prog. 2 |
Programm 2
3 neue Karten verkaufen |
---|---|---|---|---|---|
0 | 100 | ||||
1 | Anzahl der verkauften Karten lesen
Ergebnis: 100 |
S-Lock
von P1 |
100 | ||
2 | S-Lock
von P1 |
100 | S-Lock
von P2 |
Anzahl der verkauften Karten lesen
Ergebnis: 100 | |
3 | 5 Karten werden zurückgenommen
Neuen Wert berechnen: 100−5=95 Exclusive-Lock anfordern warten auf P2 |
S-Lock
von P1 |
100 | S-Lock
von P2 |
|
4 | warten auf P2 | S-Lock
von P1 |
100 | S-Lock
von P2 |
3 Karten werden verkauft
Neuen Wert berechnen: 100+3=103 Exclusive-Lock anfordern warten auf P1 |
5 | warten auf P2 | S-Lock
von P1 |
100 | S-Lock
von P2 |
warten auf P1 |
Nun kommt es darauf an, wie das RDBMS in so einem Fall reagiert. Einige RDBMS – z. B. DB2 – kann man so parametrisieren, dass eine Transaktion nur eine bestimmte Zeit auf gesperrte Ressourcen wartet. Sobald diese Zeit verstrichen und die Ressource immer noch gesperrt ist, wird die Transaktion zurückgerollt und das Programm erhält eine Fehlermeldung (SQLCODE −911). Das wäre eine Lösung für das Problem, denn durch den Rollback der ersten Transaktion ist auch der Share-Lock entfernt worden und die zweite Transaktion bekommt nun den Exclusive-Lock, auf den sie gewartet hat. Wenn im Programm der SQLCODE −911 gezielt abgefragt wird, dann kann das Programm in einem solchen Fall den Satz erneut lesen und erhält nun den Wert, den das andere Programm gerade geschrieben hat. So geht kein Update verloren.
Zeitpunkt | Programm 1
5 Karten zurücknehmen |
Locks von
Prog. 1 |
Gespeicherte Anzahl
verkaufter Karten |
Locks von
Prog. 2 |
Programm 2
3 neue Karten verkaufen |
---|---|---|---|---|---|
6 | SQLCODE −911
Rollback |
100 | S-Lock
von P2 |
warten auf P1 | |
7 | 103 | X-Lock
von P2 |
Neuen Wert (103) schreiben | ||
8 | Anzahl der verkauften Karten lesen
Share-Lock anfordern warten auf P2 |
103 | X-Lock
von P2 |
||
9 | Share-Lock anfordern
warten auf P2 |
103 | commit | ||
10 | Share-Lock erhalten
Anzahl der verkauften Karten lesen Ergebnis: 103 |
S-Lock
von P1 |
103 | ||
11 | 5 Karten werden zurückgenommen
Neuen Wert berechnen: 103-5=98 X-Lock anfordern Neuen Wert (98) schreiben |
X-Lock
von P1 |
98 | ||
12 | Commit | 98 |
Der zweite Versuch kann genauso wie der erste Versuch misslingen, falls inzwischen ein drittes Programm einen Share-Lock auf den Satz gelegt hat. Daher muss das Lesen und Schreiben im Programm in einer Schleife ausgeführt werden.
Nun kann man sich überlegen, ob die Schleife beliebig oft wiederholt werden soll, oder ob nach n Versuchen die Verarbeitung dann doch aufgegeben und mit einer Fehlermeldung beendet werden soll.
Schleife Select ... Neuen Wert berechnen Update ... if (sqlcode not in (0, −911)) return(FEHLER) Until (sqlcode = 0 or Anz_Schleifen_Durchlaeufe > n) if (sqlcode <> 0) return(FEHLER)
Falls das RDBMS im Fall eines Deadlock so lange wartet, bis ein Administrator eingreift, dann ist diese Variante keine gute Lösung.
Verarbeitung serialisieren
Wenn das erste Programm die Information schon gleich beim Lese-Zugriff exklusiv sperrt, dann muss das zweite Programm schon mit seinem Lesezugriff warten. Sobald das erste Programm auch den Schreibzugriff ausgeführt hat und die Ressource wieder freigibt, kann das zweite Programm seine Verarbeitung fortsetzen. Diese Variante ist eine erzwungene Serialisierung der Verarbeitung.
Wenn die Information in einer Datei gespeichert wird, muss das Programm die Datei gleich zum Schreiben öffnen.
Wenn die Information in einer Datenbank-Tabelle gespeichert wird, dann kann der Satz z. B. durch einen CURSOR FOR UPDATE gelesen werden, oder die gesamte Tabelle kann durch LOCK TABLE IN EXCLUSIVE MODE gesperrt werden.
Lese- und Schreibzugriff atomisieren
Wenn zwischen dem Lesezugriff und dem Schreibzugriff keine weitere Verarbeitung erforderlich ist, kann man diese beiden Zugriffe auch zusammenfassen (Siehe Atomare Operation).
Bei einem RDBMS könnte der Zugriff für das Beispiel lauten:
update Tab set Anzahl_verkaufte_Karten = Anzahl_verkaufte_Karten + :Aktueller_Verkauf
Dadurch entfällt ein gesonderter Lese-Zugriff. Ein verlorenes Update kann nicht mehr vorkommen.
Lesen und Schreiben mit Benutzerinteraktion
In der Praxis kommt das Problem des verlorenen Updates auch häufig in Verbindung mit Benutzerinteraktionen vor. Damit ist gemeint, dass die gelesenen Informationen an den Benutzer ausgegeben werden und von ihm verändert werden können. Danach werden die geänderten Informationen zurückgeschrieben. Wenn ein anderer Benutzer dieselben Informationen ändern will, dann kann es sein, dass die Änderungen des ersten Benutzers verloren gehen. Folgendes ist bei Benutzerinteraktion anders, als im Fall ohne:
- Lesen und Schreiben können nicht verschmolzen werden.
- Lesen und Schreiben werden in den meisten Fällen als zwei unabhängige Transaktionen ausgeführt.
- Es muss berücksichtigt werden, dass sich der Benutzer möglicherweise viel Zeit lässt mit der Eingabe (z. B. Mittagspause), was bedeutet, dass viel Zeit zwischen Lesen und Schreiben vergehen kann.
- Während der Benutzerinteraktion kann die Verbindung abbrechen (Netzwerk-Problem, das Programm wird beendet, …).
Verhindern kann man das, indem man vor dem Schreiben überprüft, ob die Daten inzwischen geändert wurden und diesen Lese- und Schreibzugriff in einer einzigen Transaktion zusammenfasst. Dabei muss nicht jeder einzelne Spaltenwert des Datensatzes geprüft werden. Es genügt, zu wissen, ob der Datensatz geändert wurde. Das kann durch eine zusätzliche Spalte mit einer Ganzzahl erreicht werden. Hierdurch wird prüfbar, ob beim Schreibzugriff noch der Wert des Lesezugriffs vor der Datenänderung steht. Ist dem so, muss die nun abschließende Lese-Schreib-Transaktion den Wert in dieser Spalte entsprechend um 1 erhöhen. Gleichzeitig werden auch die anderen Spaltenwerte aktualisiert. Man muss sich also nur eine einzige Ganzzahl gelesener_spaltentestwert merken. In SQL sieht das etwa so aus:
- Der Datensatz wird gelesen um anschließend bearbeitet werden zu können. (gelesener_spaltentestwert muss in einer Variable abgespeichert werden):
SELECT tabelle spalte_1, ..., gelesener_spaltentestwert WHERE id_tabelle;
- Der Datensatz wird verändert und anschließend zurückgeschrieben. (Die Überprüfung auf Änderung durch konkurrierende Zugriffe und der Schreibvorgang werden zusammengefasst):
UPDATE tabelle SET spalte_1=spaltenwert_1, ..., gelesener_spaltentestwert=gelesener_spaltentestwert+1 WHERE spaltentestwert=gelesener_spaltentestwert AND id_tabelle;
- Wird hierbei kein Datensatz gefunden, stimmt gelesener_spaltentestwert nicht mehr und es hat zwischenzeitlich ein Update durch einen anderen Zugriff stattgefunden. Es muss also jetzt eine Fehlerauswertung erfolgen und entsprechend reagiert werden.
- Im Erfolgsfall ist die Bearbeitung des Datensatzes jetzt beendet.
Die Spaltenwerte sind dann natürlich entsprechend der verwendeten Programmiersprache als Variablen anzugeben.