Оптимистические и пессимистические блокировки на примере Hibernate (JPA)

42b5deb5e5311e2cda4da606681cf665

Привет, Хабр! Недавно пытался изучить тему «оптимистические» и «пессимистические» блокировки, но на мое удивление ни в ру сегменте, ни в англ — хороших статей, которые дают полное представление об двух типах блокировок с применением Hibernate, — нет, поэтому я решил агрегировать всю информацию в одной короткой статье. Так как это моя первая статья, буду рад критике:) Итак, погнали.

P.S. Это статья не является полным гайдом, так как в первую очередь она нацелена на то, чтобы дать понятное описание двух решений одной проблемы, а если нужны примеры использования, то добро пожаловать в Google:)

Зачем нужны блокировки?

Блокировки в первую очередь нужны для того, чтобы поддерживать консистентность данных в таблице БД. Есть два типа блокировок: оптимистическая и пессимистическая. Из названия можно догадаться, что они предлагают две разных стратегии для поддержания целостности данных. Оптимистическая блокировка нацелена на неблокирующий сценарий, а пессимистическая наоборот на блокирующий.

Они обе решают проблему «потерянного изменения» (так называемый, lost update). Приведу пример. Предположим, что вы мамочка‑шопоголик на wildberries. Вы сделали два заказа, счет был выставлен, но разбит на разные транзакции для разных заказов. Транзакции начали выполняться одновременно. Обе считали данные, обе начали выполнять бизнес‑логику и обе завершились. Вроде бы все гуд? Где подвох? Но проблема в том, что если транзакция по первому заказу выполнилась быстрее, чем транзакция по второму заказу. То изменения первой транзакции будут перетерты изменениями второй — как будто первого заказа вообще и не было.

С проблематикой ознакомились, поэтому можно пойти дальше.

Оптимистическая блокировка

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

В Hibernate оптимистическая блокировка реализована на базе версионирования с использованием аннотации @Version для поля класса (обычно, оно имеет тип int) . Как это работает? Объект подгружается из базы данных, у него есть номер версии (если запись только создана — у нее номер версии 0). И когда изменения выгружаются, например, при вызове метода entityManager.flush() или при завершении транзакции (потому что здесь тоже вызывается метод .flush()), тогда текущий номер версии сравнивается с актуальной версией в базе данных. Если актуальная версия больше, чем текущая, то выбрасывается исключение OptimisticLockException, которое сигнализирует пользователю о том, что какой-то пользователь уже поменял объект и мы работаем с неактуальным состоянием. Если версия больше или равна той, которая в база данных, то запись фиксируется и версия увеличивается на 1. Избежать исключения можно с помощью метода entityManager.merge(...).

Проще говоря, оптимистическая блокировка работает по принципу CAS (Compare And Swap): сравнил и, если версия больше, заменил.

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

Hibernate предоставляет два типа этой блокировки:

  1. LockModeType.OPTIMISTIC — блокировка по умолчанию для всех сущностей, помеченных аннотацией @Version.

  2. LockModeType.OPTIMISTIC_FORCE_INCREMENT — блокировка полностью аналогичная прошлой, но проверяет версии дочерних элементов (например, связных с помощью отношений один ко многим).

Блокировки можно задавать с помощью следующих методов: entityManager.lock(...), entityManager.find(..., LockModeType), query.setLockMode(...).

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

Это такой вид блокировки, который лочит запись до тех пор, пока вы не завершится транзакция. Пока запись удерживается, остальные пользователи ожидают, когда можно будет захватить запись. Если ожидание слишком долгое, то будет выброшено исключение LockTimeoutException. Время ожидания можно изменять вручную при создании блокировки с помощью добавления Map, в котором есть ключ jakarta.persistence.lock.timeout. Это исключение помечает транзакцию, как rollback.

Аналог synchronized в Java, но только здесь используются средства баз данных.

Hibernate предоставляет три типа блокировки:

  1. LockModeType.PESSIMISTIC_READ — блокирует любое изменение записи до тех пор пока транзакция не завершит свою работу, но при этом разрешает другим сущностям её читать. Позволяет не допустить «грязное чтение». Это называется общей блокировкой, так как блокируется запись одной транзакцией, но доступ для чтения разрешен для всех.

  2. LockModeType.PESSIMISITIC_WRITE — блокирует любое чтение и изменение записи до тех пор пока транзакция не завершит свою работу. Это называется эксклюзивная блокировка, так как предоставляет запись только для одной транзакции.

  3. LockModeType.PESSIMISTIC_FORCE_INREMENT — совмещение пессимистической write блокировки и оптимистической. Позволяет предотвратить ситуации, когда сначала первая транзакция с помощью оптимистической блокировки считала сущность, затем мы пессимистично захватили эту же запись во второй транзакции, завершились быстрее чем первая транзакция, то первая транзакция перетрет наши данные. Этот вид блокировки не даст этому случиться.

Здесь важно избегать deadlock-ов, которые могут заблокировать выполнение транзакций и они зависнут на какое-то время, а затем будет выброшено исключение. Часто СУБД сами видят deadlock-и и сразу выкидывают исключение.

Сценарий возникновения deadlock:

  1. Транзакция T1 захватила объект A

  2. Транзакция T2 захватила объект B

  3. Транзакция T1 пытается захватить объект B, а транзакция T2 пытается захватить объект A

  4. Welcome to deadlock:)

Также в рамках пессимистической блокировки есть понятие PessimisticLockScope. Поэтому чисто символически я её упомяну. У нее есть два значения:

  1. PessimisticLockScope.NORMAL — блокирует исключительно саму запись (если есть наследование, то блокируются все субтаблицы).

  2. PessimisticLockScope.SHARED — блокирует саму запись и все её связи.

Выше я упомянул про «общую» и «эксклюзивную» блокировки. Их суть я описал кратко выше, но стоит упомянуть, что SQL‑statement для общей блокировки будет select * from Table for shared, а для эксклюзивной select * from Table for update.

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

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

© Habrahabr.ru