Транзакция это группа операций, которая выполняется как одно целое.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- или ROLLBACK, если что-то пошло не так
ACID
- Atomicity (атомарность): либо все изменения, либо ни одного.
- Consistency (согласованность): после транзакции соблюдены все ограничения (ключи, CHECK, внешние ключи).
- Isolation (изоляция): параллельные транзакции не мешают друг другу сильнее, чем позволяет уровень изоляции.
- Durability (долговечность): после COMMIT данные переживут падение сервера. В PostgreSQL это обеспечивает WAL (журнал упреждающей записи): изменение сначала пишется в журнал на диск.
Аномалии параллельного доступа
- Dirty read: читаем незакоммиченные изменения другой транзакции.
- Non-repeatable read: читаем строку дважды, между чтениями её изменили и закоммитили, видим разное.
- Phantom read: повторный запрос с тем же условием возвращает новые строки, которые вставила другая транзакция.
- Lost update: две транзакции прочитали значение, обе записали своё, одно изменение потерялось.
- Write skew: две транзакции читают пересекающиеся данные и меняют разные строки, вместе нарушая инвариант (оба дежурных врача снялись с дежурства, каждый видел, что второй остаётся).
Уровни изоляции по стандарту
| Уровень | Dirty read | Non-repeatable | Phantom |
|---|---|---|---|
| Read Uncommitted | возможно | возможно | возможно |
| Read Committed | нет | возможно | возможно |
| Repeatable Read | нет | нет | возможно |
| Serializable | нет | нет | нет |
Как это в PostgreSQL
- Уровень по умолчанию: Read Committed. Каждый оператор видит снимок данных на момент своего начала.
- Read Uncommitted ведёт себя как Read Committed: грязного чтения в PostgreSQL нет вообще.
- Repeatable Read работает как snapshot isolation: снимок берётся один на всю транзакцию, поэтому фантомов тоже нет. Если пытаемся изменить строку, которую после нашего снимка изменила другая закоммиченная транзакция, получаем ошибку сериализации, транзакцию надо повторить.
- Serializable (SSI) дополнительно ловит write skew и откатывает одну из конфликтующих транзакций. Приложение обязано уметь повторять транзакцию.
MVCC
PostgreSQL не перезаписывает строку при UPDATE: он создаёт новую версию, а старую помечает удалённой. Каждая транзакция видит версии, подходящие её снимку. Поэтому читатели не блокируют писателей, а писатели читателей. Цена: мёртвые версии копятся, их убирает VACUUM (обычно autovacuum). Долгая открытая транзакция мешает VACUUM чистить таблицы и раздувает их.
Блокировки
Две транзакции, меняющие одну строку, всё равно конфликтуют: вторая ждёт, пока первая завершится.
SELECT ... FOR UPDATEблокирует выбранные строки до конца транзакции: классический способ избежать lost update (пессимистичная блокировка).FOR UPDATE SKIP LOCKEDпропускает занятые строки: так делают очереди задач на таблице.- Оптимистичная блокировка: колонка
version,UPDATE ... SET version = version + 1 WHERE id = 1 AND version = 7, если затронуто 0 строк, кто-то успел раньше. - Атомарный UPDATE
SET balance = balance - 100безопаснее, чем «прочитал в приложении, посчитал, записал».
Deadlock: транзакция A держит строку 1 и ждёт строку 2, B держит 2 и ждёт 1. PostgreSQL обнаруживает это и откатывает одну из них. Профилактика: брать блокировки в одном порядке (например, по возрастанию id).
Частые ошибки
- Читать в приложении, считать и записывать без блокировки или версии.
- Держать транзакцию открытой во время HTTP-запроса во внешний сервис.
- Включить Serializable и не написать повтор транзакции.
- Думать, что Read Uncommitted в PostgreSQL покажет грязные данные.
Что спросят на собесе
- Расшифруй ACID.
- Какие бывают аномалии и какие уровни изоляции от чего защищают.
- Какой уровень по умолчанию в PostgreSQL и как работает MVCC.
- Как избежать lost update при списании баланса.
- Что такое deadlock и как его избежать.