Транзакция это набор операций, которые могут быть либо целиком и успешно выполнены, либо полностью не выполнены.
Транзакции в базах данных соответствуют свойствам ACID:
- Атомарность — транзакция может быть либо целиком выполнена, либо целиком отменена.
- Согласованность — состояние данных должно быть логически согласованным после выполнения (или отмены) транзации
- Изолированность — в процессе работы транзакции другие выполняющиеся в это время транзакции не влияют на неё.
- наДёжность — что-бы не произошло, транзакция останется атомарной.
Если вдруг транзакции в вашей базе не соответствуют этим принципам, то это плохие, негодные транзакции и они не заслуживают чести носить это имя.
Использование транзакций в дикой природе проще показать на примере: положим у нас есть система, в которой клиенты размещают какие-то заказы. За каждый заказ со счёта клиента снимается какая-то сумма. Таким образом, одна логическая единица работы разбивается на три операции в базе данных:
- Добавить заказ.
- Получить текущее значение счёта клиента.
- Уменьшить счёт клиента и обновить значение в базе.
Оборачивание этих трёх операций в одну транзакцию гарантирует нам, что либо мы добавим заказ и спишем деньги, либо ничего не произойдёт вообще. Третьего не дано.
Транзакции в Hibernate
Hibernate построен поверх JDBC API и реализует модель транзакций JDBC. Если быть точным, Hibernate способен работать или с JDBC транзакциями или с JTA транзакциями.О JTA — Java Transaction API я напишу как-нибудь позже, а пока сосредоточимся на JDBC транзакциях, тем более что с точки зрения использования их отличий не так и много.
Транзакцию можно начать вызовом beginTransaction() объекта Session, либо запросить у Session связанный с ней объект Transaction и позвать у последнего метод begin(). С объектом Session всегда связан ровно один объект Transaction, доступ к которому может быть получен вызовом getTransaction():
1
2
|
Session session = sessionFactory.openSession();
Transaction t=session.getTransaction();
|
Методов для подтверждения или отката транзакции у объекта Session нет, необходимо всегда обращаться к объекту Transaction:
1
2
3
4
5
|
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 при сохранении изменений.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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 разрешает доступ к объектам все транзакциям сразу, без каких-либо ограничений, но при сохранении объектов проверяет, нет ли изменений, внесённых другими транзакциями. В случае, если обнаружится конкурирующее изменение, транзакция откатывается.
Пессимистичное блокирование
Пессимистичное блокирование выполняется на уровне базы и поэтому не требует вмешательств в код сущности. Блокировка в случае пессимистичного блокирование всегда запрашивается для конкретного объекта во время его загрузки или позднее:
1
2
3
4
5
6
7
|
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 — данные блокируются в момент записи и никто с момента захвата блокировки не может в них писать и не может их читать до окончания транзакции, владеющей блокировкой. Использование этой блокировки может вызывать долгое ожидание блокировки.