Pessimistisches Sperren in JPA

1. Übersicht

Es gibt viele Situationen, in denen wir Daten aus einer Datenbank abrufen möchten. Manchmal möchten wir es für die weitere Verarbeitung für uns selbst sperren, damit niemand anderes unsere Aktionen unterbrechen kann.

Wir können uns zwei Mechanismen zur Kontrolle der Parallelität vorstellen, die uns dies ermöglichen: Festlegen der richtigen Transaktionsisolationsstufe oder Festlegen einer Sperre für Daten, die wir derzeit benötigen.

Die Transaktionsisolation ist für Datenbankverbindungen definiert. Wir können es so konfigurieren, dass der unterschiedliche Grad der Sperrdaten beibehalten wird.

Die Isolationsstufe wird jedoch festgelegt, sobald die Verbindung hergestellt wurde, und wirkt sich auf jede Anweisung innerhalb dieser Verbindung aus. Glücklicherweise können wir pessimistisches Sperren verwenden, das Datenbankmechanismen verwendet, um einen detaillierteren exklusiven Zugriff auf die Daten zu reservieren.

Wir können eine pessimistische Sperre verwenden, um sicherzustellen, dass keine anderen Transaktionen reservierte Daten ändern oder löschen können.

Es gibt zwei Arten von Sperren, die wir beibehalten können: eine exklusive Sperre und eine gemeinsam genutzte Sperre. Wir konnten Daten lesen, aber nicht schreiben, wenn jemand anderes eine gemeinsame Sperre hat. Um die reservierten Daten zu ändern oder zu löschen, benötigen wir eine exklusive Sperre.

Wir können exklusive Sperren mit den Anweisungen ' SELECT… FOR UPDATE ' erwerben .

2. Sperrmodi

Die JPA-Spezifikation definiert drei pessimistische Sperrmodi, die wir diskutieren werden:

  • PESSIMISTIC_READ - Ermöglicht es uns, eine gemeinsame Sperre zu erhalten und zu verhindern, dass die Daten aktualisiert oder gelöscht werden
  • PESSIMISTIC_WRITE - Ermöglicht es uns, eine exklusive Sperre zu erhalten und zu verhindern, dass die Daten gelesen, aktualisiert oder gelöscht werden
  • PESSIMISTIC_FORCE_INCREMENT - funktioniert wie PESSIMISTIC_WRITE und erhöht zusätzlich ein Versionsattribut einer versionierten Entität

Alle von ihnen sind statische Mitglieder der LockModeType- Klasse und ermöglichen Transaktionen, eine Datenbanksperre zu erhalten. Sie alle bleiben erhalten, bis die Transaktion festgeschrieben oder zurückgesetzt wird.

Es ist erwähnenswert, dass wir jeweils nur eine Sperre erhalten können. Wenn es unmöglich ist, wird eine PersistenceException ausgelöst.

2.1. PESSIMISTIC_READ

Wann immer wir nur Daten lesen und keine schmutzigen Lesevorgänge feststellen möchten , können wir PESSIMISTIC_READ (Shared Lock) verwenden. Wir können jedoch keine Aktualisierungen oder Löschungen vornehmen.

Es kommt manchmal vor, dass die von uns verwendete Datenbank die Sperre PESSIMISTIC_READ nicht unterstützt. Daher ist es möglich, dass wir stattdessen die Sperre PESSIMISTIC_WRITE erhalten .

2.2. PESSIMISTIC_WRITE

Jede Transaktion, die eine Sperre für Daten erwerben und Änderungen daran vornehmen muss, sollte die Sperre PESSIMISTIC_WRITE erhalten . Gemäß der JPA- Spezifikation verhindert das Halten der Sperre PESSIMISTIC_WRITE , dass andere Transaktionen die Daten lesen, aktualisieren oder löschen.

Bitte beachten Sie, dass einige Datenbanksysteme eine Parallelitätskontrolle für mehrere Versionen implementieren, mit der Leser bereits blockierte Daten abrufen können.

2.3. PESSIMISTIC_FORCE_INCREMENT

Diese Sperre funktioniert ähnlich wie PESSIMISTIC_WRITE , wurde jedoch eingeführt, um mit versionierten Entitäten zusammenzuarbeiten - Entitäten, deren Attribut mit @Version versehen ist .

Vor Aktualisierungen versionierter Entitäten kann die Sperre PESSIMISTIC_FORCE_INCREMENT abgerufen werden . Wenn Sie diese Sperre aktivieren, wird die Versionsspalte aktualisiert.

Es liegt an einem Persistenzanbieter, zu bestimmen, ob er PESSIMISTIC_FORCE_INCREMENT für nicht versionierte Entitäten unterstützt oder nicht. Wenn dies nicht der Fall ist, wird die PersistanceException ausgelöst .

2.4. Ausnahmen

Es ist gut zu wissen, welche Ausnahme bei der Arbeit mit pessimistischem Sperren auftreten kann. Die JPA- Spezifikation bietet verschiedene Arten von Ausnahmen:

  • PessimisticLockException - Gibt an, dass das Abrufen einer Sperre oder das Konvertieren einer freigegebenen in eine exklusive Sperre fehlschlägt und zu einem Rollback auf Transaktionsebene führt
  • LockTimeoutException - Gibt an, dass das Abrufen einer Sperre oder das Konvertieren einer gemeinsam genutzten Sperre in ein exklusives Zeitlimit zu einem Rollback auf Anweisungsebene führt
  • PersistanceException - Zeigt an, dass ein Persistenzproblem aufgetreten ist. PersistanceException und ihre Untertypen mit Ausnahme von NoResultException , NonUniqueResultException, LockTimeoutException und QueryTimeoutException markieren die aktive Transaktion, die zurückgesetzt werden soll.

3. Verwenden pessimistischer Sperren

Es gibt verschiedene Möglichkeiten, eine pessimistische Sperre für einen einzelnen Datensatz oder eine Gruppe von Datensätzen zu konfigurieren. Mal sehen, wie es in JPA geht.

3.1. Finden

Es ist wahrscheinlich der einfachste Weg. Es reicht aus, ein LockModeType- Objekt als Parameter an die find- Methode zu übergeben:

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

3.2. Abfrage

Zusätzlich können wir auch ein Query- Objekt verwenden und den setLockMode- Setter mit einem Sperrmodus als Parameter aufrufen :

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

3.3. Explizites Sperren

Es ist auch möglich, die mit der Suchmethode abgerufenen Ergebnisse manuell zu sperren:

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

3.4. Aktualisierung

Wenn wir den Status der Entität mit der Aktualisierungsmethode überschreiben möchten , können wir auch eine Sperre setzen:

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

3.5. NamedQuery

Mit der Annotation @NamedQuery können wir auch einen Sperrmodus festlegen :

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

4. Bereich sperren

Der Parameter für den Sperrbereich definiert, wie mit Sperrbeziehungen der gesperrten Entität umgegangen wird. Es ist möglich, eine Sperre nur für eine einzelne Entität zu erhalten, die in einer Abfrage definiert ist, oder deren Beziehungen zusätzlich zu blockieren.

To configure the scope we can use PessimisticLockScope enum. It contains two values: NORMAL and EXTENDED.

We can set the scope by passing a parameter ‘javax.persistance.lock.scope‘ with PessimisticLockScope value as an argument to the proper method of EntityManager, Query, TypedQuery or NamedQuery:

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. With this locking scope, we lock the entity itself. When used with joined inheritance it also locks the ancestors.

Let's look at the sample code with two entities:

@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 }

When we want to obtain a lock on the Employee, we can observe the SQL query which spans over those two entities:

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

The EXTENDED scope covers the same functionality as NORMAL. In addition, it's able to block related entities in a join table.

Simply put, it works with entities annotated with @ElementCollection or @OneToOne, @OneToMany etc. with @JoinTable.

Let's look at the sample code with the @ElementCollection annotation:

@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 }

Let's analyze some queries when searching for the Customer entity:

SELECT CUSTOMERID, LASTNAME, NAME FROM CUSTOMER WHERE (CUSTOMERID = ?) FOR UPDATE SELECT CITY, COUNTRY, Customer_CUSTOMERID FROM customer_address WHERE (Customer_CUSTOMERID = ?) FOR UPDATE

We can see that there are two ‘FOR UPDATE‘ queries which lock a row in the customer table as well as a row in the join table.

Another interesting fact we should be aware of is that not all persistence providers support lock scopes.

5. Setting Lock Timeout

Besides setting lock scopes, we can adjust another lock parameter – timeout. The timeout value is the number of milliseconds that we want to wait for obtaining a lock until the LockTimeoutException occurs.

We can change the value of timeout similarly to lock scopes, by using property ‘javax.persistence.lock.timeout' with the proper number of milliseconds.

It's also possible to specify ‘no wait' locking by changing timeout value to zero. However, we should keep in mind that there are database drivers which don'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

When setting the proper isolation level is not enough to cope with concurrent transactions, JPA gives us pessimistic locking. It enables us to isolate and orchestrate different transactions so they don't access the same resource at the same time.

To achieve that we can choose between discussed types of locks and consequently modify such parameters as their scopes or timeouts.

Andererseits sollten wir uns daran erinnern, dass das Verständnis von Datenbanksperren ebenso wichtig ist wie das Verständnis der Mechanismen zugrunde liegender Datenbanksysteme. Es ist auch wichtig zu bedenken, dass das Verhalten von pessimistischen Sperren von dem Persistenzanbieter abhängt, mit dem wir arbeiten.

Schließlich ist der Quellcode dieses Tutorials auf GitHub für den Ruhezustand und für EclipseLink verfügbar.