Хабрахабр

[Из песочницы] Замечательная аннотация Version в JPA

Введение

Итак, начнем! Что же означает аннотация Version в JPA?

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

Какие же могут возникнуть проблемы?

  1. Потерянные обновления могут возникнуть в ситуациях, когда две транзакции, выполняющиеся параллельно, пытаются обновить одни и те же данные.
  2. Грязные чтения возникают, когда транзакция видит еще не сохранённые изменения, сделанные другой транзакцией. В таких случая может возникнуть проблема из-за отката второй транзакции, но при этом данные уже были прочитаны первой.
  3. Неповторяемые чтения возникают, когда первая транзакция получила данные, а вторая транзакция внесла в них изменение и успешно закоммитила их, до окончания первой транзакции. Иначе говоря, когда в рамках одной транзакции один и тот же запрос на получение, например всей таблицы, возвращает разные результаты.
  4. Фантомное чтение — проблема похожая на неповторяемые чтения, за тем исключением, что возвращается разное количество строк.

Коротко о их решениях

  1. READ UNCOMMITED — решается с помощью аннотации Version в JPA(об этом как раз и статья)
  2. READ COMMITED — позволяет читать только закоммиченные изменения
  3. REPEATABLE READ — тут немного посложнее. Наша транзакция «не видит» изменения данных, которые были ею ранее прочитаны, а другие транзакции не могут изменять тех данных, что попали в нашу транзакцию.
  4. SERIALIZABLE — последовательное выполнение транзакций

Каждый последующий пункт покрывает все предыдущие, иначе говоря может заменить решения, указанные ранее. Таким образом SERIALIZABLE имеет самый высокий уровень изолированности, а READ UNCOMMITED — самый низкий.

Version

Version решает проблему с потерянными обновлениями. Как именно, сейчас и посмотрим.

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

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

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

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

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

LockModeType или как выставить блокировку

Блокировку можно выставить через вызов метода look у EntityManager.

entityManager.lock(myObject, LockModeType.OPTIMISTIC);

LockModeType задает стратегию блокирования.

LockModeType бывает 6 видов(2 из которых относятся к оптимистичным, а 3 к пессимистичным):

  1. NONE — отсутствие блокировки
  2. OPTIMISTIC
  3. OPTIMISTIC_FORCE_INCREMENT
  4. PESSIMISTIC_READ
  5. PESSIMISTIC_WRITE
  6. PESSIMISTIC_FORCE_INCREMENT

Создадим нашу Entity

import lombok.Getter;
import lombok.Setter;
import javax.persistence.*; @EntityListeners(OperationListenerForMyEntity.class)
@Entity
public class MyEntity'; }
}

Создадим класс, где будут реализованы все Callback методы

import javax.persistence.*; public class OperationListenerForMyEntity { @PostLoad public void postLoad(MyEntity obj) { System.out.println("Loaded operation: " + obj); } @PrePersist public void prePersist(MyEntity obj) { System.out.println("Pre-Persistiting operation: " + obj); } @PostPersist public void postPersist(MyEntity obj) { System.out.println("Post-Persist operation: " + obj); } @PreRemove public void preRemove(MyEntity obj) { System.out.println("Pre-Removing operation: " + obj); } @PostRemove public void postRemove(MyEntity obj) { System.out.println("Post-Remove operation: " + obj); } @PreUpdate public void preUpdate(MyEntity obj) { System.out.println("Pre-Updating operation: " + obj); } @PostUpdate public void postUpdate(MyEntity obj) { System.out.println("Post-Update operation: " + obj); }
}

Main.java

import javax.persistence.*;
import java.util.concurrent.*; // В этом классе создаем несколько потоков и смотрим, что будет происходить.
public class Main {
// Создаем фабрику, т.к. создание EntityManagerFactory дело дорогое, обычно делается это один раз. private static EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("ru.easyjava.data.jpa.hibernate"); public static void main(String[] args) {
// Создаем 10 потоков(можно и больше, но в таком случае будет сложно разобраться). ExecutorService es = Executors.newFixedThreadPool(10); try {
// Метод persistFill() нужен для авто-заполнения таблицы. persistFill(); for(int i=0; i<10; i++){ int finalI = i; es.execute(() -> {
// Лучше сначала запустить без метода updateEntity(finalI) так, чтоб java создала сущность в базе и заполнила ее. Но так как java - очень умная, она сама запоминает последний сгенерированный id, даже если вы решили полностью очистить таблицу, id новой строки будет таким, как будто вы не чистили базу данных(может возникнуть ситуация, в которой вы запускаете метод persistFill(), а id в бд у вас начинаются с 500). updateEntity(finalI); }); } es.shutdown(); try { es.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } finally { entityManagerFactory.close(); } } // Метод для получения объекта из базы и изменения его. private static void updateEntity(int index) {
// Создаем EntityManager для того, чтобы можно было вызывать методы, управления жизненным циклом сущности. EntityManager em = entityManagerFactory.createEntityManager(); MyEntity myEntity = null; try { em.getTransaction().begin();
// Получаем объект из базы данных по индексу 1. myEntity = em.find(MyEntity.class, 1);
// Вызываем этот sout, чтобы определить каким по очереди был "вытянут" объект. System.out.println("load = "+index);
// Эту строчку мы и будем изменять (а именно LockModeType.*). em.lock(myEntity, LockModeType.OPTIMISTIC);
// Изменяем поле Value, таким образом, чтобы понимать транзакция из какого потока изменила его. myEntity.setValue("WoW_" + index); em.getTransaction().commit(); em.close(); System.out.println("--Greeter updated : " + myEntity +" __--__ "+ index); }catch(RollbackException ex){ System.out.println("ГРУСТЬ, ПЕЧАЛЬ=" + myEntity); } } public static void persistFill() { MyEntity myEntity = new MyEntity(); myEntity.setValue("JPA"); EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.persist(myEntity); em.getTransaction().commit(); em.close(); }
}

Первый запуск с закомментированным методом updateEntity

Pre-Persistiting operation: MyEntity{id=null, version=0, value='JPA'}
Post-Persist operation: MyEntity{id=531, version=0, value='JPA'}
Все ожидаемо. Меняем id в методе find и идем дальше.

LockModeType.OPTIMISTIC

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

Результаты:

Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 3
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 2
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_2'}
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_3'}
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 9
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_9'}
Loaded operation: MyEntity{id=531, version=0, value='JPA'}
load = 1
Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_1'}
Post-Update operation: MyEntity{id=531, version=1, value='WoW_1'}
--Greeter updated : MyEntity{id=531, version=1, value='WoW_1'} __--__ 1
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=0, value='WoW_2'}
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=0, value='WoW_3'}
Loaded operation: MyEntity{id=531, version=1, value='WoW_1'}
load = 4
Pre-Updating operation: MyEntity{id=531, version=1, value='WoW_4'}
Post-Update operation: MyEntity{id=531, version=2, value='WoW_4'}
--Greeter updated : MyEntity{id=531, version=2, value='WoW_4'} __--__ 4
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=0, value='WoW_9'}
Loaded operation: MyEntity{id=531, version=2, value='WoW_4'}
load = 0
Pre-Updating operation: MyEntity{id=531, version=2, value='WoW_0'}
Post-Update operation: MyEntity{id=531, version=3, value='WoW_0'}
--Greeter updated : MyEntity{id=531, version=3, value='WoW_0'} __--__ 0
Loaded operation: MyEntity{id=531, version=3, value='WoW_0'}
load = 6
Pre-Updating operation: MyEntity{id=531, version=3, value='WoW_6'}
Post-Update operation: MyEntity{id=531, version=4, value='WoW_6'}
Loaded operation: MyEntity{id=531, version=4, value='WoW_6'}
load = 5
Pre-Updating operation: MyEntity{id=531, version=4, value='WoW_5'}
Post-Update operation: MyEntity{id=531, version=5, value='WoW_5'}
--Greeter updated : MyEntity{id=531, version=4, value='WoW_6'} __--__ 6
--Greeter updated : MyEntity{id=531, version=5, value='WoW_5'} __--__ 5
Loaded operation: MyEntity{id=531, version=5, value='WoW_5'}
load = 7
Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_7'}
Post-Update operation: MyEntity{id=531, version=6, value='WoW_7'}
Loaded operation: MyEntity{id=531, version=5, value='WoW_5'}
load = 8
Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_8'}
--Greeter updated : MyEntity{id=531, version=6, value='WoW_7'} __--__ 7
ГРУСТЬ, ПЕЧАЛЬ=MyEntity{id=531, version=5, value='WoW_8'}

Наблюдения: Как видно из результатов первыми начали загружаться потоки 3, 2, 9 и 1, для них были вызваны методы Pre-Update callback. Первый поток, где вызвался метод Post-Update был 1, как видно из результатов там уже было изменено(увеличилось на 1) поле помеченное аннотацией Version. Соответственно все оставшиеся потоки 2, 3, 9 выбросили исключение. И так далее. Результат выполнения value = WoW_7, version = 6. Действительно, последний Post-Update был у потока 7 с версией = 6.

LockModeType.OPTIMISTIC_FORCE_INCREMENT

Работает по тому же алгоритму, что и LockModeType.OPTIMISTIC за тем исключением, что после commit значение поле Version принудительно увеличивается на 1. В итоге окончательно после каждого коммита поле увеличится на 2(увеличение, которое можно увидеть в Post-Update + принудительное увеличение). Вопрос. Зачем? Если после коммита мы хотим еще «поколдовать» над этими же данными, и нам не нужны сторонние транзакции, которые могут ворваться между первым коммитом и закрытием нашей транзакции.

Может произойти обрушение всех транзакций. Важно! Если данные попытаться изменить на те же самые, то в таком случае методы Pre-Update и Post-Update не вызовутся. Это приведет к ошибке остальных транзакций. Например, у нас параллельно считали данные несколько транзакций, но поскольку на вызовы методов pre и post (update) нужно время, то та транзакция, которая пытается изменить данные(на те же), сразу же выполнится.

LockModeType.PESSIMISTIC_READ, LockModeType.PESSIMISTIC_WRITE и LockModeType.PESSIMISTIC_FORCE_INCREMENT

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

PESSIMISTIC_READ — пессимистичная блокировка на чтение.
LockModeType. LockModeType. PESSIMISTIC_FORCE_INCREMENT — пессимистичная блокировка на запись (и чтение) с принудительным увеличением поля Version. PESSIMISTIC_WRITE — пессимистичная блокировка на запись (и чтение).
LockModeType.

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

Результат по LockModeType.PESSIMISTIC_READ(представлен не целиком):

load = 0
Pre-Updating operation: MyEntity{id=549, version=5, value='WoW_0'}
Post-Update operation: MyEntity{id=549, version=6, value='WoW_0'}
Loaded operation: MyEntity{id=549, version=6, value='WoW_0'}
load = 8
Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_8'}
Loaded operation: MyEntity{id=549, version=6, value='WoW_0'}
load = 4
Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_4'}
...
ERROR: ОШИБКА: обнаружена взаимоблокировка Подробности: Процесс 22760 ожидает в режиме ExclusiveLock блокировку "кортеж (0,66) отношения 287733 базы данных 271341"; заблокирован процессом 20876.
Процесс 20876 ожидает в режиме ShareLock блокировку "транзакция 8812"; заблокирован процессом 22760.

Как результат, потоки 4 и 8 заблокировали друг друга, что привело к не разрешимому конфликту. До этого потоку 0 никто не мешал выполняться. Аналогичная ситуация со всеми потоками до 0.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть