Транзакции и блокировки в Hibernate

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

Транзакции в базах данных соответствуют свойствам ACID:

  • Атомарность — транзакция может быть либо целиком выполнена, либо целиком отменена.
  • Согласованность — состояние данных должно быть логически согласованным после выполнения (или отмены) транзации
  • Изолированность — в процессе работы транзакции другие выполняющиеся в это время транзакции не влияют на неё.
  • наДёжность — что-бы не произошло, транзакция останется атомарной.

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

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

  • Добавить заказ.
  • Получить текущее значение счёта клиента.
  • Уменьшить счёт клиента и обновить значение в базе.

Оборачивание этих трёх операций в одну транзакцию гарантирует нам, что либо мы добавим заказ и спишем деньги, либо ничего не произойдёт вообще. Третьего не дано.

Транзакции в Hibernate

Hibernate построен поверх JDBC API и реализует модель транзакций JDBC. Если быть точным, Hibernate способен работать или с JDBC транзакциями или с JTA транзакциями.О JTA — Java Transaction API я напишу как-нибудь позже, а пока сосредоточимся на JDBC транзакциях, тем более что с точки зрения использования их отличий не так и много.

Транзакцию можно начать вызовом beginTransaction() объекта Session, либо запросить у Session связанный с ней объект Transaction и позвать у последнего метод begin(). С объектом Session всегда связан ровно один объект Transaction, доступ к которому может быть получен вызовом getTransaction():

Session session = sessionFactory.openSession();
Transaction t=session.getTransaction();

Методов для подтверждения или отката транзакции у объекта Session нет, необходимо всегда обращаться к объекту Transaction:

session.beginTransaction();
session.getTransaction().commit();


session.beginTransaction();
session.getTransaction().rollback();

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

Операции над транзакциями

У объекта Transaction есть ещё несколько методов, кроме commit() и rollback(), которые позволяют тонко управлять поведением транзакции. Метод isActive() позволяет проверить, есть ли в рамках объекта Transaction управляемую им транзакция. Очевидно, что такая транзакция существует в промежутке времени между вызовами begin() и commit()/rollback().

Метод setRollbackOnly() помечает транзакцию как откаченную в будущем. В отличие от rollback() этот метод не закрывает транзакцию и все последующие запросы к базе будут продолжать выполняться в рамках той же самой транзакции, но завершить эту транзакцию можно будет только откатом и вызовом rollback(). Вызов commit() на такой транзакции выбросит исключение. Проверить состояние транзакции можно вызовом getRollbackOnly().

Блокировки в Hibernate

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

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

Почему это так важно? Классический пример: вы разработали систему покупки билетов. И в жизни этой системы настаёт момент, когда в наличии остаётся последний билет, на который претендуют два покупателя. Если эти два покупателя одновременно начнут покупать билет, то первый покупатель увидит, что есть один билет и купит его, то есть обновит базу данных и запишет, что билетов больше нет. Однако второй покупатель так же увидит, что есть один билет и так же купит его, то есть обновит базу данных и запишет, что билетов больше нет. В результате параллельного выполнения транзакций один и тот же билет продастся два раза, что приведёт к неминуемому скандалу, при попытке его использовать. Поэтому важно, чтобы только одна транзакция могла изменять данные и именно это и обеспечивает механизм блокировок.

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

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

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

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

Как и в JPA, оптимистичное блокирование выполнено на уровне Hibernate, а не базы данных. Для поддержки таких блокировок в класс вводится специально поле версии, которое анализирует Hibernate при сохранении изменений.

@Entity
public class Company extends AbstractIdentifiableObject {
    @Version
    private long version;


    @Getter
    @Setter
    private String name;


    @Getter
    @Setter
    @ManyToMany(mappedBy = "workingPlaces")
    private Collection<Person> workers;
}

Поле, аннотирование @Version может быть целочисленным или временнЫм. Hibernate разрешает доступ к объектам все транзакциям сразу, без каких-либо ограничений, но при сохранении объектов проверяет, нет ли изменений, внесённых другими транзакциями. В случае, если обнаружится конкурирующее изменение, транзакция откатывается.

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

Пессимистичное блокирование выполняется на уровне базы и поэтому не требует вмешательств в код сущности. Блокировка в случае пессимистичного блокирование всегда запрашивается для конкретного объекта во время его загрузки или позднее:

Person p = session.load(Person.class, 3L, LockMode.PESSIMISTIC_READ);


session.lock(p, LockMode.PESSIMISTIC_WRITE);


session.createCriteria(Person.class)
  .setLockMode(LockMode.PESSIMISTIC_READ)
  .uniqueResult();

В примере выше блокировка запрашивается при загрузке объекта методом load(), накладывается другая блокировка на уже загруженный объект методом lock() и, наконец, все объекты, соответствующие критерию будут загружены с блокировкой, указанной в setLockMode().

Если не говорить о тонкостях, в Hibernate поддерживаются две главных пессимистичных блокировки:

  • LockMode.PESSIMISTIC_READ — данные блокируются в момент чтения и это гарантирует, что никто в ходе выполнения транзакции не сможет их изменить. Остальные транзакции, тем не менее, смогут параллельно читать эти данные. Использование этой блокировки может вызывать долгое ожидание блокировки или даже выкидывание OptimisticLockException.
  • LockMode.PESSIMISTIC_WRITE — данные блокируются в момент записи и никто с момента захвата блокировки не может в них писать и не может их читать до окончания транзакции, владеющей блокировкой. Использование этой блокировки может вызывать долгое ожидание блокировки.

LockMode.PESSIMISTIC_READ может поддерживаться не всеми базами данных и в этом случае автоматически будет применён LockMode.PESSIMISTIC_WRITE

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

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