«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 — указывает, что получение блокировки или преобразование общей блокировки в монопольную блокировку и приводит к ошибке на уровне инструкции rollback PersistanceException — указывает, что возникла проблема сохраняемости. PersistanceException и его подтипы, за исключением NoResultException, NonUniqueResultException, LockTimeoutException и QueryTimeoutException, помечают активную транзакцию для отката.

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

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

3.1. Найти

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

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

3.2. Query

Кроме того, мы также можем использовать объект 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 также позволяет нам установить режим блокировки:

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

4. Lock Scope

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

Для настройки области мы можем использовать перечисление 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 . PessimisticLockScope.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 . PessimisticLockScope.EXTENDED

Область действия EXTENDED имеет ту же функциональность, что и NORMAL. Кроме того, он может блокировать связанные объекты в таблице соединений.

Проще говоря, он работает с сущностями, аннотированными с помощью @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.