Блокировки, это механизм, позволяющий параллельную работу с одними и теми же данными в базе данных. Когда более чем одна транзакция пытается получить доступ к одним и тем же данным в одно и то же время, в дело вступают блокировки, которые гарантируют, что только одна из этих транзакций изменит данные.
Почему это так важно? Классический пример: вы разработали систему покупки билетов. И в жизни этой системы настаёт момент, когда в наличии остаётся последний билет, на который претендуют два покупателя. Если эти два покупателя одновременно начнут покупать билет, то первый покупатель увидит, что есть один билет и купит его, то есть обновит базу данных и запишет, что билетов больше нет. Однако второй покупатель так же увидит, что есть один билет и так же купит его, то есть обновит базу данных и запишет, что билетов больше нет. В результате параллельного выполнения транзакций один и тот же билет продастся два раза, что приведёт к неминуемому скандалу, при попытке его использовать. Поэтому важно, чтобы только одна транзакция могла изменять данные и именно это и обеспечивает механизм блокировок.
Существует два основных подхода к блокированию транзакций: оптимистичный и пессимистичный. Оптимистичный подход предполагает, что параллельно выполняющиеся транзакции редко обращаются к одним и тем же данным и позволяет им спокойно и свободно выполнять любые чтения и обновления данных. Но, при окончании транзакции, то есть записи данных в базу, производится проверка, изменились ли данные в ходе выполнения данной транзакции и если да, транзакция обрывается и выбрасывается исключение.
Пессимистичный подход напротив, ориентирован на транзакции, которые постоянно или достаточно часто конкурируют за одни и те же данные и поэтому блокирует доступ к данным превентивно, в тот момент когда читает их. Другие транзакции останавливаются, когда пытаются обратиться к заблокированным данным и ждут снятия блокировки (или кидают исключение).
Разница в том, что в первом случае обеспечивается более высокий уровень конкурентности при доступе к базе, который оплачивается необходимостью переповтора транзакций, в случае коллизии. Во втором случае транзакции гарантируется, что только она будет иметь полный доступ к данным, но за счёт понижения уровня конкурентности и затрат времени на ожидание блокировки.
Оптимистичное блокирование
Оптимистичное блокирование в JPA реализовано с внедрением специального поля версии в сущность:
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 может быть целочисленным или временнЫм. При завершении транзакции, если сущность была оптимистично заблокирована, будет проверено, не изменилось ли значение @Version кем-либо ещё, после того как данные были прочитаны и, если изменилось, будет выкинуто OptimisticLockException. Использование этого поля позволяет отказаться от блокировок на уровне базы данных и сделать всё на уровне JPA, улучшая уровень конкурентности.
JPA поддерживает два типа оптимистичной блокировки:
- LockModeType.OPTIMISTIC — блоикировка на чтение, которая работает, как описано выше: если при завершении транзакции кто-то извне изменит поле @Version, то транзакция автоматически будет откачена и будет выброшено OptimisticLockException.
- LockModeType.OPTIMISTIC_FORCE_INCREMENT — блокировка на запись. Ведёт себя как и блокировка на чтение, но при этом увеличивает значение поля @Version.
Обе блокировки ставятся путём вызова метода lock() у EntityManager, в который передаётся сущность, требующая блокировки и уровень блокировки:
1
2
3
|
EntityManager em = entityManagerFactory.createEntityManager();
em.lock(company1, LockModeType.OPTIMISTIC);
em.lock(company2, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
|
Блокировка будет автоматически снята при завершении транзакции, снять её до этого вручную невозможно.
Пессимистичное блокирование
Пессимистичное блокирование выполняется на уровне базы и поэтому не требует вмешательств в код сущности. Так же, как и в случае с оптимистичным блокированием, поддерживаются блокировки чтения и записи:
- LockModeType.PESSIMISTIC_READ — данные блокируются в момент чтения и это гарантирует, что никто в ходе выполнения транзакции не сможет их изменить. Остальные транзакции, тем не менее, смогут параллельно читать эти данные. Использование этой блокировки может вызывать долгое ожидание блокировки или даже выкидывание PessimisticLockException.
- LockModeType.PESSIMISTIC_WRITE — данные блокируются в момент записи и никто с момента захвата блокировки не может в них писать и не может их читать до окончания транзакции, владеющей блокировкой. Использование этой блокировки может вызывать долгое ожидание блокировки.
Кроме того, для сущностей с @Version существует третий вариант пессимистичной блокировки:
- LockModeType.PESSIMISTIC_FORCE_INCREMENT — ведёт себя как LockModeType.PESSIMISTIC_READ, но в конце транзакции увеличивает значение поля @Version, даже если фактически сущность не изменилась.
Накладываются пессимистичные блокировки так же как и оптимистичные, вызовом метода lock():
1
2
3
|
em.lock(company1, LockModeType.PESSIMISTIC_READ);
em.lock(company2, LockModeType.PESSIMISTIC_WRITE);
em.lock(company3, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
|
Снимаются они тоже автоматические, по завершению транзакции.
Уровни изоляции транзакций
Говоря о блокировках и конкурентном доступе к данным нельзя не упомянуть о изоляции транзакций в базе.
Я уже писал раньше, что транзакция это атомарный набор операций, который может быть либо применён целиком, либо целиком отменён. Но кроме этого, сама база данных умеет изолировать транзакции, чтобы избегать их влияния друг на друга. Таких влияний обычно выделяют три варианта:
- Грязное чтение — транзакция при выполнении последовательных чтений объекта читает так же и изменения, вносимые другими транзакицями, но ещё даже не подтверждённые (commit). Эти изменения могут пропасть при откате других транзакций. То есть, когда одна транзакция меняет сущность, вторая транзакция при чтении этой сущности из базы сразу увидит изменение, даже если первая транзакция впоследствии откатится.
- Неповторяемое чтение — транзакция при выполнении последовательных чтений объекта читает изменения внесёнными другими, уже подтверждёнными и закончившимися транзакциями. То есть, если одна транзакция прочитала сущность, потом другая транзакция её обновила и успешно завершилась, то первая транзакция, если прочитает сущность ещё раз, увидит уже обновлённые данные.
- Фантомное чтение — транзакция при выполнении последовательных выборок строк по одним и тем же критериям получает каждый раз разный список строк, в которым попадают строки, добавленные другими, уже подтверждёнными и закончившимися транзакциями. То есть, если транзакция загружает список сущностей один раз, а в это время другая транзакция создаёт в базе новую сущность, то при повторной загрузке этого списка транзакция увидит и новую сущность тоже. Отчасти этого похоже на неповторяемое чтение, но здесь речь идёт только о новых сущностях (строках, если быть точным).
Существует четыре уровня изоляции транзакций, в которых вышеописанные эффекты могут происходить, а могут и не происходить:
- Serializable — транзакции полностью изолируются друг от друга и ни одна транзакция ни коим образом не влияет на другие. Самая низкая степень параллельности транзакций.
- Repeatable read — изменения данных, которые были прочитаны в транзакции ранее в транзакцию не попадают, другие транзакции не могут изменять данные, прочитанные этой транзакцией. Возможен эффект фантомного чтения, степерь параллельности транзакций выше, чем у Serializable.
- Read commited — транзакции получают изменения в данных от других транзакций, которые были успешно подтверждены. Возможны эффекты фантомного чтения и неповторяемого чтения. Этот уровень изоляции обычно используется по умолчанию в базах данных и обеспечивает хорошую степень параллельности транзакций.
- Read uncommited — самый низший уровень изоляции транзакций, который гарантирует только, что изменения, внёсённые одной транзакцией, не будут перезаписаные другой транзакцией. Подвержден всем эффектам влияния транзакций друг на друга. Обеспечивает наивысшую степерь параллельности транзакций.
Уровни изоляции транзакций можно рассматривать как механизм, позволяющий решать проблему паралльеного доступа к данным и изменения данных без явных ручных блокировок. С другой стороны, использование и адекватных задаче уровней изоляции и ручных блокировок обоих типов позволит поддерживать достаточную высокую степень конкурентности транзакций при гарантии корректного их обновления.