- PVSM.RU - https://www.pvsm.ru -
Итак, начнем! Что же означает аннотация Version в JPA?
Если коротко, то она отвечает за блокировки в JPA. Данная аннотация решает одну из проблем, которые могут возникнуть в результате параллельного выполнения транзакций.
Каждый последующий пункт покрывает все предыдущие, иначе говоря может заменить решения, указанные ранее. Таким образом SERIALIZABLE имеет самый высокий уровень изолированности, а READ UNCOMMITED — самый низкий.
Version решает проблему с потерянными обновлениями. Как именно, сейчас и посмотрим.
Перед тем как перейти к коду, стоит оговорить, что существует два типа блокировок: оптимистичные и пессимистичные. Разница в том, что первые ориентируются на ситуации, в которых множество транзакций пытаются изменить одно поле в одно и тоже время, возникают крайне редко, а другие ориентируются на обратную ситуацию. В соответствии с этим есть отличие в их логике выполнения.
В оптимистичных блокировках при коммите в базу данных производится сравнивание значения поля, помеченного как version, на момент получения данных и на данный момент. Если оно изменилось, то есть какая-то другая транзакция опередила нашу и успела изменить данные, то в таком случае наша транзакция выбрасывает ошибку, и необходимо заново запускать ее.
При использовании оптимистичных блокировок обеспечивается более высокий уровень конкурентности при доступе к базе, но в таком случае приходится повторять транзакции, которые не успели внести изменения раньше других.
В пессимистичных же блокировка накладывается сразу же перед предполагаемой модификацией данных на все строки, которые такая модификация предположительно затрагивает.
А при использовании пессимистичных блокировок гарантируется отсутствие противоречий при выполнении транзакции, за счет помещение остальных в режим ожидания(но на это тратится время), как следствие понижение уровня конкурентности.
Блокировку можно выставить через вызов метода look у EntityManager.
entityManager.lock(myObject, LockModeType.OPTIMISTIC);
LockModeType задает стратегию блокирования.
LockModeType бывает 6 видов(2 из которых относятся к оптимистичным, а 3 к пессимистичным):
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@EntityListeners(OperationListenerForMyEntity.class)
@Entity
public class MyEntity{
@Version
private long version;
@Id
@GeneratedValue
@Getter
@Setter
private Integer id;
@Getter
@Setter
private String value;
@Override
public String toString() {
return "MyEntity{" +
"id=" + id +
", version=" + version +
", value='" + value + ''' +
'}';
}
}
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);
}
}
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();
}
}
Pre-Persistiting operation: MyEntity{id=null, version=0, value='JPA'}
Post-Persist operation: MyEntity{id=531, version=0, value='JPA'}
Все ожидаемо. Меняем id в методе find и идем дальше.
Это оптимистическая блокировка, ну это и так логично. Как я писал выше, происходит сравнения значение поля 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 за тем исключением, что после commit значение поле Version принудительно увеличивается на 1. В итоге окончательно после каждого коммита поле увеличится на 2(увеличение, которое можно увидеть в Post-Update + принудительное увеличение). Вопрос. Зачем? Если после коммита мы хотим еще «поколдовать» над этими же данными, и нам не нужны сторонние транзакции, которые могут ворваться между первым коммитом и закрытием нашей транзакции.
Важно! Если данные попытаться изменить на те же самые, то в таком случае методы Pre-Update и Post-Update не вызовутся. Может произойти обрушение всех транзакций. Например, у нас параллельно считали данные несколько транзакций, но поскольку на вызовы методов pre и post (update) нужно время, то та транзакция, которая пытается изменить данные(на те же), сразу же выполнится. Это приведет к ошибке остальных транзакций.
Так как работа оставшихся видов блокировок выглядит похожим образом, поэтому напишу о всех сразу и рассмотрю результат только по PESSIMISTIC_READ.
LockModeType.PESSIMISTIC_READ — пессимистичная блокировка на чтение.
LockModeType.PESSIMISTIC_WRITE — пессимистичная блокировка на запись (и чтение).
LockModeType.PESSIMISTIC_FORCE_INCREMENT — пессимистичная блокировка на запись (и чтение) с принудительным увеличением поля Version.
В результате выполнения подобных блокировок может произойти долгое ожидание блокировки, что в свою очередь может привести к ошибке.
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.
Автор: Shust_Ivan
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/303837
Ссылки в тексте:
[1] Источник: https://habr.com/post/434836/?utm_source=habrahabr&utm_medium=rss&utm_campaign=434836
Нажмите здесь для печати.