Pull to refresh

Замечательная аннотация Version в JPA

Reading time 8 min
Views 32K

Введение


Итак, начнем! Что же означает аннотация 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 или как выставить блокировку


Блокировку можно выставить через вызов метода lock у 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{
    @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 + '\'' +
                '}';
    }
}


Создадим класс, где будут реализованы все 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.

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

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

Результат по 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.
Tags:
Hubs:
+8
Comments 4
Comments Comments 4

Articles