Что происходит при параллельном запуске двух транзакций с одинаковыми условиями?
Что проверяют
Понимаете ли вы, что параллельные транзакции не выстраиваются в очередь сами, и знаете ли, чем именно защищаете инвариант: уровнем изоляции, блокировкой или версией строки. Классический вход в тему потерянного обновления.
Как отвечать
Разобрать конкретный сценарий, а не перечислять уровни изоляции подряд. Две транзакции читают строку по одинаковому условию, считают новое значение в памяти приложения и пишут обратно. На уровне READ COMMITTED, который стоит по умолчанию в PostgreSQL, обе прочитают одно и то же, обе запишут, и второе изменение затрёт первое. Ошибки не будет, будет неверный остаток.
Что происходит в самой базе:
- обычный
SELECTблокировок не ставит, поэтому конфликт и не обнаруживается; UPDATEодной строки блокируется: вторая транзакция ждёт коммита первой и наREAD COMMITTEDперечитывает строку перед записью;- на
REPEATABLE READв PostgreSQL вторая транзакция вместо ожидания получит ошибку сериализации, и повторять её надо целиком; SERIALIZABLEловит и конфликты по диапазону строк, ценой отказов и повторов.
Защиту называют парой. Пессимистично: SELECT ... FOR UPDATE, в JPA это LockModeType.PESSIMISTIC_WRITE, строка блокируется сразу. Оптимистично: поле с @Version, при записи проверяется, что версия не менялась, иначе OptimisticLockException и повтор. Третий путь, часто лучший: считать прямо в запросе, тогда гонка исчезает вместе с чтением в память.
Где спотыкаются
Думают, что @Transactional сам защищает от параллельных изменений. Он только открывает и закрывает транзакцию, а конфликты разруливает база по своему уровню изоляции.
Второе: повышают уровень изоляции и не пишут повторы. На REPEATABLE READ и SERIALIZABLE база отвечает отказом, и без ретрая пользователь видит ошибку вместо результата.
Третье: берут блокировки в разном порядке в разных местах кода и получают взаимную блокировку. База разрубит её, откатив одну транзакцию, но лечится это не увеличением таймаута, а единым порядком захвата строк.