Какие есть проблемы с атомарностью операций в PostgreSQL и как их решать
Что проверяют
Понимаете ли вы разницу между атомарностью одного оператора и атомарностью бизнес-операции из нескольких шагов. Классический сценарий: списание с баланса при параллельных запросах. Следом спрашивают, как это выглядит в Spring и что делать, если транзакцию откатила база.
Как отвечать
Транзакция в PostgreSQL атомарна: применяются либо все изменения, либо ни одного. Проблемы начинаются там, где атомарность путают с изоляцией:
- потерянное обновление: на уровне по умолчанию
READ COMMITTEDсхема «прочитал баланс в Java, вычел, записал» теряет параллельные списания. Лечится атомарнымUPDATE accounts SET balance = balance - 100 WHERE id = ? AND balance >= 100с проверкой числа обновлённых строк; - пессимистичная блокировка:
SELECT ... FOR UPDATEдержит строку до конца транзакции. В Spring Data JPA это@Lock(LockModeType.PESSIMISTIC_WRITE); - оптимистичная блокировка: колонка версии и
@Versionв сущности. Конфликт превращается вOptimisticLockException, операцию повторяют; - строгие уровни:
REPEATABLE READиSERIALIZABLEв PostgreSQL не ждут, а обрывают конфликтующую транзакцию с SQLSTATE40001. Без цикла повторов в коде это просто ошибка у пользователя; - дедлоки: база сама находит их после
deadlock_timeout(по умолчанию 1 секунда) и откатывает одну из транзакций. Профилактика: брать блокировки в одинаковом порядке; - вставка без гонок:
INSERT ... ON CONFLICT DO UPDATEвместо «проверил, что записи нет, и вставил».
Про границы: транзакция базы не покрывает отправку в Kafka или вызов внешнего API, для этого есть паттерн transactional outbox.
Где спотыкаются
Говорят, что READ UNCOMMITTED даёт грязное чтение. В PostgreSQL этот уровень принимается синтаксически, но ведёт себя как READ COMMITTED, а REPEATABLE READ здесь не пропускает и фантомы.
Забывают, как работает @Transactional: вызов метода изнутри того же класса обходит прокси, и транзакции нет, а откат по умолчанию происходит только на непроверяемых исключениях.
Не знают, что после ошибки внутри транзакции PostgreSQL отвергает все следующие команды до ROLLBACK. Чтобы продолжить работу после частичного сбоя, нужен SAVEPOINT.