Введение
Итак, начнем! Что же означает аннотация Version в JPA?
Если коротко, то она отвечает за блокировки в JPA. Данная аннотация решает одну из проблем, которые могут возникнуть в результате параллельного выполнения транзакций.
Какие же могут возникнуть проблемы?
- Потерянные обновления могут возникнуть в ситуациях, когда две транзакции, выполняющиеся параллельно, пытаются обновить одни и те же данные.
- Грязные чтения возникают, когда транзакция видит еще не сохранённые изменения, сделанные другой транзакцией. В таких случая может возникнуть проблема из-за отката второй транзакции, но при этом данные уже были прочитаны первой.
- Неповторяемые чтения возникают, когда первая транзакция получила данные, а вторая транзакция внесла в них изменение и успешно закоммитила их, до окончания первой транзакции. Иначе говоря, когда в рамках одной транзакции один и тот же запрос на получение, например всей таблицы, возвращает разные результаты.
- Фантомное чтение — проблема похожая на неповторяемые чтения, за тем исключением, что возвращается разное количество строк.
Коротко о их решениях
- READ UNCOMMITED — решается с помощью аннотации Version в JPA(об этом как раз и статья)
- READ COMMITED — позволяет читать только закоммиченные изменения
- REPEATABLE READ — тут немного посложнее. Наша транзакция «не видит» изменения данных, которые были ею ранее прочитаны, а другие транзакции не могут изменять тех данных, что попали в нашу транзакцию.
- SERIALIZABLE — последовательное выполнение транзакций
Каждый последующий пункт покрывает все предыдущие, иначе говоря может заменить решения, указанные ранее. Таким образом SERIALIZABLE имеет самый высокий уровень изолированности, а READ UNCOMMITED — самый низкий.
Version
Version решает проблему с потерянными обновлениями. Как именно, сейчас и посмотрим.
Перед тем как перейти к коду, стоит оговорить, что существует два типа блокировок: оптимистичные и пессимистичные. Разница в том, что первые ориентируются на ситуации, в которых множество транзакций пытаются изменить одно поле в одно и тоже время, возникают крайне редко, а другие ориентируются на обратную ситуацию. В соответствии с этим есть отличие в их логике выполнения.
В оптимистичных блокировках при коммите в базу данных производится сравнивание значения поля, помеченного как version, на момент получения данных и на данный момент. Если оно изменилось, то есть какая-то другая транзакция опередила нашу и успела изменить данные, то в таком случае наша транзакция выбрасывает ошибку, и необходимо заново запускать ее.
При использовании оптимистичных блокировок обеспечивается более высокий уровень конкурентности при доступе к базе, но в таком случае приходится повторять транзакции, которые не успели внести изменения раньше других.
В пессимистичных же блокировка накладывается сразу же перед предполагаемой модификацией данных на все строки, которые такая модификация предположительно затрагивает.
А при использовании пессимистичных блокировок гарантируется отсутствие противоречий при выполнении транзакции, за счет помещение остальных в режим ожидания(но на это тратится время), как следствие понижение уровня конкурентности.
LockModeType или как выставить блокировку
Блокировку можно выставить через вызов метода look у EntityManager.
entityManager.lock(myObject, LockModeType.OPTIMISTIC);
LockModeType задает стратегию блокирования.
LockModeType бывает 6 видов(2 из которых относятся к оптимистичным, а 3 к пессимистичным):
- NONE — отсутствие блокировки
- OPTIMISTIC
- OPTIMISTIC_FORCE_INCREMENT
- PESSIMISTIC_READ
- PESSIMISTIC_WRITE
- PESSIMISTIC_FORCE_INCREMENT
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 и идем дальше.
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.
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