PostgreSQLMiddleTechnical

Что такое ACID? Объясни каждую букву на конкретном примере с банковским переводом.

ACID: Atomicity — все операции выполняются или ни одна; Consistency — данные соответствуют ограничениям до и после; Isolation — параллельные транзакции не видят промежуточных состояний друг друга; Durability — зафиксированные данные переживают сбой.

ACID: четыре свойства транзакций

Аббревиатура ACID описывает гарантии, которые СУБД предоставляет приложению при работе с транзакциями. Разберём каждое свойство на примере банковского перевода: Алиса переводит 1000 рублей Бобу.

A — Atomicity (Атомарность)

Транзакция — неделимая единица: либо все операции выполняются, либо ни одна. Если после списания с Алисы произойдёт сбой сервера, PostgreSQL при старте откатит незавершённую транзакцию по WAL-журналу, и баланс Алисы вернётся к исходному значению.

BEGIN;
  UPDATE accounts SET balance = balance - 1000 WHERE id = 1; -- Алиса
  -- Если здесь упадёт сервер — следующая строка не выполнится
  UPDATE accounts SET balance = balance + 1000 WHERE id = 2; -- Боб
COMMIT;
-- При ROLLBACK или сбое оба UPDATE отменяются

C — Consistency (Согласованность)

До начала и после завершения транзакции данные должны удовлетворять всем ограничениям и инвариантам. В нашем примере сумма балансов всех счетов должна оставаться неизменной. PostgreSQL проверяет CHECK-ограничения, FOREIGN KEY и UNIQUE в момент фиксации (или немедленно, для NOT DEFERRABLE).

ALTER TABLE accounts ADD CONSTRAINT balance_non_negative CHECK (balance >= 0);
-- Попытка перевести больше, чем есть на счёте:
BEGIN;
  UPDATE accounts SET balance = balance - 5000 WHERE id = 1; -- нарушит CHECK
COMMIT;
-- ERROR: new row for relation "accounts" violates check constraint "balance_non_negative"
-- Транзакция автоматически откатится

I — Isolation (Изолированность)

Параллельные транзакции не должны мешать друг другу. PostgreSQL реализует изоляцию через MVCC (Multi-Version Concurrency Control): каждая транзакция видит снапшот данных на момент начала. Уровни изоляции регулируют, какие аномалии допустимы:

  • READ COMMITTED (по умолчанию) — каждый оператор видит уже зафиксированные данные; возможны non-repeatable reads.
  • REPEATABLE READ — снапшот фиксируется на начало транзакции; исключает non-repeatable reads, но допускает phantom reads (в PostgreSQL — нет, реализован строже стандарта).
  • SERIALIZABLE — полная эмуляция последовательного выполнения транзакций через SSI.
-- Транзакция 1 читает баланс Алисы
BEGIN ISOLATION LEVEL REPEATABLE READ;
  SELECT balance FROM accounts WHERE id = 1; -- видит 5000
  -- В это время Транзакция 2 списывает 1000 и COMMIT
  SELECT balance FROM accounts WHERE id = 1; -- всё ещё видит 5000 (снапшот!)
COMMIT;

D — Durability (Долговечность)

После COMMIT данные сохранены даже при сбое питания или краше ОС. PostgreSQL использует WAL (Write-Ahead Log): сначала запись попадает в журнал на диске (fsync), затем в heap. При старте после краша база воспроизводит WAL и восстанавливает все зафиксированные транзакции.

-- Параметры, влияющие на Durability (не менять в production без понимания!)
SHOW synchronous_commit;   -- on (по умолчанию) — fsync WAL до ответа клиенту
SHOW fsync;                -- on (по умолчанию) — гарантирует сброс на диск

-- Для некритичных данных (логи) можно ослабить:
SET synchronous_commit = off;  -- риск потери последних ~200 мс транзакций при краше

Полный пример перевода с проверкой инвариантов

DO $$
DECLARE
  from_balance numeric;
BEGIN
  -- Блокируем строки в порядке id во избежание deadlock
  SELECT balance INTO from_balance
  FROM accounts
  WHERE id = 1
  FOR UPDATE;

  IF from_balance < 1000 THEN
    RAISE EXCEPTION 'Insufficient funds: balance is %', from_balance;
  END IF;

  UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
  UPDATE accounts SET balance = balance + 1000 WHERE id = 2;

  COMMIT;
END;
$$;

Подводные камни

  • Согласованность — ответственность приложения, а не только СУБД: PostgreSQL не знает о бизнес-инварианте «сумма балансов должна быть равна X»; если не написать CHECK или не проверить в коде, инвариант нарушится.
  • READ COMMITTED допускает non-repeatable reads: два SELECT в одной транзакции могут дать разные результаты, если между ними кто-то зафиксировал изменение. Для финансовых расчётов используйте REPEATABLE READ или SERIALIZABLE.
  • SERIALIZABLE в PostgreSQL реализован через SSI и может вызывать serialization failures (ERROR 40001) — приложение должно уметь повторять транзакцию.
  • synchronous_commit = off ослабляет Durability: при краше могут быть потеряны последние ~200 мс зафиксированных транзакций — клиент получил COMMIT, но данные не выжили.
  • Долгие открытые транзакции блокируют VACUUM и накапливают мёртвые версии строк (bloat), что деградирует производительность всей БД.
  • Явный SELECT ... FOR UPDATE необходим для предотвращения race condition при проверке баланса: без блокировки строки два параллельных перевода могут оба «увидеть» достаточный баланс и оба списать деньги.
  • SAVEPOINT позволяет частично откатить транзакцию, но не нарушает Atomicity — внешний COMMIT всё равно фиксирует всё, что осталось после ROLLBACK TO SAVEPOINT.

Common mistakes

  • Объяснять ACID только расшифровкой.
  • Думать, что ACID сам решает все race conditions.
  • Не связывать durability с WAL.

What the interviewer is testing

  • Просит пример rollback.
  • Проверяет isolation на конкурентном списании.
  • Уточняет роль WAL после commit.

Sources

Related topics