Транзакция и ACID

Материал из llmdocs — справочник разработчика
Портал: Базы данных
Транзакция и ACID
РасшифровкаAtomicity, Consistency, Isolation, Durability
Концепция транзакцииДжим Грей (1981)
Термин ACIDТео Хэрдер, Андреас Ройтер (1983)
Команды SQLBEGIN, COMMIT, ROLLBACK, SAVEPOINT
Механизмыжурнал упреждающей записи (WAL), блокировки, MVCC
ПротивопоставляетсяBASE (в распределённых NoSQL-системах)

Транзакция — последовательность операций с базой данных, которая выполняется как одно неделимое целое: либо применяются все изменения, либо ни одного. ACID — аббревиатура четырёх свойств, которые традиционно гарантирует транзакция в реляционных СУБД: атомарность (Atomicity), согласованность (Consistency), изоляция (Isolation) и долговечность (Durability).

Классический пример — перевод денег: списание с одного счёта и зачисление на другой должны произойти вместе. Если сервер упадёт между двумя операциями, деньги не должны исчезнуть или появиться из ниоткуда.

История

Понятие транзакции как единицы работы с гарантиями восстановления сформулировал Джим Грей в статье 1981 года;[6] за работы в этой области он получил премию Тьюринга (1998). Аббревиатуру ACID ввели Тео Хэрдер и Андреас Ройтер в обзоре 1983 года, посвящённом восстановлению баз данных после сбоев.[1]

Пример

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- если что-то пошло не так: ROLLBACK;
COMMIT;

Пока транзакция не зафиксирована (COMMIT), другие сеансы не видят промежуточного состояния, а при ошибке или явном ROLLBACK база возвращается к исходному состоянию.[2] Точка сохранения (SAVEPOINT) позволяет откатить только часть транзакции.

В приложениях транзакции обычно открывают через драйвер или ORM:

with conn:                      # psycopg: COMMIT при успехе, ROLLBACK при исключении
    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))

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

Атомарность

Транзакция выполняется полностью или не выполняется вовсе. Если на середине возникла ошибка — нарушено ограничение, кончилось место, пропало питание, — все уже сделанные изменения отменяются. Приложению не нужно вручную «подчищать» частично записанные данные.

Согласованность

Транзакция переводит базу из одного корректного состояния в другое: после фиксации выполняются все объявленные ограничения — типы, NOT NULL, UNIQUE, CHECK, внешние ключи. Эта буква отличается от остальных: база проверяет только те правила, которые ей описаны, а бизнес-логику («баланс не может быть отрицательным») поддерживает приложение, в том числе с помощью ограничений CHECK.

Изоляция

Параллельные транзакции не должны мешать друг другу: результат должен быть таким, как если бы они выполнялись по очереди. Полная изоляция (сериализуемость) дорога, поэтому стандарт SQL определяет несколько уровней изоляции, на которых допускаются те или иные аномалии:[3]

Уровень Грязное чтение Неповторяющееся чтение Фантомы
Read Uncommitted возможно возможно возможны
Read Committed нет возможно возможны
Repeatable Read нет нет возможны
Serializable нет нет нет

Это требования-минимумы стандарта; конкретные СУБД часто дают больше. В PostgreSQL уровень по умолчанию — Read Committed, а изоляция реализована через MVCC: каждая транзакция видит снимок данных, и читатели не блокируют писателей.

Долговечность

После успешного COMMIT изменения не пропадут даже при внезапном отключении питания. Обычно это обеспечивает журнал упреждающей записи (WAL): описание изменения сначала надёжно записывается на диск в журнал (с вызовом fsync), и только потом СУБД сообщает об успехе. После сбоя база воспроизводит журнал и восстанавливает зафиксированные транзакции.[4] SQLite использует для той же цели журнал отката или WAL-режим.[5]

Типичные проблемы

  • Взаимоблокировки (deadlock): две транзакции ждут блокировки, удерживаемые друг другом. СУБД обнаруживает цикл и прерывает одну из них — приложение должно уметь повторить транзакцию.
  • Ошибки сериализации на уровне Serializable — тоже сигнал к повтору, а не к аварии.
  • Долгие транзакции удерживают блокировки и мешают очистке старых версий строк; транзакции стоит держать короткими и не ждать внутри них ответа пользователя или внешнего API.
  • Потерянное обновление: «прочитать — изменить в коде — записать» без блокировки (SELECT ... FOR UPDATE) или атомарного UPDATE ... SET x = x + 1 теряет параллельные изменения.

ACID и распределённые системы

Обеспечить ACID на одном сервере относительно просто; в распределённой базе это требует протоколов согласования (двухфазная фиксация, консенсус Raft или Paxos) и добавляет задержки. Поэтому многие системы NoSQL исторически выбирали модель BASE (Basically Available, Soft state, Eventual consistency) — доступность и итоговую согласованность вместо строгих гарантий. Компромисс между согласованностью и доступностью при разделении сети описывает теорема CAP. Современные распределённые СУБД (Google Spanner, CockroachDB, YugabyteDB) предоставляют ACID-транзакции поверх шардированных и реплицированных данных.

См. также

Источники

  1. T. Haerder, A. Reuter — Principles of Transaction-Oriented Database Recovery (ACM Computing Surveys, 1983)
  2. PostgreSQL Documentation — Transactions (tutorial)
  3. PostgreSQL Documentation — Transaction Isolation
  4. PostgreSQL Documentation — Write-Ahead Logging
  5. SQLite — Atomic Commit In SQLite
  6. Jim Gray — The Transaction Concept: Virtues and Limitations (1981)