Learning Platform
Урок 13.01 · 18 мин
Средний
TransactionsACIDAtomicityConsistencyIsolationDurability

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

Транзакция — это группа операций, которая выполняется как единое целое. Либо все операции применяются к данным, либо ни одна. Между ними не существует промежуточного состояния, видимого снаружи.

Классический пример — банковский перевод. Списать 1000 ₽ со счёта Ани и зачислить 1000 ₽ на счёт Бориса — это две операции UPDATE. Если между ними упадёт сервер, у Ани будет −1000, а у Бориса не появится +1000. Деньги «исчезнут» из системы.

Транзакция превращает эти две операции в одну логическую
, и СУБД гарантирует, что либо обе сработают, либо обе откатятся.

Над этой гарантией стоит набор из четырёх свойств — ACID: Atomicity, Consistency, Isolation, Durability. Их сформулировал в 1983 году Andreas Reuter и Theo Härder в каноническом обзоре. С тех пор это базовый словарь, на котором СУБД договариваются с разработчиком о том, что именно гарантируется при работе с данными.

ACID — четыре буквы, четыре гарантии

Каждая буква отвечает за свой аспект надёжности транзакций. Это не «магия СУБД», а конкретный контракт.

A — Atomicityвсё или ничего
применяется целиком или откатываетсяБез atomicity между двумя UPDATE можно получить «полу-применённое» состояние при падении.
C — Consistencyинварианты сохраняются
после commit все ограничения схемы валидныCHECK, FOREIGN KEY, UNIQUE — после commit всё это валидно. Если транзакция нарушает — откат.
I — Isolationпараллельные не мешают
результат как при последовательном порядкеУровень изоляции выбирается явно. SERIALIZABLE — самый строгий, READ COMMITTED — default в PostgreSQL.
D — Durabilitycommit = на диске
после commit изменения переживут падениеГарантируется через write-ahead log: WAL записывается с fsync до того, как commit вернётся.

Дальше — по каждой букве отдельно, с конкретным примером того, что нарушается без неё.

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

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

Это слово специально позаимствовано из физики: атом в древнегреческом — «неделимое». То же и здесь: транзакция — это единица, которую СУБД не позволяет «надломить пополам». Внутри неё может быть один UPDATE или тысяча — для внешнего наблюдателя всё это один акт.

Самый простой способ это увидеть — выполнить два UPDATE внутри транзакции, потом сделать ROLLBACK. Изменения исчезнут, как будто их никогда не было.

Atomicity: BEGIN, UPDATE, ROLLBACK — никаких следов:

PostgreSQL

Внутри транзакции in_stock стало отрицательным — но ROLLBACK всё отменил. До COMMIT любые UPDATE/INSERT/DELETE находятся в подвешенном состоянии: видны только текущей сессии, могут быть откачены одним словом.

Что было бы без atomicity: если сервер упал бы между UPDATE products и UPDATE order_items, мы получили бы товар с уменьшенным остатком, но без заказа. Это рассинхрон, который потом ловят костыли вроде ночных скриптов «проверь, что не побилось». В мире NoSQL без транзакций такие скрипты называют «reconciliation jobs» — целая инженерная дисциплина, которая в SQL не нужна благодаря atomicity.

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

Atomicity при сбое: транзакция либо вся, либо никак

Любая точка сбоя в середине транзакции до COMMIT — равносильна ROLLBACK. После COMMIT транзакция уже зафиксирована.

BEGINt = 0
UPDATE At = 1изменения накоплены в WAL-буфере
UPDATE Bt = 2
сбой в этой точкеt = 2.5WAL не получил commit-запись. WAL replay при перезапуске откатит транзакцию.
(альтернатива) COMMITt = 3commit-запись в WAL с fsync. Транзакция зафиксирована безусловно.

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

Consistency — самая «расплывчатая» буква в ACID. Формально: после commit база остаётся в согласованном состоянии, то есть все ограничения (CHECK, FOREIGN KEY, UNIQUE, NOT NULL) выполнены.

Это не «СУБД гарантирует, что ваши инварианты соблюдены». Это: «СУБД проверит те инварианты, которые вы ей объявили». Если вы не написали CHECK (balance >= 0) — никто не помешает отправить баланс в минус. Consistency — это про обещанные правила.

В литературе по distributed systems consistency имеет совсем другой смысл — там это про синхронность реплик (CAP-теорема, eventual consistency). Это разные понятия с одним словом. ACID-consistency — про инварианты на одной базе. CAP-consistency — про согласованность между узлами кластера. Не путать.

Consistency: CHECK не пропустит транзакцию, нарушающую инвариант:

PostgreSQL

UPDATE упадёт с ошибкой check constraint accounts_balance_cents_check, и транзакция автоматически перейдёт в aborted state. Любая следующая команда внутри той же транзакции вернёт ошибку «current transaction is aborted, commands ignored until end of transaction block». Единственный выход — ROLLBACK.

Это и есть consistency в действии: СУБД отказывается фиксировать состояние, где инвариант нарушен. Но обязанность сформулировать инварианты — на вас. Без CHECK СУБД бы пропустила минусовой баланс без вопросов.

Историческая ремарка про Consistency

Стоит сказать: буква C в ACID — самая спорная. Многие исследователи (включая Härder, одного из авторов оригинальной формулировки) позже признавались, что C добавили скорее «для красоты слова». На самом деле consistency — это следствие атомарности плюс корректно объявленных ограничений, не самостоятельное свойство СУБД.

То есть СУБД не «обеспечивает consistency» в смысле «следит за вашей бизнес-логикой». Она обеспечивает atomicity (всё или ничего) и проверяет constraints (CHECK, FK, UNIQUE). Из этих двух свойств следует, что после commit инварианты, которые вы объявили в схеме, выполнены. Это и называется consistency.

Поэтому современные учебники иногда говорят, что «настоящие» свойства транзакций — это Atomicity, Isolation, Durability. Consistency — это то, что вы получаете в результате, если правильно использовали atomicity и объявили constraints.

I — Isolation (изоляция)

Isolation отвечает за параллельные транзакции. Если две сессии одновременно меняют данные, результат должен быть как если бы они выполнялись последовательно — либо A, потом B; либо B, потом A. Параллельность не должна порождать состояний, невозможных в последовательном расписании.

На практике строгая изоляция дорогая, поэтому СУБД предлагают уровни изоляции — компромиссы между производительностью и количеством разрешённых аномалий. PostgreSQL по умолчанию работает в READ COMMITTED — это самый слабый практичный уровень. Самый строгий — SERIALIZABLE. Подробно про уровни и про конкретные аномалии — в уроках 4 и 5 этого модуля.

Сейчас важно понять: isolation — это про то, как ваша транзакция взаимодействует с чужими, выполняющимися одновременно. Atomicity и consistency работают и в одиночной системе, isolation — только в многопользовательской.

Простой пример: два пользователя одновременно покупают последний iPhone на складе. Транзакция A читает остаток (1 шт.) и проверяет «есть в наличии». Транзакция B параллельно делает то же самое. Обе видят остаток = 1, обе создают заказ, обе списывают по 1. Если isolation слабая — мы продали один iPhone дважды. Если строгая (SERIALIZABLE) — одна из транзакций откатится. Этой простой картинке посвящён весь следующий урок про аномалии.

Внутри одной сессии isolation не видна, но можно увидеть «свои» изменения, которых не видят другие сессии:

PostgreSQL

D — Durability (устойчивость)

Durability — самое простое из четырёх. После того как COMMIT вернул управление приложению, изменения должны пережить что угодно: внезапное отключение питания, kill -9 процесса базы, падение ОС. При следующем запуске они должны быть на месте.

Механизм —

write-ahead log (WAL)
. PostgreSQL пишет каждое изменение сначала в WAL-файл с fsync, и только после того, как fsync вернулся успешно, возвращает COMMIT OK приложению. Если в этот момент выключить питание — после перезапуска СУБД прочитает WAL и применит все изменения, для которых был зафиксирован commit. То, что было «в процессе», — откатит.

Это объясняет одну вещь, важную для backend-разработчиков: COMMIT — это медленная операция. Не потому что СУБД ленивая, а потому что она физически ждёт ответа от диска о том, что данные действительно записаны. Поэтому если у тебя 100 коротких транзакций — батчинг их в одну может ускорить запись в десятки раз. Каждый отдельный commit стоит ~миллисекунду (на SSD) или ~10ms (на HDD).

Durability через WAL: что происходит при COMMIT

Запись в WAL и fsync — это и есть «договор» о durability. Только после успешного fsync клиент получает OK.

1. UPDATE/INSERT в транзакцииизменения накапливаются в WAL-буфере и shared_buffersЭто всё в памяти. На диск ещё ничего не сохранилось.
2. COMMIT отправлен клиентомPostgreSQL пишет commit-запись в WAL-буфер
3. write() WAL-буфера на дискOS принимает данные, но они ещё могут быть в page cache
4. fsync() WAL-файлаждём, пока физически попадёт на устройствоЭто критичный момент. Если упасть до fsync — изменений нет. После fsync — они есть гарантированно.
5. COMMIT OK клиентутеперь приложение может считать, что данные сохранены
...позжеdata-страницы лениво пишутся на диск; checkpoint каждые ~5 минутЭто уже не блокирует клиента — расходы амортизируются.

Из этой картинки следует один важный фокус: можно отключить fsync в настройках (synchronous_commit = off) — и тогда COMMIT возвращается мгновенно, без ожидания fsync. Скорость растёт в 10+ раз. Но durability в этом случае ослабляется: если упасть в окне между COMMIT OK и физическим fsync (типично 200ms), последние транзакции потеряются. Это компромисс для специфических нагрузок — например, для metrics/logs, где потеря последних 200ms терпима. Для финансов — нельзя.

ACID на практике: типичные ошибки

Несколько практических наблюдений, которые я часто встречаю.

Открыли транзакцию и забыли закрыть. Психопатологическая ошибка: разработчик в psql написал BEGIN, что-то посмотрел, переключился на другую вкладку. Транзакция продолжает существовать часами. Она не пишет ничего, но держит snapshot, и VACUUM перестаёт чистить таблицы. Через неделю — раздутая таблица, медленные запросы, паника в команде. Запрос для поиска: SELECT pid, xact_start FROM pg_stat_activity WHERE state IN ('idle in transaction', 'idle in transaction (aborted)');.

Делают update без транзакции. Пишут несколько связанных UPDATE без BEGIN. Каждый — своя транзакция, между ними нет atomicity. Если упасть после первого — данные в неконсистентном состоянии. Защита: ORM-обёртка @transaction.atomic или явный BEGIN/COMMIT.

Полагаются на ACID для бизнес-логики. «У нас же транзакции, значит, всё ок». Нет, ACID не знает, что в вашей бизнес-логике важно. Если вам важна уникальность email — пишите UNIQUE constraint, иначе СУБД её не обеспечит.

Делают HTTP-запросы внутри транзакции. Транзакция длится секундами вместо миллисекунд. Локи держатся долго, VACUUM не работает. Правило: всё «дорогое» (HTTP, S3, файлы) — снаружи транзакции.

ACID — это не магия, это контракт

Главное заблуждение про ACID: «база сама всё гарантирует». На самом деле:

  • Atomicity работает только внутри одной транзакции. Если у вас две транзакции в разных сессиях — atomicity между ними не действует. Распределённые транзакции (2PC) — это отдельная сложная тема, и реальные системы их избегают.
  • Consistency обеспечивается только теми ограничениями, которые вы написали. СУБД не угадывает бизнес-логику.
  • Isolation работает на том уровне, который вы выбрали. На дефолтном READ COMMITTED некоторые аномалии разрешены — об этом весь урок 4.
  • Durability гарантируется только после возврата COMMIT. Если вы не дождались ответа от базы и считаете данные «сохранёнными» — это ваша ошибка, не СУБД.

ACID — это контракт: вы получаете гарантии в обмен на правильное использование транзакций, ограничений и явное управление уровнем изоляции.

Что произойдёт без ACID

Каждое нарушение свойства приводит к конкретному классу багов. Все они встречаются в реальных системах, особенно в NoSQL без транзакций.

без Aполу-применённые операции: списали со счёта, не зачислилиАтомарность нарушается при падении в середине группы операций без транзакции.
без Cневалидные данные в базе: отрицательные балансы, orphan-foreign-keysБез CHECK/FK/UNIQUE база примет любой мусор.
без Idirty read, lost update, write skew — данные «портят друг друга»Конкурентные транзакции дают результаты, невозможные при последовательном выполнении.
без Dcommit прошёл, питание упало — данных нетЭто типичная проблема legacy MyISAM-таблиц в MySQL, например.

Транзакции на уровне приложения

Часто разработчики не знают, что они уже работают с транзакциями, даже не вызвав BEGIN. Это потому что в драйверах (psycopg, pg, JDBC) обычно есть концепция autocommit = false — драйвер сам вставляет BEGIN перед первым запросом и COMMIT после.

В psycopg (Python):

conn = psycopg.connect(...)
cur = conn.cursor()
cur.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
cur.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
conn.commit()  # вот это COMMIT

Между первым execute и conn.commit() идёт одна транзакция. Если упасть до commit() — оба UPDATE откатятся (или ни один не применится). Это ровно та же atomicity, о которой мы говорили выше.

В Node.js pg:

const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query('UPDATE ...');
  await client.query('UPDATE ...');
  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
}

Здесь явно — BEGIN/COMMIT/ROLLBACK пишем сами. Драйвер не делает магии за нас.

В ORM (Django, SQLAlchemy, Rails) — обычно есть декоратор/контекст-менеджер @transaction.atomic или with session.begin(). Они оборачивают блок кода в транзакцию, делают commit при успехе и rollback при исключении.

Главное правило для backend-разработчика: всегда понимай, в какой транзакции выполняется твой код. Это влияет на видимость данных, на блокировки, на retry-логику. ACID не «работает магически» — оно работает, потому что вы (или ваш ORM) явно открыли транзакцию.

А что с NoSQL

Долгое время в маркетинге NoSQL ACID был синонимом «медленно и не масштабируется». Появилось альтернативное обещание — BASE: Basically Available, Soft state, Eventually consistent. То есть «данные когда-нибудь согласуются, но сейчас могут не быть».

Сегодня история выровнялась: MongoDB и DynamoDB научились ACID-транзакциям (с ограничениями), PostgreSQL научился горизонтальному масштабированию через шардирование. Жёсткое противопоставление SQL vs NoSQL по признаку ACID — это уже устаревший взгляд. Но PostgreSQL остаётся образцом: все четыре свойства, все четыре уровня изоляции, mature WAL, точечные блокировки — всё это в коробке.

В современных распределённых системах появились новые понятия — linearizability, serializability (изоляция в распределённой среде), causality. Они расширяют классическое ACID на кластеры из множества узлов. Если интересно — это материал отдельного курса по distributed systems. Для понимания PostgreSQL на одном инстансе хватает классики Reuter–Härder.

Проверка знанийKnowledge check
Транзакция выполнила два UPDATE и закоммитилась. Через секунду питание сервера упало. Что гарантирует ACID, и что не гарантирует?
ОтветAnswer
ACID (через durability) гарантирует, что после перезапуска оба UPDATE будут видны в базе. Механизм: WAL был записан с fsync до того, как COMMIT вернулся приложению — значит, на диске уже есть полная запись о транзакции. При recovery PostgreSQL применит её к data-страницам. Что НЕ гарантирует ACID: что приложение само корректно дождалось COMMIT. Если приложение отправило SQL и не проверило ответ от базы (например, упало раньше) — оно не узнает, прошла ли транзакция. Это классическая проблема "не знаю, надо ли повторять": решается идемпотентностью операций или явной проверкой состояния после перезапуска.

Распространённые ошибки понимания ACID

Когда я провожу собеседования, я регулярно встречаю одни и те же недоразумения. Перечислю, чтобы вы их не повторяли.

«ACID гарантирует, что данные не потеряются» — нет. ACID гарантирует durability после возврата COMMIT. Если ваше приложение упало до COMMIT — данных нет, и это нормально. Защита от такой потери — это уже логика приложения (например, queue-based retry).

«Транзакция — это всегда быстрее» — нет, это всегда дороже, чем autocommit одной командой. Транзакция требует ведения списка локов, версий и WAL-записей. Транзакция выигрывает в скорости только когда она оборачивает много связанных команд и заменяет N отдельных COMMIT’ов на один.

«SERIALIZABLE — самый правильный уровень, всегда используйте его» — нет. SERIALIZABLE может вернуть serialization failure и потребовать retry. В коде это нужно явно обработать. Если ваше приложение не готово к retry — SERIALIZABLE может сделать ему хуже, чем RC + явные блокировки.

«CHECK = проверка на стороне клиента, она медленная» — нет, CHECK выполняется в PostgreSQL на стороне сервера, до записи на диск. Это часть валидации INSERT/UPDATE. Стоимость минимальна — копейки.

«Транзакция блокирует чтения других сессий» — нет, благодаря MVCC. Читатели в PostgreSQL никогда не блокируют писателей и наоборот. Блокировка появляется только когда два писателя пишут одну строку.

Что дальше в модуле

В следующих уроках мы будем не пересказывать теорию, а видеть всё это руками. Урок 2 — синтаксис BEGIN/COMMIT/ROLLBACK и работа autocommit’а. Урок 3 — savepoints для частичного отката. Урок 4 — четыре аномалии параллельности на конкретных сценариях из e-commerce. Урок 5 — уровни изоляции и snapshot isolation в PostgreSQL. Урок 6 — явные блокировки SELECT FOR UPDATE и очереди задач на SKIP LOCKED.

К концу модуля у вас будет полное понимание, как PostgreSQL обеспечивает ACID — и как писать код, который этим пользуется.

Дальше за рамками модуля — внутреннее устройство WAL, MVCC, vacuum’а; распределённые транзакции и 2PC; репликация и failover. Эти темы — для отдельных курсов по PostgreSQL internals и distributed systems. Здесь же мы остановимся на уровне разработчика-практика: знать, что гарантировано, как этим пользоваться, какие есть подводные камни.

Как Postgres реализует Atomicity через MVCC WAL и Durability — как commit оказывается на диске

Чек-лист

  • Транзакция — это группа операций, выполняемая как единое целое.
  • ACID — четыре гарантии: Atomicity (всё или ничего), Consistency (инварианты сохраняются), Isolation (параллельные не мешают), Durability (commit = на диске).
  • Atomicity работает только внутри одной транзакции; ROLLBACK откатывает всё.
  • Consistency проверяет объявленные вами ограничения (CHECK, FK, UNIQUE). СУБД не знает вашу бизнес-логику.
  • Isolation — отдельная большая тема; PostgreSQL по умолчанию даёт READ COMMITTED.
  • Durability реализуется через WAL: запись в лог с fsync до того, как commit вернулся.
  • ACID — это контракт, а не магия: вы получаете гарантии в обмен на правильное использование транзакций и ограничений.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что именно гарантирует свойство Atomicity в ACID?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6