open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 07 · 01

ACID на практике

Транзакция — это забор: BEGIN…COMMIT делает группу записей всё-или-ничего, а SAVEPOINT режет внутри точки отката. Но ACID — это не управление конкурентностью: по умолчанию оно не сериализует.

SQL Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Платёжный сервис списывает с одного счёта и зачисляет на другой двумя UPDATE. Однажды ночью процесс падает между ними: с 1 200 клиентов списали, но не зачислили, и деньги просто исчезли из реестра. Фикс — три слова, которые автор забыл: BEGIN и COMMIT. Транзакция сделала бы так, что обе записи лягут вместе или никак. ACID не академичен; это разница между реестром и слухом.

Что на самом деле даёт каждая буква

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

ACID — это четыре гарантии, и инженеры путают ту, которой не получают, с теми, что получают.

  • Atomicity (атомарность) — каждый оператор между BEGIN и COMMIT ложится вместе, или никакой. Любая ошибка или ROLLBACK выбрасывает весь пакет. Полуготовое состояние никогда не видно и не сохраняется.
  • Consistency (согласованность) — закоммиченная транзакция оставляет базу удовлетворяющей всем ограничениям (foreign key, CHECK, unique, NOT NULL). Если оператор нарушит ограничение, транзакция падает с ошибкой и нужен откат. Postgres обеспечивает правила; твои инварианты («баланс не уходит в минус») ты кодируешь как ограничения, иначе они не гарантированы.
  • Isolation (изоляция) — конкурентные транзакции не видят незакоммиченной работы друг друга. Важно: у изоляции есть уровни: по умолчанию READ COMMITTED — это слабое обещание, а не полная сериализуемость. Именно эту букву люди считают сильнее, чем она есть.
  • Durability (долговечность) — как только COMMIT вернулся, изменение переживёт крах или потерю питания. Postgres гарантирует это, сбрасывая write-ahead log (WAL) на диск до подтверждения коммита.
-- Атомарный перевод денег: обе строки двигаются, или ни одна.
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;   -- только теперь перевод долговечен и виден другим

Если второй UPDATE упадёт (скажем, счёт 2 удалён), транзакция переходит в aborted-состояние; первый UPDATE отбрасывается при ROLLBACK. Деньги не утекают.

BEGIN, COMMIT, ROLLBACK и неявная транзакция

Каждый оператор уже внутри транзакции. Вне явного BEGIN Postgres оборачивает каждый одиночный оператор в собственную транзакцию, которая коммитится автоматически (autocommit). Поэтому один UPDATE accounts SET balance = balance * 1.05 сам по себе атомарен — все подходящие строки меняются или ни одна, даже без BEGIN.

BEGIN ты пишешь только когда нужно сделать несколько операторов одной атомарной единицей. После ошибки внутри транзакционного блока Postgres отвергает каждый следующий оператор с current transaction is aborted, пока ты не сделаешь ROLLBACK (или COMMIT, который Postgres превратит в откат). Никакого «проигнорировать ошибку и продолжить» — если только не используешь savepoint.

SAVEPOINT: частичный откат внутри одной транзакции

SAVEPOINT — именованная метка, к которой можно откатиться, не бросая всю транзакцию. Она позволяет одной транзакции пережить восстановимую ошибку в части своей работы.

BEGIN;
INSERT INTO jobs (id, status) VALUES (501, 'queued');

SAVEPOINT try_dedupe;
-- это может нарушить unique-индекс, если задача уже есть:
INSERT INTO jobs (id, status) VALUES (501, 'queued');
-- падает; вместо потери всей транзакции откатываем только до savepoint:
ROLLBACK TO SAVEPOINT try_dedupe;

-- первый INSERT всё ещё жив; продолжаем и коммитим его:
COMMIT;

Именно это большинство драйверов делают под капотом для «вложенных транзакций» — в Postgres нет настоящих вложенных транзакций, только savepoint’ы. Цена, однако, реальна: каждый savepoint потребляет subtransaction ID, и транзакция, открывающая десятки тысяч их, может вызвать пресловутый subtransaction (subxid) overflow — как только backend превышает 64 живых подтранзакции, остальным приходится обращаться к pg_subtrans на диске, и задержка чтения на нагруженном primary может скакнуть с микросекунд до миллисекунд. Savepoint — скальпель, а не тело цикла.

Частая ошибка

Самое частое непонимание ACID: «я обернул это в транзакцию, значит конкурентные обновления безопасны». Нет. Два перевода, которые SELECT один и тот же баланс на уровне по умолчанию READ COMMITTED, могут оба прочитать 500, оба вычесть 100 и оба записать 400 — lost update (потерянное обновление) — хотя каждый идеально атомарен и долговечен. Атомарность — про твои операторы как единицу; она ничего не говорит про чередование с другими сессиями. Сериализовать конкурентный доступ — задача уровней изоляции и явных блокировок, следующих уроков этого раздела.

Чего транзакция НЕ даёт

Транзакция по умолчанию — не мьютекс и не сериализуема. На READ COMMITTED (умолчание Postgres) каждый оператор видит свежий снимок, взятый в начале оператора, так что две конкурентные последовательности read-modify-write могут гоняться. Атомарность и долговечность ты получаешь бесплатно; защиту от lost update, write skew или фантомных строк ты не получаешь, пока не поднимешь уровень изоляции или не возьмёшь явные блокировки строк. Знание этой границы — вся причина, по которой существует остаток раздела.

Викторина

Две конкурентные сессии каждая выполняет на уровне по умолчанию: BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT. Стартовый баланс 500. Что гарантировано?

Расставь шаги по порядку

Упорядочи жизненный цикл атомарного перевода в два шага, восстанавливающегося от ошибки дубль-вставки через savepoint:

  1. 1 BEGIN — открыть явную транзакцию, чтобы следующие операторы были одной единицей
  2. 2 Сделать первую запись (списать со счёта 1)
  3. 3 SAVEPOINT перед записью, которая может упасть
  4. 4 Попытаться рискованную запись; при ошибке ROLLBACK TO SAVEPOINT отменяет только этот шаг
  5. 5 COMMIT — сделать уцелевшие записи долговечными и видимыми
Вспомните перед уходом
  1. 01
    Что гарантирует атомарность и к какой наименьшей единице она уже применяется без BEGIN?
  2. 02
    Чем SAVEPOINT отличается от ROLLBACK и какова его продакшн-цена на масштабе?
  3. 03
    Почему оборачивание read-modify-write в транзакцию НЕ предотвращает lost update по умолчанию?
Итог

ACID — это четыре отдельных обещания, и сеньоры держат их раздельно. Atomicity: BEGINCOMMIT делает группу записей всё-или-ничего; любая ошибка отравляет транзакцию до ROLLBACK, и даже одиночный autocommit-оператор атомарен. Consistency: коммит должен оставить все ограничения удовлетворёнными — Postgres обеспечивает объявленные правила, а бизнес-инварианты ты кодируешь как ограничения. Isolation: конкурентные транзакции прячут незакоммиченную работу, но изоляция уровневая, и умолчание READ COMMITTED — слабое обещание, которое не сериализует. Durability: как только COMMIT вернулся, WAL на диске и изменение переживёт крах. SAVEPOINT добавляет частичный откат внутри одной транзакции, с реальной subxid-ценой на масштабе. Единственное, чего транзакция не даёт по умолчанию, — защиты от конкурентных гонок read-modify-write; это требует уровней изоляции и явных блокировок, которые ты встретишь дальше, включая проблему lost update, ради которой существует SELECT FOR UPDATE. Теперь, когда встретишь отчёт об инциденте с «потерянным обновлением» или «балансом ушедшим в минус», первый вопрос — какую букву ACID предполагали, но реально не получили.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.