Annotation de Wonderful Version dans JPA

Présentation


Commençons donc! Que signifie l' annotation de version dans JPA?

En bref, elle est responsable du blocage dans l'APP. Cette annotation résout l'un des problèmes qui peuvent survenir à la suite de transactions parallèles.

Quels problèmes peuvent survenir?


  1. Des mises à jour perdues peuvent se produire dans des situations où deux transactions s'exécutant en parallèle tentent de mettre à jour les mêmes données.
  2. Des lectures incorrectes se produisent lorsqu'une transaction voit des modifications qui n'ont pas encore été enregistrées par une autre transaction. Dans de tels cas, un problème peut se produire en raison de la restauration de la deuxième transaction, mais les données ont déjà été lues en premier.
  3. Des lectures non reproductibles se produisent lorsque la première transaction a reçu des données et que la deuxième transaction les a modifiées et les a validées avec succès jusqu'à la fin de la première transaction. En d'autres termes, lorsque dans la même transaction la même demande pour obtenir, par exemple, la table entière, renvoie des résultats différents.
  4. La lecture fantôme est un problème similaire à la répétition de lectures, à l'exception qu'un nombre différent de lignes est renvoyé.

En bref sur leurs décisions


  1. LIRE UNCOMMITED - résolu en utilisant l'annotation de version dans JPA (c'est exactement l'article)
  2. READ COMMITED - permet de lire uniquement les modifications validées
  3. REPEATABLE READ - un peu plus compliqué ici. Notre transaction «ne voit pas» les changements dans les données qu'elle a précédemment lues, et d'autres transactions ne peuvent pas changer les données qui sont tombées dans notre transaction.
  4. SERIALIZABLE - exécution séquentielle de transaction

Chaque paragraphe suivant couvre tous les précédents, en d'autres termes, il peut remplacer les solutions indiquées précédemment. Ainsi, SERIALIZABLE a le niveau d'isolement le plus élevé et READ UNCOMMITED le plus bas.

La version


La version résout le problème des mises à jour perdues . Comment exactement, maintenant nous verrons.

Avant de passer au code, il convient de mentionner qu'il existe deux types de verrous: optimiste et pessimiste . La différence est que les premiers sont guidés par des situations dans lesquelles de nombreuses transactions tentent de changer un champ en même temps, se produisent extrêmement rarement, tandis que d'autres sont guidées par la situation opposée. Conformément à cela, il existe une différence dans leur logique d'exécution.

Dans les verrous optimistes , lors de la validation dans la base de données, la valeur du champ marqué comme version est comparée au moment où les données sont reçues et au moment. Si elle a changé, c'est-à-dire qu'une autre transaction est en avance sur la nôtre et a réussi à modifier les données, alors dans ce cas, notre transaction génère une erreur et vous devez la redémarrer.

Lorsque vous utilisez des verrous optimistes , un niveau de compétitivité plus élevé est garanti lors de l'accès à la base de données, mais dans ce cas, vous devez répéter les transactions qui n'ont pas eu le temps de faire des modifications avant les autres.

Dans les cas pessimistes , le verrou est imposé immédiatement avant la modification de données présumée sur toutes les lignes qu'une telle modification est censée affecter.

Et lorsque vous utilisez des verrous pessimistes , l'absence de contradictions lors de l'exécution de la transaction est garantie, car le reste est en mode veille (mais cela prend du temps), ce qui réduit le niveau de concurrence.

LockModeType ou comment définir un verrou


Le verrou peut être défini en appelant la méthode look d'EntityManager.

entityManager.lock(myObject, LockModeType.OPTIMISTIC); 

LockModeType définit une stratégie de verrouillage.

LockModeType peut être de 6 types (dont 2 optimistes et 3 pessimistes ):

  1. AUCUN - pas de serrure
  2. OPTIMISTIQUE
  3. OPTIMISTIC_FORCE_INCREMENT
  4. PESSIMISTIC_READ
  5. PESSIMISTIC_WRITE
  6. PESSIMISTIC_FORCE_INCREMENT

Créer notre entité
 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 + '\'' + '}'; } } 


Créons une classe où toutes les méthodes de rappel seront implémentées.
 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(); } } 


Première exécution avec la méthode updateEntity commentée
 Pre-Persistiting operation: MyEntity{id=null, version=0, value='JPA'} Post-Persist operation: MyEntity{id=531, version=0, value='JPA'}  .  id   find   . 


LockModeType.OPTIMISTIC

C'est un bloc optimiste , eh bien, c'est tellement logique. Comme je l'ai écrit ci-dessus, la valeur du champ de version est comparée; si elle est différente, une erreur est générée. Regardez ça.

Résultats:
 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'} 


Observations: Comme vous pouvez le voir sur les résultats, les flux 3, 2, 9 et 1 ont été les premiers à se charger, et les méthodes de rappel pré-mise à jour ont été appelées pour eux. Le premier thread où la méthode Post-Update a été appelée était 1, comme vous pouvez le voir dans les résultats, le champ marqué avec l'annotation de version a déjà été modifié (augmenté de 1). Par conséquent, tous les threads 2, 3, 9 restants ont levé une exception. Et ainsi de suite. Le résultat est value = WoW_7, version = 6. En effet, la dernière post-mise à jour était dans le thread 7 avec version = 6.

LockModeType.OPTIMISTIC_FORCE_INCREMENT

Il fonctionne selon le même algorithme que LockModeType.OPTIMISTIC, sauf qu'après validation la valeur du champ Version est forcée augmentée de 1. Par conséquent, après chaque validation, le champ augmentera de 2 (l'augmentation qui peut être observée dans Post-Update + augmentation forcée) . Question Pourquoi? Si, après un commit, nous voulons «conjurer» les mêmes données, et nous n'avons pas besoin de transactions tierces qui pourraient rompre entre le premier commit et la clôture de notre transaction.

Important! Si vous essayez de modifier les données de la même manière, dans ce cas, les méthodes de pré-mise à jour et de post-mise à jour ne seront pas appelées. Un effondrement de toutes les transactions peut se produire. Par exemple, plusieurs transactions ont été lues en parallèle avec nous, mais comme les appels aux méthodes pré et post (mise à jour) prennent du temps, la transaction qui essaie de changer les données (de la même) sera immédiatement exécutée. Cela entraînera une erreur des transactions restantes.

LockModeType.PESSIMISTIC_READ, LockModeType.PESSIMISTIC_WRITE et LockModeType.PESSIMISTIC_FORCE_INCREMENT

Étant donné que le travail des types de verrous restants semble similaire, je vais donc écrire tout à la fois et ne considérer le résultat que par PESSIMISTIC_READ.

LockModeType.PESSIMISTIC_READ - verrou de lecture pessimiste .
LockModeType.PESSIMISTIC_WRITE - verrou d'écriture pessimiste (et lecture).
LockModeType.PESSIMISTIC_FORCE_INCREMENT - verrou d'écriture pessimiste (et lecture) avec augmentation forcée du champ Version.

À la suite de ces verrous, une longue attente pour un verrou peut se produire, ce qui peut entraîner une erreur.

Résultat sur LockModeType.PESSIMISTIC_READ (non présenté dans son intégralité):
 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. 


En conséquence, les fils 4 et 8 se sont bloqués, ce qui a conduit à un conflit insoluble. Avant cela, personne n'a interféré avec le thread 0. Une situation similaire avec tous les threads jusqu'à 0.

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


All Articles