Перейти к основному содержимому

Пессимистическая блокировка в JPA

· 7 мин. чтения

1. Обзор

Есть много ситуаций, когда мы хотим получить данные из базы данных. Иногда мы хотим заблокировать его для себя для дальнейшей обработки, чтобы никто другой не мог прервать наши действия.

Мы можем думать о двух механизмах управления параллелизмом, которые позволяют нам это сделать: установка надлежащего уровня изоляции транзакций или установка блокировки данных, которые нам нужны в данный момент.

Изоляция транзакций определяется для соединений с базой данных. Мы можем настроить его для сохранения различной степени блокировки данных.

Однако уровень изоляции устанавливается после создания соединения и влияет на каждый оператор в этом соединении. К счастью, мы можем использовать пессимистическую блокировку, которая использует механизмы базы данных для резервирования более детального монопольного доступа к данным.

Мы можем использовать пессимистическую блокировку, чтобы гарантировать, что никакие другие транзакции не смогут изменить или удалить зарезервированные данные.

Есть два типа блокировок, которые мы можем сохранить: эксклюзивная блокировка и общая блокировка. Мы могли читать, но не записывать данные, когда кто-то еще держит общую блокировку. Чтобы изменить или удалить зарезервированные данные, нам нужна эксклюзивная блокировка.

Мы можем получить эксклюзивные блокировки, используя операторы « SELECT … FOR UPDATE ».

2. Режимы блокировки

Спецификация JPA определяет три пессимистичных режима блокировки, которые мы собираемся обсудить:

  • PESSIMISTIC_READ — позволяет нам получить общую блокировку и предотвратить обновление или удаление данных.
  • PESSIMISTIC_WRITE — позволяет получить эксклюзивную блокировку и предотвратить чтение, обновление или удаление данных.
  • PESSIMISTIC_FORCE_INCREMENT — работает как PESSIMISTIC_WRITE и дополнительно увеличивает атрибут версии версионного объекта.

Все они являются статическими членами класса LockModeType и позволяют транзакциям получать блокировку базы данных. Все они сохраняются до тех пор, пока транзакция не будет зафиксирована или откатана.

Стоит отметить, что мы можем получить только один замок за раз. Если это невозможно, генерируется PersistenceException .

2.1. PESSIMISTIC_READ

Всякий раз, когда мы хотим просто читать данные и не сталкиваться с грязными операциями чтения, мы можем использовать PESSIMISTIC_READ (общая блокировка). Однако мы не сможем делать какие-либо обновления или удаления.

Иногда бывает так, что используемая нами база данных не поддерживает блокировку PESSIMISTIC_READ , поэтому вместо нее мы можем получить блокировку PESSIMISTIC_WRITE .

2.2. PESSIMISTIC_WRITE

Любая транзакция, которой необходимо получить блокировку данных и внести в них изменения, должна получить блокировку PESSIMISTIC_WRITE . Согласно спецификации JPA , удержание блокировки PESSIMISTIC_WRITE предотвратит чтение, обновление или удаление данных другими транзакциями.

Обратите внимание, что некоторые системы баз данных реализуют контроль параллелизма с несколькими версиями , который позволяет читателям извлекать данные, которые уже были заблокированы.

2.3. PESSIMISTIC_FORCE_INCREMENT

Эта блокировка работает аналогично PESSIMISTIC_WRITE , но она была введена для взаимодействия с версионными сущностями — сущностями, у которых есть атрибут, аннотированный @Version .

Любым обновлениям версионных сущностей может предшествовать получение блокировки PESSIMISTIC_FORCE_INCREMENT . Получение этой блокировки приводит к обновлению столбца версии.

Поставщик постоянства должен определить, поддерживает ли он PESSIMISTIC_FORCE_INCREMENT для неверсионных сущностей или нет. Если это не так, он выдает PersistanceException .

2.4. Исключения

Полезно знать, какое исключение может возникнуть при работе с пессимистической блокировкой. Спецификация JPA предоставляет различные типы исключений:

  • PessimisticLockException — указывает, что получение блокировки или преобразование общей блокировки в монопольную не удается и приводит к откату на уровне транзакции.
  • LockTimeoutException — указывает, что получение блокировки или преобразование общей блокировки в монопольную блокировку и приводит к откату на уровне оператора.
  • PersistanceException — указывает, что возникла проблема сохранения. PersistanceException и его подтипы, кроме NoResultException , NonUniqueResultException , LockTimeoutException и QueryTimeoutException , помечают активную транзакцию для отката.

3. Использование пессимистичных замков

Существует несколько возможных способов настроить пессимистическую блокировку отдельной записи или группы записей. Давайте посмотрим, как это сделать в JPA.

3.1. Находить

Это, наверное, самый прямой способ. Достаточно передать объект LockModeType в качестве параметра методу find :

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

3.2. Запрос

Кроме того, мы также можем использовать объект Query и вызвать установщик setLockMode с режимом блокировки в качестве параметра:

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

3.3. Явная блокировка

Также можно вручную заблокировать результаты, полученные методом find:

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

3.4. Обновить

Если мы хотим перезаписать состояние объекта методом обновления , мы также можем установить блокировку:

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

3.5. Именованный запрос

Аннотация @NamedQuery также позволяет нам установить режим блокировки:

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

4. Блокировка области

Параметр области блокировки определяет, как поступать с отношениями блокировки заблокированного объекта. Можно получить блокировку только одной сущности, определенной в запросе, или дополнительно заблокировать ее связи.

Для настройки области мы можем использовать перечисление PessimisticLockScope . Он содержит два значения: NORMAL и EXTENDED .

Мы можем установить область, передав параметр ' javax.persistance.lock.scope ' со значением PessimisticLockScope в качестве аргумента в соответствующий метод EntityManager , Query , TypedQuery или NamedQuery :

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

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

4.1. ПессимистическийLockScope.NORMAL

Мы должны знать, что PessimisticLockScope.NORMAL является областью действия по умолчанию. С помощью этой области блокировки мы блокируем сам объект. При использовании с объединенным наследованием он также блокирует предков.

Давайте посмотрим на пример кода с двумя сущностями:

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

Когда мы хотим получить блокировку для Employee , мы можем наблюдать SQL - запрос, который охватывает эти два объекта:

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. ПессимистическийLockScope.EXTENDED

Область РАСШИРЕННАЯ охватывает те же функции, что и ОБЫЧНАЯ . Кроме того, он может блокировать связанные объекты в таблице соединений .

Проще говоря, он работает с сущностями, аннотированными с помощью @ElementCollection или @OneToOne , @OneToMany и т. д. с @JoinTable .

Давайте посмотрим на пример кода с аннотацией @ElementCollection :

@Entity
public class Customer {

@Id
private Long customerId;
private String name;
private String lastName;
@ElementCollection
@CollectionTable(name = "customer_address")
private List<Address> addressList;

// getters and setters
}

@Embeddable
public class Address {

private String country;
private String city;

// getters and setters
}

Проанализируем некоторые запросы при поиске сущности Customer :

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

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

Мы видим, что есть два запроса FOR UPDATE , которые блокируют строку в таблице клиентов, а также строку в таблице соединений.

Еще один интересный факт, о котором мы должны знать, заключается в том, что не все поставщики сохраняемости поддерживают области блокировки.

5. Установка тайм-аута блокировки

Помимо настройки области блокировки, мы можем настроить еще один параметр блокировки — тайм-аут. Значение тайм-аута — это количество миллисекунд, которое мы хотим ждать для получения блокировки, пока не произойдет LockTimeoutException .

Мы можем изменить значение тайм-аута аналогично области блокировки, используя свойство ' javax.persistence.lock.timeout' с правильным числом миллисекунд.

Также можно указать блокировку «без ожидания», изменив значение тайм-аута на ноль. Однако мы должны иметь в виду, что существуют драйверы баз данных, которые не поддерживают установку значения тайм-аута таким образом.

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

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

6. Заключение

Когда установка надлежащего уровня изоляции недостаточна для работы с параллельными транзакциями, JPA дает нам пессимистическую блокировку. Это позволяет нам изолировать и организовывать различные транзакции, чтобы они не обращались к одному и тому же ресурсу в одно и то же время.

Для этого мы можем выбирать между обсуждаемыми типами блокировок и, следовательно, изменять такие параметры, как их области действия или тайм-ауты.

С другой стороны, мы должны помнить, что понимание блокировок базы данных так же важно, как и понимание механизмов базовых систем баз данных. Также важно иметь в виду, что поведение пессимистичных блокировок зависит от поставщика постоянства, с которым мы работаем.

Наконец, исходный код этого руководства доступен на GitHub для спящего режима и для EclipseLink .