Wunderbare Versionsanmerkung in JPA

Einführung


Also fangen wir an! Was bedeutet die Versionsanmerkung in JPA?

Kurz gesagt, sie ist für die Blockierung in der JPA verantwortlich. Diese Anmerkung löst eines der Probleme, die sich aus parallelen Transaktionen ergeben können.

Welche Probleme können auftreten?


  1. Verlorene Aktualisierungen können in Situationen auftreten, in denen zwei parallel ausgeführte Transaktionen versuchen, dieselben Daten zu aktualisieren.
  2. Dirty Reads treten auf, wenn eine Transaktion Änderungen sieht, die noch nicht von einer anderen Transaktion gespeichert wurden. In solchen Fällen kann ein Problem aufgrund des Rollbacks der zweiten Transaktion auftreten, die Daten wurden jedoch bereits zuerst gelesen.
  3. Nicht wiederholbare Messwerte treten auf, wenn die erste Transaktion Daten empfangen hat und die zweite Transaktion Änderungen daran vorgenommen und diese bis zum Ende der ersten Transaktion erfolgreich festgeschrieben hat. Mit anderen Worten, wenn innerhalb derselben Transaktion dieselbe Anforderung zum Abrufen beispielsweise der gesamten Tabelle unterschiedliche Ergebnisse zurückgibt.
  4. Das Phantomlesen ist ein Problem, das dem Wiederholen von Lesungen ähnelt, mit der Ausnahme, dass eine andere Anzahl von Zeilen zurückgegeben wird.

Kurz über ihre Entscheidungen


  1. READ UNCOMMITED - gelöst mit der Versionsanmerkung in JPA (dies ist genau der Artikel)
  2. READ COMMITED - Ermöglicht das Lesen nur festgeschriebener Änderungen
  3. REPEATABLE READ - hier etwas komplizierter. Unsere Transaktion "sieht keine Änderungen" in den zuvor gelesenen Daten, und andere Transaktionen können die Daten, die in unsere Transaktion fielen, nicht ändern.
  4. SERIALIZABLE - sequentielle Transaktionsausführung

Jeder nachfolgende Absatz deckt alle vorherigen ab, dh er kann die zuvor angegebenen Lösungen ersetzen. Somit hat SERIALIZABLE die höchste Isolationsstufe und READ UNCOMMITED die niedrigste.

Version


Die Version löst das Problem mit verlorenen Updates . Wie genau, jetzt werden wir sehen.

Bevor wir zum Code übergehen, sollten wir erwähnen, dass es zwei Arten von Sperren gibt: optimistisch und pessimistisch . Der Unterschied besteht darin, dass erstere von Situationen geleitet werden, in denen viele Transaktionen versuchen, ein Feld gleichzeitig zu ändern, äußerst selten auftreten, während andere von der entgegengesetzten Situation geleitet werden. Dementsprechend gibt es einen Unterschied in ihrer Ausführungslogik.

Bei optimistischen Sperren wird beim Festschreiben in die Datenbank der Wert des als Version gekennzeichneten Felds zum Zeitpunkt des Datenempfangs und zum Zeitpunkt verglichen. Wenn es sich geändert hat, dh eine andere Transaktion vor unserer liegt und die Daten geändert werden konnten, gibt unsere Transaktion in diesem Fall einen Fehler aus, und Sie müssen sie neu starten.

Wenn Sie optimistische Sperren verwenden, wird beim Zugriff auf die Datenbank ein höheres Maß an Wettbewerbsfähigkeit sichergestellt. In diesem Fall müssen Sie jedoch Transaktionen wiederholen, für die vor anderen keine Zeit für Änderungen vorhanden war.

In pessimistischen Fällen wird die Sperre unmittelbar vor der angeblichen Datenänderung für alle Zeilen verhängt, von denen eine solche Änderung angeblich betroffen ist.

Und wenn pessimistische Sperren verwendet werden, ist das Fehlen von Widersprüchen während der Ausführung der Transaktion garantiert, da der Rest in den Standby-Modus versetzt wird (dies dauert jedoch einige Zeit), wodurch das Wettbewerbsniveau verringert wird.

LockModeType oder wie man eine Sperre setzt


Die Sperre kann durch Aufrufen der Look-Methode von EntityManager festgelegt werden.

entityManager.lock(myObject, LockModeType.OPTIMISTIC); 

LockModeType definiert eine Sperrstrategie.

LockModeType kann aus 6 Typen bestehen (von denen 2 optimistisch und 3 pessimistisch sind ):

  1. KEINE - kein Schloss
  2. OPTIMISTISCH
  3. OPTIMISTIC_FORCE_INCREMENT
  4. PESSIMISTIC_READ
  5. PESSIMISTIC_WRITE
  6. PESSIMISTIC_FORCE_INCREMENT

Erstellen Sie unsere Entität
 import lombok.Getter; import lombok.Setter; import javax.persistence.*; @EntityListeners(OperationListenerForMyEntity.class) @Entity public class MyEntity{ @Version private long version; @Id @GeneratedValue @Getter @Setter private Integer id; @Getter @Setter private String value; @Override public String toString() { return "MyEntity{" + "id=" + id + ", version=" + version + ", value='" + value + '\'' + '}'; } } 


Erstellen wir eine Klasse, in der alle Callback-Methoden implementiert werden.
 import javax.persistence.*; public class OperationListenerForMyEntity { @PostLoad public void postLoad(MyEntity obj) { System.out.println("Loaded operation: " + obj); } @PrePersist public void prePersist(MyEntity obj) { System.out.println("Pre-Persistiting operation: " + obj); } @PostPersist public void postPersist(MyEntity obj) { System.out.println("Post-Persist operation: " + obj); } @PreRemove public void preRemove(MyEntity obj) { System.out.println("Pre-Removing operation: " + obj); } @PostRemove public void postRemove(MyEntity obj) { System.out.println("Post-Remove operation: " + obj); } @PreUpdate public void preUpdate(MyEntity obj) { System.out.println("Pre-Updating operation: " + obj); } @PostUpdate public void postUpdate(MyEntity obj) { System.out.println("Post-Update operation: " + obj); } } 


Main.java
 import javax.persistence.*; import java.util.concurrent.*; //        ,   . public class Main { //  , ..  EntityManagerFactory  ,     . private static EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("ru.easyjava.data.jpa.hibernate"); public static void main(String[] args) { //  10 (  ,       ). ExecutorService es = Executors.newFixedThreadPool(10); try { //  persistFill()   - . persistFill(); for(int i=0; i<10; i++){ int finalI = i; es.execute(() -> { //      updateEntity(finalI) ,  java       .    java -  ,      id,       , id    ,       (  ,      persistFill(),  id       500). updateEntity(finalI); }); } es.shutdown(); try { es.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } finally { entityManagerFactory.close(); } } //         . private static void updateEntity(int index) { //  EntityManager  ,     ,    . EntityManager em = entityManagerFactory.createEntityManager(); MyEntity myEntity = null; try { em.getTransaction().begin(); //        1. myEntity = em.find(MyEntity.class, 1); //   sout,       "" . System.out.println("load = "+index); //       (  LockModeType.*). em.lock(myEntity, LockModeType.OPTIMISTIC); //   Value,  ,        . myEntity.setValue("WoW_" + index); em.getTransaction().commit(); em.close(); System.out.println("--Greeter updated : " + myEntity +" __--__ "+ index); }catch(RollbackException ex){ System.out.println(", =" + myEntity); } } public static void persistFill() { MyEntity myEntity = new MyEntity(); myEntity.setValue("JPA"); EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.persist(myEntity); em.getTransaction().commit(); em.close(); } } 


Zuerst mit der kommentierten updateEntity-Methode ausführen
 Pre-Persistiting operation: MyEntity{id=null, version=0, value='JPA'} Post-Persist operation: MyEntity{id=531, version=0, value='JPA'}  .  id   find   . 


LockModeType.OPTIMISTIC

Dies ist ein optimistischer Block, nun, das ist so logisch. Wie ich oben geschrieben habe, wird der Wert des Versionsfelds verglichen. Wenn er anders ist, wird ein Fehler ausgegeben. Überprüfen Sie dies heraus.

Ergebnisse:
 Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 3 Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 2 Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_2'} Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_3'} Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 9 Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_9'} Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 1 Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_1'} Post-Update operation: MyEntity{id=531, version=1, value='WoW_1'} --Greeter updated : MyEntity{id=531, version=1, value='WoW_1'} __--__ 1 , =MyEntity{id=531, version=0, value='WoW_2'} , =MyEntity{id=531, version=0, value='WoW_3'} Loaded operation: MyEntity{id=531, version=1, value='WoW_1'} load = 4 Pre-Updating operation: MyEntity{id=531, version=1, value='WoW_4'} Post-Update operation: MyEntity{id=531, version=2, value='WoW_4'} --Greeter updated : MyEntity{id=531, version=2, value='WoW_4'} __--__ 4 , =MyEntity{id=531, version=0, value='WoW_9'} Loaded operation: MyEntity{id=531, version=2, value='WoW_4'} load = 0 Pre-Updating operation: MyEntity{id=531, version=2, value='WoW_0'} Post-Update operation: MyEntity{id=531, version=3, value='WoW_0'} --Greeter updated : MyEntity{id=531, version=3, value='WoW_0'} __--__ 0 Loaded operation: MyEntity{id=531, version=3, value='WoW_0'} load = 6 Pre-Updating operation: MyEntity{id=531, version=3, value='WoW_6'} Post-Update operation: MyEntity{id=531, version=4, value='WoW_6'} Loaded operation: MyEntity{id=531, version=4, value='WoW_6'} load = 5 Pre-Updating operation: MyEntity{id=531, version=4, value='WoW_5'} Post-Update operation: MyEntity{id=531, version=5, value='WoW_5'} --Greeter updated : MyEntity{id=531, version=4, value='WoW_6'} __--__ 6 --Greeter updated : MyEntity{id=531, version=5, value='WoW_5'} __--__ 5 Loaded operation: MyEntity{id=531, version=5, value='WoW_5'} load = 7 Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_7'} Post-Update operation: MyEntity{id=531, version=6, value='WoW_7'} Loaded operation: MyEntity{id=531, version=5, value='WoW_5'} load = 8 Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_8'} --Greeter updated : MyEntity{id=531, version=6, value='WoW_7'} __--__ 7 , =MyEntity{id=531, version=5, value='WoW_8'} 


Beobachtungen: Wie Sie den Ergebnissen entnehmen können, wurden die Streams 3, 2, 9 und 1 als erste geladen, und die Rückrufmethoden vor dem Update wurden für sie aufgerufen. Der erste Thread, in dem die Post-Update-Methode aufgerufen wurde, war 1, wie Sie aus den Ergebnissen ersehen können. Das mit der Versionsanmerkung gekennzeichnete Feld wurde bereits geändert (um 1 erhöht). Dementsprechend warfen alle verbleibenden Threads 2, 3, 9 eine Ausnahme. Usw. Das Ergebnis ist value = WoW_7, version = 6. Tatsächlich war das letzte Post-Update in Thread 7 mit version = 6.

LockModeType.OPTIMISTIC_FORCE_INCREMENT

Es funktioniert nach dem gleichen Algorithmus wie LockModeType.OPTIMISTIC, außer dass nach dem Festschreiben der Wert des Versionsfelds zwangsweise um 1 erhöht wird. Infolgedessen wird das Feld nach jedem Festschreiben um 2 erhöht (die Erhöhung, die in Post-Update + erzwungene Erhöhung zu sehen ist). . Frage Warum? Wenn wir nach einem Commit dieselben Daten "beschwören" möchten und keine Transaktionen von Drittanbietern benötigen, die zwischen dem ersten Commit und dem Abschluss unserer Transaktion unterbrochen werden könnten.

Wichtig! Wenn Sie versuchen, die Daten in dieselben zu ändern, werden in diesem Fall die Methoden Pre-Update und Post-Update nicht aufgerufen. Es kann zum Zusammenbruch aller Transaktionen kommen. Zum Beispiel wurden mehrere Transaktionen parallel zu uns gelesen, aber da Aufrufe der Pre- und Post-Methode (Update) einige Zeit in Anspruch nehmen, wird die Transaktion, die versucht, die Daten (in dieselbe) zu ändern, sofort ausgeführt. Dies führt zu einem Fehler der verbleibenden Transaktionen.

LockModeType.PESSIMISTIC_READ, LockModeType.PESSIMISTIC_WRITE und LockModeType.PESSIMISTIC_FORCE_INCREMENT

Da die Arbeit der verbleibenden Arten von Sperren ähnlich aussieht, werde ich auf einmal darüber schreiben und das Ergebnis nur von PESSIMISTIC_READ betrachten.

LockModeType.PESSIMISTIC_READ - pessimistische Lesesperre.
LockModeType.PESSIMISTIC_WRITE - pessimistische Schreibsperre (und Lesen).
LockModeType.PESSIMISTIC_FORCE_INCREMENT - pessimistische Schreibsperre (und Lesen) mit erzwungener Vergrößerung des Versionsfelds.

Infolge solcher Sperren kann ein langes Warten auf eine Sperre auftreten, was wiederum zu einem Fehler führen kann.

Ergebnis auf LockModeType.PESSIMISTIC_READ (nicht vollständig dargestellt):
 load = 0 Pre-Updating operation: MyEntity{id=549, version=5, value='WoW_0'} Post-Update operation: MyEntity{id=549, version=6, value='WoW_0'} Loaded operation: MyEntity{id=549, version=6, value='WoW_0'} load = 8 Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_8'} Loaded operation: MyEntity{id=549, version=6, value='WoW_0'} load = 4 Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_4'} ... ERROR: :   :  22760    ExclusiveLock  " (0,66)  287733   271341";   20876.  20876    ShareLock  " 8812";   22760. 


Infolgedessen blockierten sich die Threads 4 und 8 gegenseitig, was zu einem unlösbaren Konflikt führte. Zuvor hat niemand Thread 0 gestört. Eine ähnliche Situation mit allen Threads bis 0.

Source: https://habr.com/ru/post/de434836/


All Articles