Перейти к содержанию
Innovialabs

Транзакции и ACID в базах данных простыми словами

Основы4 мин

Коротко

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

Коротко о главном

Транзакция — группа операций с базой данных, которые должны выполниться или не выполниться вместе. Классический пример — перевод денег: списать 100 у Алишера и зачислить 100 Бобуру. Если сервер упадёт между этими шагами, деньги не должны исчезнуть.

ACID — четыре гарантии, которые делают это безопасным:

БукваСвойствоНа примере перевода
AАтомарность (Atomicity)Выполняются оба обновления или ни одного
CСогласованность (Consistency)Баланс не уходит в минус, если это запрещено правилом
IИзоляция (Isolation)Параллельный перевод не видит недоделанных данных
DДолговечность (Durability)После «успешно» перевод переживёт отключение питания

Перевод на SQL

BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- Алишер
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- Бобур

COMMIT;
  • BEGIN (или START TRANSACTION) открывает транзакцию.
  • COMMIT фиксирует все изменения и делает их видимыми для других.
  • ROLLBACK отменяет всё, что сделано после BEGIN.

Если посередине что-то пошло не так, выполняется ROLLBACK, и база возвращается в состояние до перевода.

Атомарность: всё или ничего

База отслеживает каждое изменение внутри транзакции. Если транзакцию откатили или соединение оборвалось до COMMIT, изменения отбрасываются. Не бывает состояния, в котором у Алишера списали 100, а Бобур их не получил.

Согласованность: правила всегда соблюдены

Согласованность означает, что транзакция переводит базу из одного корректного состояния в другое. «Корректность» задают ваши ограничения:

ALTER TABLE accounts
  ADD CONSTRAINT balance_not_negative CHECK (balance >= 0);

Если у Алишера только 50, первый UPDATE нарушит проверку, оператор завершится ошибкой, и транзакцию нельзя будет зафиксировать. Так же работают внешние ключи, NOT NULL и UNIQUE. База следит за правилами, которые вы объявили; необъявленные бизнес-правила остаются задачей приложения.

Изоляция: параллельные транзакции не мешают друг другу

В реальной системе одновременно идёт множество транзакций. Без изоляции два перевода со счёта Алишера могли бы оба прочитать баланс 100, оба списать по 100 и оставить счёт в неверном состоянии.

Базы предлагают уровни изоляции — от более слабых и быстрых к строгим:

УровеньОт чего защищает
Read UncommittedПочти ни от чего; можно прочитать незафиксированные данные
Read CommittedОт грязного чтения: видны только зафиксированные данные
Repeatable ReadЕщё от неповторяемого чтения: прочитанная строка не меняется у вас под ногами
SerializableЕщё от фантомов: результат такой, будто транзакции шли по очереди

Значения по умолчанию отличаются: в PostgreSQL — Read Committed, в MySQL с InnoDB — Repeatable Read. Точное поведение на каждом уровне тоже различается между СУБД, так что сверяйтесь с документацией своей базы.

Для перевода на уровне Read Committed заблокируйте строки, которые будете менять:

BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- проверяем баланс в приложении, затем:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

FOR UPDATE заставит второй перевод с того же счёта ждать, пока первый не завершится.

Долговечность: зафиксировано — значит сохранено

Когда COMMIT вернул успех, изменение записано в надёжный журнал на диске (в PostgreSQL это WAL, write-ahead log, в InnoDB — redo log). После сбоя база проигрывает журнал и восстанавливает зафиксированные данные. Долговечность защищает от перезапуска сервера, но не от потери самого диска — для этого нужны бэкапы и реплики.

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

Большинство драйверов и ORM оборачивают этот шаблон за вас. Идея везде одинакова:

with conn:                      # открывает транзакцию
    with conn.cursor() as cur:
        cur.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (100, 1))
        cur.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (100, 2))
# commit при успехе, rollback при исключении

Частые ошибки

  • Неожиданный autocommit: без явной транзакции каждый оператор фиксируется сам, и сбой между двумя обновлениями оставляет полперевода.
  • Долгие транзакции: держать транзакцию открытой, пока ждёте пользователя или внешний API, — значит держать блокировки и тормозить других.
  • Вызов внешних сервисов внутри транзакции: откат не отменит отправленное SMS или платёжный запрос.
  • Нет повторов: на уровне Serializable или при взаимных блокировках (deadlock) база может намеренно прервать транзакцию — приложение должно её повторить.

Частые вопросы

Многие — в той или иной мере. Например, MongoDB поддерживает транзакции над несколькими документами, а у Redis есть свой механизм MULTI/EXEC с другими гарантиями. Прежде чем доверять базе деньги или остатки, проверьте, что именно она обещает.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.