synchronisierte Zugriffe auf die gleiche Entity (JPA)

JanHH

Top Contributor
Hallo,

habe ein Problem ähnlich diesem hier:

http://www.java-forum.org/data-tier/103607-jpa-pessimistic-lock-funktioniert.html

(hatte auch schonmal sowas gefragt, aber da kamen irgendwie keine Antworten, die mir weitergeholfen haben).

Also als Beispiel (welches auch der praktischen Anwendung recht nahe kommt), soll ein Zähler implementiert werden, und in der Webanwendung gibts einfach einen Button, um diesen um eins zu erhöhen. Also lesen, +1, wieder schreiben. Nun kanns ja sein, dass da zwei Zugriffe quasi gleichzeitig kommen. Dann hat man wohl die "der letzte gewinnt"-Situation:

Transaktion 1 liest Wert (14), dann liest Transaktion 2 den gleichen Wert, T1 schreibt (15), T2 schreibt auch (15). Wobei der korrekte Wert für T2 allerdings 16 wäre.

Man müsste diese Zugriffe nun also, ganz klassischen wie Methoden, synchronisieren. Das so zu machen wäre auch der letzte Notnagel, aber eigentlich ist es das Ziel, das in der Datenbank zu synchronisieren.

Habs nun soweit hinbekommen, die Entity-Klasse "versioned" zu machen, was anscheinend notwendig ist, damit entityManager.lock(...) überhaupt funktioniert. Nun gut. Damit kann man den erwähnten Fall immerhin verhindern, bei T2 wird dann eine Exception geworfen, die man auch brav abfangen und auswerten kann. Hilft aber auch nur halb weiter, denn T2 soll ja einen Wert (16) bekommen. Für die Funktionsweise der Anwendung ist es notwendig, dass der Prozess, der T2 aufruft, mit dem korrekten Wert weiterarbeitet. Einfach nur der Hinweis "sorry fehlgeschlagen" ist nutzlos.

Was also tun? In einer Endlosschleife die Transaktion so lange aufrufen und Exceptions abfangen, bis es endlich mal klappt (was in den allermeisten Fällen beim ersten Mal der Fall sein dürfte, aber es muss halt SICHER sein)?

Gibts da irgendwelchen SQL- oder JPA-Code für, der die Transaktionen quasi hintereinander reiht? Würde es helfen, für die Datenbank (PostgreSQL 8.4, die ist bei sowas wohl recht leistungsfähig) eine stored procedure in java zu schreiben, die sowas dann automatisch erledigt? Oder wie macht man das !?

Ich meine, der Anwendungsfall ist ja nun nicht gerade exotisch.. sowas gibts doch andauernd. Da muss es doch irgendeine Standardlösung für geben!

Und das im Programm zu synchronisieren ist keine gute Lösung, weil es möglich sein soll, mit mehreren Servern gleichzeitig auf die Datenbank zuzugreifen.

Gruß+Danke
Jan
 

JanHH

Top Contributor
Ist wohl keinThema für Sonntag, was? Aber weiss wirklich niemand was zu dieser, wie ich finde, recht alltäglichen Aufgabenstellung zu sagen...?
 

JanHH

Top Contributor
Wenn niemand weiss wies geht wäre es zumindest schön wenn ein "Experte" mir bestätigen würde, dass es doch nicht "so einfach" ist. Stehe da grad etwas im regen.. höflich nachfrag.
 
M

maki

Gast
Hi JanHH,

Transaktion 1 liest Wert (14), dann liest Transaktion 2 den gleichen Wert, T1 schreibt (15), T2 schreibt auch (15). Wobei der korrekte Wert für T2 allerdings 16 wäre.
dafür reicht imho Optimistic Locking, in JPA per @Version umgesetzt, zumindest gäbe esm dann einen Fehler für den User.

Man müsste diese Zugriffe nun also, ganz klassischen wie Methoden, synchronisieren.
Bei Pessemistic Locking keine gute Idee imho, wenn da ein paar Threads in der Schlange stehen und die Transaktionen "lange" offen sind kann das schon mal dauern.
Trotzdem sollte dass doch funktionieren?
Welchen LockModeType verwendest du denn? So ein bisserl Code könnte mehr Leute annimieren ihren Senf dazu zu geben ;)

Und das im Programm zu synchronisieren ist keine gute Lösung, weil es möglich sein soll, mit mehreren Servern gleichzeitig auf die Datenbank zuzugreifen.
... und spätestens hier ist dann Schluss lustig :joke:
Dazu braucht es imho einen Zusatz wie Terracotta oder ähnliches, ist weder alltäglich noch trivial wenn ein ORM mit (inkl. Cache) Spiel ist. Ansonsten eben eine zusätzliche Schicht vor die DB setzen (JEE).

Pessemistic Locking hat Riesennachteile für die Skalierbarkeit, da es zum Flaschenhals werden kann.
 
Zuletzt bearbeitet von einem Moderator:

JanHH

Top Contributor
Danke für die Antwort!

Ich verwende seam 2 und deshalb JPA 1.0 und da stehen als LockModeType nur READ und WRITE zur Verfügung. Habs mit beidem ausprobiert, also Code in etwa so:
Java:
Object o=entityManager.find(...);
entityManager.lock(LockModeType.<READ/WRITE>);
...
an sich trivial.

In der entity eine int-Property mit @Version.

Es funktioniert soweit, dass mit beide LockModeType's die besagte Fehlermeldung für den User kommt (in etwa "Daten wurden von einem anderen User verändert"), aber das hilft halt nicht wirklich weiter. Alle Prozesse, die darauf zugreifen, sollen ja (korrekt) weiterlaufen, und keine Exceptions melden. Und dazu fällt mir zumindest nur ein, es einfach in einer Endlosschleife immer wieder zu versuchen, bis es dann mal klappt.. was ja im Allgemeinen recht schnell der Fall sein müsste.
 
Zuletzt bearbeitet:

mvitz

Top Contributor
Was willst du auch anderes machen, wenn ein Fehler auftritt,

1) Abbrechen und dem User den Fehler anzeigen --> hast du ausgeschlossen
2) Alternative gehen --> geht anscheinend in deinem Fall auch nicht
3) Nochmal probieren
4) Fehler im vorhinen vermeiden --> Synchronisation

Andere Möglichkeit seh ich da jetzt nicht.
 

Jay_030

Aktives Mitglied
Also das hört sich nach dem klassischen Anwendungsfall für Optimistic Locking an: Wenn während dem Update des Wertes, dieser überschrieben wird, soll eine Exception geworfen werden. Dann kannst du darauf reagieren und den Wert neu aus der DB laden. Das Update kann danach erneut durchgeführt werden. Was ist daran so schlimm?

Btw: Ich kenne das von NHibernate (.Net Ableger von Hibernate) so, dass man explizit ein SelectForUpdate machen kann und NHibernate die Entity entsprechend in der Datenbank sperrt. Ein gleichzeitiges SelectForUpdate würde bei einem der Datenbankzugriffe zu eienr Exception führen. Dadurch kann man dann angemessen darauf reagieren. Im Idealfall soll das SelectForUpdate eh nur für kurzlebige Transaktionen genutzt werden.
 

JanHH

Top Contributor
Also das hört sich nach dem klassischen Anwendungsfall für Optimistic Locking an: Wenn während dem Update des Wertes, dieser überschrieben wird, soll eine Exception geworfen werden. Dann kannst du darauf reagieren und den Wert neu aus der DB laden. Das Update kann danach erneut durchgeführt werden. Was ist daran so schlimm?

Da ist nichts dran "schlimm", meine Frage war ja nur, "ist das dann der richtige Weg". Ist es aber dann wohl offenbar. Danke. Wie gesagt, für mich alles relativ neue Themen, bin daher unsicher wie man sowas macht. Aber das läuft dann ja auf die Variante "Endlosschleife" hinaus. Versuchen, wenns scheitert exception abfangen und nochmal versuchen. Und nach 25 Versuchen aufgeben.
 
M

maki

Gast
k.A. was du mit Endlosschleife meinst, aber imho ist es sehr verkehrt, , zu versuchen einen älteren Datensatz über einen neueren zu "drücken", meist ist das nicht was der User wollte bzw. was die Datenkonsistenz verlangt.

Bei Optimistic Locking geht man eben davon aus, dass der User einen Fehler bekommt und seine Aktion ggf. mit akutellen Daten wiederholen muss.
Bei Pessemistic Locking lässt man wirklich nur einen einzigen User einen Datensatz ändern, alle anderen kommen nicht in den Genuss etwas zu editieren/speichern solange er Datensatz nicht wieer freigegeben wurde.
 

mvitz

Top Contributor
Evtl. kann man einen Art Zähler sogar über die DB und ein Auto-Increment erledigen. Weiß dann nur nicht, inwiefern man so etwas per JPA machen kann ohne die DB-Unabhängigkeit zu verlieren.
 

Jay_030

Aktives Mitglied
k.A. was du mit Endlosschleife meinst, aber imho ist es sehr verkehrt, , zu versuchen einen älteren Datensatz über einen neueren zu "drücken", meist ist das nicht was der User wollte bzw. was die Datenkonsistenz verlangt.

Bei Optimistic Locking geht man eben davon aus, dass der User einen Fehler bekommt und seine Aktion ggf. mit akutellen Daten wiederholen muss.
Bei Pessemistic Locking lässt man wirklich nur einen einzigen User einen Datensatz ändern, alle anderen kommen nicht in den Genuss etwas zu editieren/speichern solange er Datensatz nicht wieer freigegeben wurde.
Wenn der Fehler kommt, muss man den Datensatz natürlich aus der DB neuladen, dann inkrementieren (also updaten) und versuchen wieder reinzuschreiben.

Aber stimmt schon: Optimistic Locking ist eigentlich für eine sog. Business Transaction gedacht. Also der gesamte Prozess, in dem ein User Änderungen an den Daten vornimmt und lesend auf diese zugreift. Und da dieser Zeitraum durchaus mehrere Minuten bis hin zu Stunden dauern kann, war das normale Locking nicht mehr praktikabel.

Evtl. kann man einen Art Zähler sogar über die DB und ein Auto-Increment erledigen. Weiß dann nur nicht, inwiefern man so etwas per JPA machen kann ohne die DB-Unabhängigkeit zu verlieren.
Sowas geht meines Wissens nach nur mit einer Stored Procedure. Wobei ich gerne wüsste, wie es mittels JPA geht. ;)
 
Zuletzt bearbeitet:
M

maki

Gast
Wenn der Fehler kommt, muss man den Datensatz natürlich aus der DB neuladen, dann inkrementieren (also updaten) und versuchen wieder reinzuschreiben.
Vielleciht verstehe ich dich nur falsch...
Wenn der Fehler auftritt, sieht das der User.
Wenn der User sich dann entscheidet, den Datensatz nochmals zu editieren & speichern, kann er das natürlich.
Automatisch darf nicht versucht werden trotzdem zu speichern (neu lesen, Änderungen mergen & wieder speichern), da sonst inkonsistenzen entstehen können (man denke an Validierung von Werten die nur in bestimmten Konstellationen gültig sind).

Aber stimmt schon: Optimistic Locking ist eigentlich für eine sog. Business Transaction gedacht.
Naja, nicht unbedingt, Pessemistic Locking ist auch für Businessanwendungen wichtig.
Der Unterschied ist der Aufwand, einmal für den User, denn wenn er dafür sehr lange braucht, kann man ihm nicht zumuten dass er unter Umständen zu langsam war und jemand anderes schneller... Pessemistic Locking dagegen ist Anspruchsvoll an das System, es kann zum Flaschenhals werden, etc.pp.

Hab schon Systeme gesehen die über 15 Jahre alt waren (Natural & ADABAS C) und Pessemistic (offline & online) Locking unterstützt haben, da hat der User auch eine Fehlermeldung bekommen wenn er einen Datensatz in der "Bearbeitungsmaske" bearbeiten wollte obwohl ein anderer User den Datensatz schon in Bearbeitung hatte.
 
Zuletzt bearbeitet von einem Moderator:

Jay_030

Aktives Mitglied
Ich habe JanHH so verstanden, dass das ein einfach hochgezählter Wert ist.

Aber du hast Recht, der User soll entscheiden. Das ist die eigentliche Intention bei Optimistic Locking. Aber das automatische Neuladen (da muss man evt. das Session-Objekt ein wenig "austricksen", damit es auch wirklich die neue Version aus der DB lädt, vll vorher noch die alte Entity ausm Cache werfen) und dann nochmal versuchen zu speichern, führt zu keinen Inkonsistenzen. Im schlimmsten Fall war wieder jmd schneller und die Version ist erneut größer der unseren. Dann müssten wir das nochmal probieren.

Je mehr ich hier jetzt schreibe, desto sicherer bin ich mir, dass das mittels einfachem Write-Lock erledigt ist. Die Transaktion darf halt in diesem Fall nicht viel länger sein, als dieses Select mit Update. Wie gesagt, LockMode.Upgrade ist das schöne SelectForUpdate bei NHibernate. Das ist so ziemlich genau dafür vorgesehen. Aber ka, ob und wenn, wie es diese Funktionalität bei JPA gibt.
 
Zuletzt bearbeitet:

JanHH

Top Contributor
Man kann sich den Anwendungsfall in etwa so vorstellen.. jeder User bekommt eine eindeutige Nummer zugewiesen, und die Nummer werden automatisch hochgezählt. Oder wenn ein User fertig ist und sich ausloggt, wird zu jedem User eine Entity mit einem eindeutige Zähler, quasi "Geschäftsvorfall Nr. xyz" gespeichert. Letzteres trifft es eigentlich besser. Die Nummern müssen alle aufsteigend und eindeutig sein, und der User ist ja bereits fertig, es ist also kein User mehr da, dem man eine Fehlermeldung und den Hinweis "bitte erneut versuchen" anzeigen kann. Es muss also irgendwie anders funktionieren.

Mit "Endlosschleife" meine ich einfach.. wenn der Versuch, den Zähler zu erhöhen, zu einer locking-bedingten Exception führt, wird es einfach erneut versucht.. solange bis es halt klappt (was meistens, wie gesagt, schnell der Fall sein dürfte), oder bis nach n Versuchen dann doch irgendwann abgebrochen wird, weil die Software ja nicht in einer wirklichen Endlosschleife feststecken soll.

Die Zugriffe sind dabei zeitlich gesehen natürlich sehr kurz.. Wert lesen, um eins erhöhen, update. Es wird in der Praxis nur ziemlich selten zu konkurrierenden Zugriffen auf den Datenbankeintrag kommen, aber unmöglich ist es halt nicht (und muss daher irgendwie abgefangen werden).
 

Jay_030

Aktives Mitglied
Die Nummern müssen alle aufsteigend und eindeutig sein, und der User ist ja bereits fertig, es ist also kein User mehr da, dem man eine Fehlermeldung und den Hinweis "bitte erneut versuchen" anzeigen kann. Es muss also irgendwie anders funktionieren.

Mit "Endlosschleife" meine ich einfach.. wenn der Versuch, den Zähler zu erhöhen, zu einer locking-bedingten Exception führt, wird es einfach erneut versucht.. solange bis es halt klappt [...]

Die Zugriffe sind dabei zeitlich gesehen natürlich sehr kurz.. Wert lesen, um eins erhöhen, update. Es wird in der Praxis nur ziemlich selten zu konkurrierenden Zugriffen auf den Datenbankeintrag kommen, aber unmöglich ist es halt nicht (und muss daher irgendwie abgefangen werden).
Am einfachsten wäre es, das mittels Stored Procedure zu lösen, aber es soll ja reines JPA sein. Ich bleibe bei meiner Meinung von gestern: Write-Lock setzen und Isolation-Level auf Read Committed. Dann muss ein konkurrierender Write-Lock warten, bis die andere Transaktion fertig ist. Dafür ist doch Pessimistic Locking da. Die Transaktion darf halt nur das Select und Update enthalten. Sollte funktionieren.
 

cr4ch

Mitglied
Jetzt mal ne Frage
Du willst doch "nur" einen fortlaufenden Counter erstellen
und das Problem, das du hast kommt nur, wenn zwei User versuchen "gleichzeitig" einen Wert zu speichern (in einer Transaktion)
oder versteh ich das falsch?
Muss das ganze über die JPA/Datenbank realisiert werden?

Klingt für mich irgendwie nach einem einfachen Counter in Java
Java:
public synchronizes int nextToken() {
	if (token == 0) {
		// initialisiere, z.B. lade den aktuellen Wert aus der Datenbank
	}
	return ++token;
}
so hättest du "egal was komme" eine eindeutige Nummer und die kannst du dann "ohne Aufwand" per JPA speichern.

Geetings
 

JanHH

Top Contributor
So ähnlich ist es bisher gelöst, aber nun solls halt in einer Datenbank stehen (der Wert soll ja nicht nur gezählt sondern auch irgendwo gespeichert sein), und da es möglich sein soll, mit mehreren Servern gleichzeitig auf die Datenbank zuzugreifen (da load balancing und Skalierbarkeit eine Anforderung an die Anwendung ist), kommt natürlich nur eine Synchronisation innerhalb der Datenbank in Frage.

@Jay_030: Es muss nicht nur reines JPA sein, aber ich fürchte, alles andere überfordert mich ;-). Und bei JPA 1.0 gibts offenbar nur die Möglichkeit, die Entity "versioned" zu machen, und dann ein Lock vom Typ LockModeType.READ und LockModeType.WRITE zu setzen. Habs mit beidem probiert, in beiden Fällen kommt bei konkurrierenden Zugriffen eine Exception. Ich denke, ich sollte mich mal etwas tiefer in die Datenbank-Materie einarbeiten..

Danke erstmal an alle!
 

Jay_030

Aktives Mitglied
Wenn du dich jetzt in die Datenbank-Geschichte einarbeitest, konkretisiere bzw. ändere ich das mit der Stored Procedure zu einem Before/After Update Trigger, der vor oder nach einem Update den Wert in der Zeile inkrementiert. Wichtig dabei ist, dass der Trigger noch im Transaktionskontext läuft. Das musst du dann in der DBMS-Doku deines Vertrauens nachlesen, wie es sich dort verhält.
 

cr4ch

Mitglied
Alternativ kannst du auch eine Stored Procedure aufrufen, die dir einen Wert zurück liefert.
Dann brauchst du dir keine großen gadanken machen, ob before, after, ... sondern bekommst den Wert von der DB
 

Oben