.NET Tutorial
Junior6 мин чтенияВопрос 2 из 17

ACID — что означает каждое свойство, как обеспечивается в SQL?

Пример: перевод денег

SQL
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;   -- списание
UPDATE accounts SET balance = balance + 500 WHERE id = 2;   -- зачисление
INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 500);
COMMIT;

Что может пойти не так без ACID:

  • Сбой после первого UPDATE — деньги списаны, но не зачислены (атомарность).
  • Баланс ушёл в минус, хотя правило запрещает (согласованность).
  • Параллельный отчёт видит сумму на счетах на 500 меньше реальной (изоляция).
  • Сервер «упал» через секунду после «успешного» ответа, и перевод пропал (долговечность).

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

Atomicity
всё или ничего

Consistency
ограничения соблюдены

Isolation
параллельные транзакции
не видят промежуточное

Durability
после COMMIT —
навсегда

WAL + откат

PK, FK, CHECK,
UNIQUE, триггеры

Блокировки / MVCC,
уровни изоляции

fsync WAL, реплики,
бэкапы

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

Atomicity
всё или ничего

Consistency
ограничения соблюдены

Isolation
параллельные транзакции
не видят промежуточное

Durability
после COMMIT —
навсегда

WAL + откат

PK, FK, CHECK,
UNIQUE, триггеры

Блокировки / MVCC,
уровни изоляции

fsync WAL, реплики,
бэкапы

Четыре гарантии транзакции

Atomicity

Каждое изменение сначала описывается в журнале предзаписи (Write-Ahead Log в PostgreSQL, transaction log в SQL Server). Если транзакция откатывается (явно ROLLBACK, ошибкой или сбоем), СУБД отменяет её изменения:

  • PostgreSQL хранит новые версии строк рядом со старыми (MVCC); у незафиксированной транзакции статус «aborted», её версии просто невидимы и позже удаляются VACUUM-ом.
  • SQL Server / Oracle используют записи undo в журнале/сегментах отката, чтобы вернуть страницы к исходному состоянию.

Поведение при ошибке внутри транзакции различается: в PostgreSQL после ошибки транзакция переходит в состояние «aborted» и ждёт ROLLBACK; в SQL Server по умолчанию откатывается только упавшая команда (SET XACT_ABORT ON меняет это на откат всей транзакции — рекомендуется).

Consistency

SQL
CREATE TABLE accounts (
    id       bigint PRIMARY KEY,
    owner_id bigint NOT NULL REFERENCES customers(id),
    balance  numeric(18,2) NOT NULL CHECK (balance >= 0),   -- баланс не может быть отрицательным
    currency char(3) NOT NULL
);

Если после списания баланс станет -100, CHECK отклонит команду, и транзакция не сможет зафиксироваться. База гарантирует декларативные ограничения; более сложные бизнес-правила («не больше 3 переводов в день») обеспечивает приложение — желательно тоже внутри транзакции.

Isolation

Полная изоляция (serializable) дорога, поэтому СУБД предлагают уровни — компромисс между корректностью и параллелизмом: Read Uncommitted, Read Committed, Repeatable Read, Serializable. Подробно — в следующем вопросе.

Механизмы:

  • Блокировки (2PL): читатели и писатели блокируют друг друга (классический SQL Server без snapshot).
  • MVCC (Multi-Version Concurrency Control): каждая транзакция видит снимок данных; читатели не блокируют писателей и наоборот (PostgreSQL, Oracle, SQL Server с READ_COMMITTED_SNAPSHOT).
Транзакция 2(перевод)Строка счёта 1Транзакция 1(отчёт)новые запросы T1 (в Read Committed)увидят v2, в Repeatable Read — всё ещё v1читает balance = 1000 (версияv1)UPDATE balance = 500 →создаётся версия v2снова читает — видит v1 =1000 (свой снимок)COMMIT
Транзакция 2(перевод)Строка счёта 1Транзакция 1(отчёт)новые запросы T1 (в Read Committed)увидят v2, в Repeatable Read — всё ещё v1читает balance = 1000 (версияv1)UPDATE balance = 500 →создаётся версия v2снова читает — видит v1 =1000 (свой снимок)COMMIT
MVCC: каждый видит свою версию строки

Durability

Файлы данныхWAL на дискеСУБДПриложениепозже, фоново (checkpoint):изменённые страницы из памяти → файлы данныхпосле сбоя: recovery проигрывает WALот последнего checkpointCOMMITзаписать запись о коммите +fsyncзаписано на дискCOMMIT OK
Файлы данныхWAL на дискеСУБДПриложениепозже, фоново (checkpoint):изменённые страницы из памяти → файлы данныхпосле сбоя: recovery проигрывает WALот последнего checkpointCOMMITзаписать запись о коммите +fsyncзаписано на дискCOMMIT OK
Что происходит при COMMIT
  • Данные в файлах таблиц обновляются лениво (страницы в памяти — shared buffers / buffer pool). Долговечность обеспечивает именно журнал: он пишется последовательно и синхронизируется с диском до ответа клиенту.
  • Настройки, ослабляющие durability ради скорости: synchronous_commit = off (PostgreSQL), DELAYED_DURABILITY (SQL Server) — можно потерять последние миллисекунды транзакций при сбое.
  • Защита от потери сервера/диска — синхронная репликация и бэкапы с архивом WAL (point-in-time recovery).

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

Как только данные распределены по нескольким сервисам или базам, одна ACID-транзакция невозможна (или слишком дорога — 2PC). Используют:

Подход Суть
BASE Basically Available, Soft state, Eventually consistent
Saga Цепочка локальных транзакций с компенсациями
Outbox Атомарная запись данных и события в одну БД + асинхронная доставка
Идемпотентность Повторная обработка сообщения не меняет результат

Вопросы на засыпку

Все ли NoSQL-базы не поддерживают ACID?

Нет. MongoDB с 4.0 поддерживает многодокументные ACID-транзакции, у многих NoSQL-баз атомарны операции над одним документом/ключом. Некоторые распределённые SQL-базы (CockroachDB, YugabyteDB, Spanner) дают ACID в кластере. Граница проходит по гарантиям конкретной системы, а не по «SQL/NoSQL».

Что такое WAL и почему последовательная запись быстрее?

Write-Ahead Log — журнал, куда изменения пишутся до изменения страниц данных. Запись в конец файла последовательная — диск (и особенно HDD) выполняет её намного быстрее случайной записи в разные страницы таблиц и индексов. Страницы данных сбрасываются потом пакетами.

Гарантирует ли ACID, что в БД не будет «грязных» бизнес-данных?

Нет. БД гарантирует свои ограничения и атомарность. Если приложение в одной транзакции записало логически неверные данные, ACID их не исправит. Поэтому инварианты стоит выражать ограничениями БД, где возможно (CHECK, UNIQUE, FK).

Что такое autocommit?

Режим, в котором каждая отдельная команда — это своя транзакция, фиксируемая автоматически. Большинство драйверов и СУБД работают так по умолчанию, пока не вызван явный BEGIN. EF Core оборачивает SaveChanges в транзакцию сам.

Итог

ACID — четыре гарантии транзакции: всё или ничего (журнал и откат), соблюдение ограничений, изоляция параллельных транзакций (блокировки и MVCC) и сохранность после коммита (WAL с fsync). Это основа надёжности реляционных баз; за пределами одной базы её заменяют Saga, Outbox и eventual consistency.