Verrouillage pessimiste dans l’APP

Verrouillage pessimiste dans l'APP

1. Vue d'ensemble

Il existe de nombreuses situations dans lesquelles nous souhaitons extraire des données d'une base de données. Parfois, nous voulons le verrouiller pour un traitement ultérieur afin que personne d'autre ne puisse interrompre nos actions.

Nous pouvons penser à deux mécanismes de contrôle de concurrence qui nous permettent de le faire: définir le niveau d'isolation des transactions approprié ou définir un verrou sur les données dont nous avons besoin pour le moment.

L'isolation de transaction est définie pour les connexions à la base de données. Nous pouvons le configurer pour conserver les différents degrés de verrouillage des données.

Cependant,the isolation level is set once the connection is created et cela affecte toutes les instructions de cette connexion. Heureusement, nous pouvons utiliser le verrouillage pessimiste qui utilise des mécanismes de base de données pour réserver un accès exclusif plus granulaire aux données.

Nous pouvons utiliser un verrou pessimiste pour nous assurer qu'aucune autre transaction ne peut modifier ou supprimer les données réservées.

Nous pouvons conserver deux types de verrous: un verrou exclusif et un verrou partagé. Nous pourrions lire mais pas écrire dans les données lorsque quelqu'un d'autre détient un verrou partagé. Afin de modifier ou de supprimer les données réservées, nous avons besoin d'un verrou exclusif.

Nous pouvons acquérir des verrous exclusifs en utilisant les instructions «SELECT … FOR UPDATE».

2. Modes de verrouillage

La spécification JPA définit trois modes de verrouillage pessimistes dont nous allons parler:

  • PESSIMISTIC_READ - nous permet d'obtenir un verrou partagé et d'empêcher la mise à jour ou la suppression des données

  • PESSIMISTIC_WRITE - nous permet d'obtenir un verrou exclusif et d'empêcher les données d'être lues, mises à jour ou supprimées

  • PESSIMISTIC_FORCE_INCREMENT - fonctionne commePESSIMISTIC_WRITE et incrémente en plus un attribut de version d'une entité versionnée

Tous sont des membres statiques de la classeLockModeType et permettent aux transactions d'obtenir un verrou de base de données. Ils sont tous conservés jusqu'à ce que la transaction soit validée ou annulée.

Il convient de noter que nous ne pouvons obtenir qu’un seul verrou à la fois. Si c'est impossible, unPersistenceException est lancé.

2.1. PESSIMISTIC_READ

Chaque fois que nous voulons simplement lire des données et ne pas rencontrer de lectures sales, nous pouvons utiliserPESSIMISTIC_READ (verrou partagé). We won’t be able to make any updates or deletes though.

Il arrive parfois que la base de données que nous utilisons ne prenne pas en charge le verrouPESSIMISTIC_READ, il est donc possible que nous obtenions le verrouPESSIMISTIC_WRITE à la place.

2.2. PESSIMISTIC_WRITE

Toute transaction qui doit acquérir un verrou sur les données et y apporter des modifications doit obtenir le verrouPESSIMISTIC_WRITE. Selon la spécification deJPA, maintenir le verrou dePESSIMISTIC_WRITE empêchera d'autres transactions de lire, mettre à jour ou supprimer les données.

Veuillez noter que certains systèmes de base de données implémententmulti-version concurrency control qui permet aux lecteurs de récupérer des données qui ont déjà été bloquées.

2.3. PESSIMISTIC_FORCE_INCREMENT

Ce verrou fonctionne de la même manière quePESSIMISTIC_WRITE, mais il a été introduit pour coopérer avec les entités versionnées - entités qui ont un attribut annoté avec@Version.

Toutes les mises à jour d'entités versionnées peuvent être précédées de l'obtention du verrouPESSIMISTIC_FORCE_INCREMENT. Acquiring that lock results in updating the version column.

C'est à un fournisseur de persistance de déterminer s'il prend en chargePESSIMISTIC_FORCE_INCREMENT pour les entités non versionnées ou non. Si ce n’est pas le cas, il lance lesPersistanceException.

2.4. Exceptions

Il est bon de savoir quelle exception peut se produire lorsque vous travaillez avec un verrouillage pessimiste. La spécificationJPA fournit différents types d'exceptions:

  • PessimisticLockException - indique que l'obtention d'un verrou ou la conversion d'un verrou partagé en verrou exclusif échoue et entraîne une annulation au niveau de la transaction

  • LockTimeoutException –  indique que l'obtention d'un verrou ou la conversion d'un verrou partagé en exclusivité expire et entraîne une annulation au niveau de l'instruction

  • PersistanceException – indique qu'un problème de persistance s'est produit. PersistanceException et ses sous-types, saufNoResultException,NonUniqueResultException,LockTimeoutException etQueryTimeoutException, marks the active transaction to be rolled back.

3. Utiliser des serrures pessimistes

There are a few possible ways to configure a pessimistic lock on a single record or group of records. Voyons comment le faire dans JPA.

3.1. Find

C’est probablement le moyen le plus simple. Il suffit de passer un objetLockModeType comme paramètre à la méthodefind:

entityManager.find(Student.class, studentId, LockModeType.PESSIMISTIC_READ);

3.2. Requete

De plus, nous pouvons également utiliser un objetQuery et appeler le settersetLockMode avec un mode de verrouillage comme paramètre:

Query query = entityManager.createQuery("from Student where studentId = :studentId");
query.setParameter("studentId", studentId);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
query.getResultList()

3.3. Verrouillage explicite

Il est également possible de verrouiller manuellement les résultats récupérés par la méthode find:

Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.lock(resultStudent, LockModeType.PESSIMISTIC_WRITE);

3.4. Rafraîchir

Si nous voulons écraser l'état de l'entité par la méthoderefresh , nous pouvons également définir un verrou:

Student resultStudent = entityManager.find(Student.class, studentId);
entityManager.refresh(resultStudent, LockModeType.PESSIMISTIC_FORCE_INCREMENT);

3.5. NamedQuery

L'annotation@NamedQuery nous permet également de définir un mode de verrouillage:

@NamedQuery(name="lockStudent",
  query="SELECT s FROM Student s WHERE s.id LIKE :studentId",
  lockMode = PESSIMISTIC_READ)

4. Verrouiller la portée

Lock scope parameter defines how to deal with locking relationships of the locked entity. Il est possible d’obtenir un verrou sur une seule entité définie dans une requête ou de bloquer en plus ses relations.

Pour configurer la portée, nous pouvons utiliser l'énumérationPessimisticLockScope. Il contient deux valeurs:NORMAL etEXTENDED.

Nous pouvons définir la portée en passant un paramètre 'javax.persistance.lock.scope' avec la valeurPessimisticLockScope comme argument à la méthode appropriée deEntityManager,Query,TypedQuery ouNamedQuery:

Map properties = new HashMap<>();
map.put("javax.persistence.lock.scope", PessimisticLockScope.EXTENDED);

entityManager.find(
  Student.class, 1L, LockModeType.PESSIMISTIC_WRITE, properties);

4.1. PessimisticLockScope.NORMAL

We should know that the PessimisticLockScope.NORMAL is the default scope. Avec cette étendue de verrouillage, nous verrouillons l'entité elle-même. Utilisé avec l'héritage joint, il verrouille également les ancêtres.

Examinons l'exemple de code avec deux entités:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {

    @Id
    private Long id;
    private String name;
    private String lastName;

    // getters and setters
}

@Entity
public class Employee extends Person {

    private BigDecimal salary;

    // getters and setters
}

Lorsque nous voulons obtenir un verrou sur lesEmployee, nous pouvons observer la requêteSQL qui s'étend sur ces deux entités:

SELECT t0.ID, t0.DTYPE, t0.LASTNAME, t0.NAME, t1.ID, t1.SALARY
FROM PERSON t0, EMPLOYEE t1
WHERE ((t0.ID = ?) AND ((t1.ID = t0.ID) AND (t0.DTYPE = ?))) FOR UPDATE

4.2. PessimisticLockScope.EXTENDED

La portée deEXTENDED couvre les mêmes fonctionnalités queNORMAL. De plus,it’s able to block related entities in a join table.

En termes simples, il fonctionne avec des entités annotées avec@ElementCollection ou@OneToOne,@OneToMany etc. avec@JoinTable.

Examinons l'exemple de code avec l'annotation@ElementCollection:

@Entity
public class Customer {

    @Id
    private Long customerId;
    private String name;
    private String lastName;
    @ElementCollection
    @CollectionTable(name = "customer_address")
    private List
addressList; // getters and setters } @Embeddable public class Address { private String country; private String city; // getters and setters }

Analysons quelques requêtes lors de la recherche de l’entitéCustomer:

SELECT CUSTOMERID, LASTNAME, NAME
FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE

SELECT CITY, COUNTRY, Customer_CUSTOMERID
FROM customer_address
WHERE (Customer_CUSTOMERID = ?) FOR UPDATE

Nous pouvons voir qu'il existe deux requêtes «FOR UPDATE» qui verrouillent une ligne dans la table client ainsi qu'une ligne dans la table de jointure.

Un autre fait intéressant dont nous devons être conscients est que tous les fournisseurs de persistance ne prennent pas en charge les étendues de verrouillage.

5. Réglage du délai de verrouillage

Outre le paramétrage des portées de verrouillage, nous pouvons régler un autre paramètre de verrouillage - timeout. The timeout value is the number of milliseconds that we want to wait for obtaining a lock until the LockTimeoutException occurs.

Nous pouvons changer la valeur de timeout de la même manière que les étendues de verrouillage, en utilisant la propriété ‘javax.persistence.lock.timeout' avec le nombre approprié de millisecondes.

Il est également possible de spécifier le verrouillage «sans attente» en modifiant la valeur du délai d’expiration sur zéro. Cependant, nous devons garder à l'esprit qu'il existe des pilotes de base de données quidon’t support setting a timeout value this way.

Map properties = new HashMap<>();
map.put("javax.persistence.lock.timeout", 1000L);

entityManager.find(
  Student.class, 1L, LockModeType.PESSIMISTIC_READ, properties);

6. Conclusion

Lorsque la définition du niveau d'isolation approprié ne suffit pas pour gérer les transactions simultanées, JPA nous autorise un verrouillage pessimiste. Cela nous permet d'isoler et d'orchestrer différentes transactions afin qu'elles n'accèdent pas à la même ressource en même temps.

Pour ce faire, nous pouvons choisir entre les types de verrous décrits et modifier en conséquence des paramètres tels que leurs portées ou leurs délais.

D'autre part, nous devons nous rappeler que la compréhension des verrous de base de données est aussi importante que la compréhension des mécanismes des systèmes de base de données sous-jacents. Il est également important de garder à l'esprit que le comportement des verrous pessimistes dépend du fournisseur de persistance avec lequel nous travaillons.

Enfin, le code source de ce tutoriel est disponible over sur GitHub pourhibernate et pourEclipseLink.