EssayAI
Блог
Блог
Математика и алгоритмы

ACID-свойства транзакций: атомарность и изоляция

17 июня 2026Время чтения: 7 минут
#базы данных#транзакции#ACID#SQL#изоляция
ACID-свойства транзакций: атомарность и изоляция

Каждый раз, когда банк переводит деньги или интернет-магазин резервирует товар, база данных выполняет транзакцию. Транзакция - это последовательность операций, которая должна выполниться целиком или не выполниться вовсе. Именно четыре свойства ACID гарантируют, что данные останутся корректными при сбоях системы и параллельной работе тысяч пользователей.

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

Атомарность означает, что транзакция неделима: либо фиксируются все её изменения, либо не фиксируется ни одно. Классический пример - банковский перевод:

BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;  -- снять
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;  -- зачислить
COMMIT;

Если сервер упадёт между двумя UPDATE, СУБД при восстановлении откатит незавершённую транзакцию через журнал WAL (Write-Ahead Log). Деньги не исчезнут и не задвоятся.

Атомарность реализуется через механизм журналирования. Перед записью данных СУБД фиксирует намерение в WAL на диске. При восстановлении сервер анализирует журнал: транзакции с COMMIT воспроизводятся, остальные откатываются. В PostgreSQL этим управляет процесс startup, в MySQL - механизм redo/undo log InnoDB.

Жизненный цикл транзакции: от BEGIN до COMMIT или ROLLBACK - атомарность гарантирует, что прерванная цепочка операций не оставляет данные в промежуточном состоянии

Consistency: данные всегда корректны

Согласованность гарантирует, что транзакция переводит базу из одного корректного состояния в другое, не нарушая ограничений целостности. Если таблица orders требует ссылки на существующего пользователя (FOREIGN KEY), то попытка вставить заказ с несуществующим user_id будет отклонена - даже если операция технически выполнима.

сумма до=сумма после\text{сумма до} = \text{сумма после}

Пример: если сумма балансов по всем счетам перед переводом равна SS, то после фиксации перевода она должна оставаться равной SS. СУБД проверяет ограничения CHECK, NOT NULL, UNIQUE, FOREIGN KEY при каждой операции. Нарушение любого из них автоматически откатывает транзакцию.

Важно понимать разницу: СУБД отвечает за «техническую» согласованность (ограничения схемы), а «бизнес-логическую» согласованность обеспечивает приложение. Например, правило «баланс не может быть отрицательным» нужно явно добавить через CHECK (balance >= 0).

Isolation: транзакции не мешают друг другу

Изоляция - самое сложное из четырёх свойств. В идеале параллельные транзакции не должны видеть промежуточных результатов друг друга. На практике полная изоляция снижает производительность, поэтому стандарт SQL определяет четыре уровня с разным балансом безопасности и скорости.

Сравнение транзакций с изоляцией и без: слева грязное чтение, справа - защита REPEATABLE READ
Сравнение транзакций с изоляцией и без: слева грязное чтение, справа - защита REPEATABLE READ

Три типа аномалий, от которых защищают уровни изоляции:

  • Грязное чтение (dirty read): T2 читает незафиксированные данные T1. Если T1 откатится, T2 получила некорректные данные.
  • Неповторяемое чтение (non-repeatable read): T2 дважды читает одну строку, между чтениями T1 её изменяет и фиксирует. Два чтения дают разные результаты.
  • Фантомное чтение (phantom read): T2 дважды выполняет запрос с условием WHERE, между запросами T1 вставляет новые строки, удовлетворяющие условию. Второй запрос возвращает другое количество строк.

Стандартные уровни изоляции:

УровеньГрязное чтениеНеповторяемое чтениеФантомное чтение
READ UNCOMMITTEDестьестьесть
READ COMMITTEDнетестьесть
REPEATABLE READнетнетесть
SERIALIZABLEнетнетнет

PostgreSQL и Oracle по умолчанию используют READ COMMITTED. MySQL InnoDB - REPEATABLE READ. Для финансовых расчётов, где важен точный снимок данных на момент начала транзакции, рекомендуется REPEATABLE READ или SERIALIZABLE.

Современные СУБД реализуют изоляцию через MVCC (Multi-Version Concurrency Control): каждая строка хранит несколько версий, и транзакция читает «снимок» данных на момент своего начала. Это позволяет читателям не блокировать писателей - ключевое преимущество перед классическими блокировками.

Durability: данные сохранены навсегда

Долговечность гарантирует: после получения подтверждения COMMIT изменения не будут потеряны, даже если сервер тут же выключится. Это достигается через запись в журнал (WAL) на диске до подтверждения транзакции.

COMMITданные на диске\text{COMMIT} \Rightarrow \text{данные на диске}

В PostgreSQL параметр synchronous_commit управляет строгостью: при on (по умолчанию) СУБД ждёт сброса WAL на диск перед ответом клиенту. При off подтверждение приходит быстрее, но существует окно потери данных при сбое (обычно несколько миллисекунд).

В распределённых системах долговечность достигается репликацией: транзакция считается зафиксированной только после подтверждения от заданного числа реплик. В PostgreSQL это настраивается через synchronous_standby_names.

Взаимодействие свойств на практике

ACID-свойства не независимы - они работают вместе. Атомарность и долговечность обеспечиваются одним механизмом (WAL). Согласованность опирается на атомарность. Изоляция влияет на то, насколько видна согласованность другим транзакциям в процессе.

Рассмотрим бронирование мест в самолёте:

BEGIN;
-- Проверяем наличие мест
SELECT count(*) FROM seats WHERE flight_id = 42 AND status = 'free';
-- Если мест достаточно - резервируем
UPDATE seats SET status = 'booked', user_id = 101
WHERE flight_id = 42 AND status = 'free' LIMIT 2;
COMMIT;

При уровне READ COMMITTED два одновременных пользователя могут оба увидеть 2 свободных места и оба попытаться забронировать - что приведёт к overstating. Решение: поднять уровень до SERIALIZABLE или использовать SELECT ... FOR UPDATE (пессимистичная блокировка).

Deadlock и управление блокировками

При пессимистичных блокировках возможна взаимная блокировка (deadlock): T1 держит ресурс A и ждёт B, T2 держит B и ждёт A. Обе транзакции ждут вечно.

СУБД автоматически обнаруживают дедлоки через граф ожидания (wait-for graph) и прерывают одну из транзакций с ошибкой. Приложение должно уметь повторить транзакцию при получении ошибки дедлока (ERROR 40P01 в PostgreSQL).

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

ACID vs BASE в NoSQL

Распределённые NoSQL-системы часто жертвуют частью ACID-гарантий ради горизонтального масштабирования. Теорема CAP (Consistency, Availability, Partition tolerance) утверждает, что при сетевом разделении система может обеспечить либо согласованность, либо доступность, но не обе сразу.

Подход BASE (Basically Available, Soft state, Eventual consistency): данные в конечном счёте согласуются, но в конкретный момент реплики могут расходиться. MongoDB, Cassandra, DynamoDB работают в этой модели. PostgreSQL с логической репликацией при определённых настройках тоже может вести себя как BASE.

Выбор между ACID и BASE определяется бизнес-требованиями: банк не может допустить «в конечном счёте правильный» баланс, а лента новостей спокойно работает с небольшим lag между репликами.

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

  • Не проверять код ошибки после COMMIT: сетевой разрыв может скрыть реальный исход транзакции - при сомнениях проверяй через идемпотентный запрос.
  • Длинные транзакции при высокой конкурентности: удерживаемые блокировки блокируют других пользователей; держи транзакции как можно короче.
  • Путать уровни изоляции и уровни блокировок: уровень изоляции - декларация о видимости данных, конкретная реализация (блокировки vs MVCC) зависит от СУБД.
  • Использовать READ UNCOMMITTED в продакшне: грязное чтение практически никогда не оправдано; минимальный безопасный уровень - READ COMMITTED.
  • Игнорировать дедлоки в коде приложения: СУБД прерывает транзакцию, но не повторяет её; логика повтора должна быть в приложении.

FAQ

Чем транзакция отличается от простого запроса? Запрос - атомарная операция сама по себе, но не может объединить несколько операций в единое целое. Транзакция группирует несколько операций с гарантией ACID. Любой одиночный INSERT/UPDATE/DELETE в SQL неявно оборачивается в транзакцию с автоматическим COMMIT (autocommit).

Когда SERIALIZABLE замедляет систему больше всего? При высокой конкурентности на одни и те же строки: СУБД вынуждена либо ставить в очередь транзакции, либо часто откатывать конфликтующие. В PostgreSQL SERIALIZABLE использует SSI (Serializable Snapshot Isolation) - более оптимистичный подход, чем классические S-блокировки, но всё равно добавляет накладные расходы на отслеживание зависимостей.

Можно ли реализовать ACID без журналирования WAL? Теоретически - через синхронную запись данных напрямую при каждом изменении. На практике WAL быстрее: журнал пишется последовательно (быстро для диска), а сами страницы данных обновляются асинхронно. Без WAL каждый COMMIT требовал бы случайных записей на диск для всех изменённых страниц.

Коротко

ACID - четыре взаимосвязанных свойства транзакций в реляционных СУБД: атомарность (всё или ничего через WAL), согласованность (ограничения схемы соблюдаются), изоляция (четыре уровня с разным балансом скорости и защиты от аномалий) и долговечность (COMMIT означает запись на диск). В большинстве задач достаточен READ COMMITTED; для конкурентного доступа к одним данным нужен REPEATABLE READ или SERIALIZABLE с обработкой дедлоков на стороне приложения.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

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