ACID коротко: зачем транзакция

О лекции

Транзакция задаёт границу «всё или ничего» для нескольких изменений. ACID описывает свойства этой границы, а не магию изоляции и не бизнес-инварианты приложения.

PostgreSQL postgres собеседование

Транзакция — граница «всё или ничего» для нескольких изменений. Один SQL-оператор в PostgreSQL уже выполняется в транзакции: включён autocommit. Явный BEGIN нужен, когда два или больше изменения должны стать одним фактом.

Пример. Списать 100 со счёта и записать проводку. Без общей границы процесс упал между UPDATE и INSERT — баланс уменьшился, проводки нет. Откатить это уже некому.

Типичные пары: создать заказ и его позиции. Зарезервировать строку склада и оформить отгрузку. ACID — четыре свойства этой границы. Их путают с репликацией, с уровнем изоляции и с правилами предметной области. Это разные слои.

В сервисе на Go транзакция почти всегда видна как Begin/Commit в репозитории. Граница должна быть короткой: внутри — несколько SQL. Снаружи — не HTTP к платёжке и не публикация в Kafka. Иначе вы держите снимок и локи, пока ждёте сеть.

Четыре буквы

Atomicity

Атомарность: commit применяет все изменения транзакции, rollback — ни одного. Это про одну сессию и один движок, не про «заказ в Postgres и сообщение в брокере». Между двумя системами атомарности нет — отсюда outbox (сначала запись в таблицу, потом отдельная доставка).

Consistency

Согласованность после commit: не нарушены ограничения схемы. PK, FK, CHECK, UNIQUE, NOT NULL. Инвариант «скидка не больше 10%» СУБД сама не знает, пока вы не выразили его constraint-ом или кодом. Consistency в ACID — про схему, не про бизнес-брошюру.

Isolation

Изоляция: параллельные транзакции не видят чужой промежуточный хаос. Насколько не видят — уровень изоляции. Dirty read (чтение незакоммиченного) Postgres не отдаёт даже на Read Committed. Остальные аномалии зависят от уровня.

Durability

Стойкость: после успешного commit изменения записаны в WAL (журнал redo) и переживут крэш процесса. Это не обещание, что синхронная реплика уже приняла байты. И не обещание, что диск реплики в другой зоне цел. Durability локальна, пока вы явно не включили синхронную репликацию.

BEGIN;
UPDATE accounts
   SET balance = balance - 100
 WHERE id = 1
   AND balance >= 100;
-- если rowcount = 0, делайте ROLLBACK: иначе гонка уйдёт в минус
INSERT INTO ledger (account_id, amount) VALUES (1, -100);
COMMIT;

Два изменения одного бизнес-факта в одной транзакции. Без BEGIN падение между UPDATE и INSERT оставит баланс без проводки.

Проверка balance >= 100 в WHERE — не полная защита от гонки двух списаний. На Read Committed оба могут прочитать 100 и оба пройти, если нет дополнительного лока или ограничения. Транзакция даёт атомарность пары операторов. За гонку на одной строке отвечают изоляция и локи.

Типичная ошибка

Держать открытый BEGIN, пока хендлер ходит в HTTP, очередь или файловое хранилище. Долгая транзакция сдвигает xmin horizon: vacuum не чистит мёртвые версии, растёт bloat. DDL в Postgres тоже транзакционный (кроме пары исключений), но лок на таблицу во время миграции не бесплатный.

На собеседовании

Расшифруйте ACID коротко и сразу дайте пример двух UPDATE без BEGIN. Отделите consistency СУБД (constraints после commit) от бизнес-инварианта, который схема не знает. Durability свяжите с WAL, не с «реплика всегда догнала».