Транзакции ACID: атомарность, изоляция и надёжность

Транзакция в базе данных - это последовательность операций, которая выполняется как единое целое. Либо все операции успешно завершаются и фиксируются командой COMMIT, либо при любой ошибке весь блок отменяется командой ROLLBACK. Чтобы гарантировать корректность данных в многопользовательских системах, транзакции должны удовлетворять четырём свойствам, объединённым аббревиатурой 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): все изменения записываются сначала в журнал, и только потом применяются к данным.
Consistency - согласованность данных
Согласованность гарантирует, что транзакция переводит базу данных из одного корректного состояния в другое. Все ограничения целостности, правила и триггеры должны соблюдаться как до, так и после транзакции.
Примеры ограничений, которые обеспечивают согласованность:
NOT NULL- поле не может быть пустымUNIQUE- значение должно быть уникальным в таблицеFOREIGN KEY- ссылочная целостность между таблицамиCHECK- пользовательское условие для значений
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount DECIMAL CHECK (amount > 0),
user_id INT REFERENCES users(id)
);
Если транзакция попытается вставить заказ с отрицательной суммой или несуществующим пользователем, СУБД откажет и выполнит ROLLBACK. Ответственность за логическую согласованность лежит на разработчике - СУБД проверяет только декларативные ограничения.
Isolation - изолированность параллельных транзакций
Изолированность - самое сложное из четырёх свойств. Она определяет, насколько одна транзакция видит изменения, вносимые другими параллельными транзакциями. Без изоляции возникают три класса аномалий:
Грязное чтение (Dirty Read) - транзакция читает данные, которые изменила другая незавершённая транзакция. Если та транзакция откатится, прочитанные данные окажутся недействительными.
Неповторяемое чтение (Non-repeatable Read) - транзакция дважды читает одну строку и получает разные значения, потому что другая транзакция успела её изменить и зафиксировать.
Фантомное чтение (Phantom Read) - транзакция дважды выполняет одинаковый запрос с условием и получает разные наборы строк из-за того, что другая транзакция вставила или удалила строки.

Стандарт SQL определяет четыре уровня изоляции, каждый из которых допускает определённые аномалии в обмен на бо́льший параллелизм:
| Уровень | Грязное чтение | Неповтор. | Фантомы |
|---|---|---|---|
READ UNCOMMITTED | возможно | возможно | возможно |
READ COMMITTED | нет | возможно | возможно |
REPEATABLE READ | нет | нет | возможно |
SERIALIZABLE | нет | нет | нет |
Установить уровень изоляции в PostgreSQL:
BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT * FROM products WHERE category = 'books'; -- другая транзакция вставляет новую книгу SELECT * FROM products WHERE category = 'books'; -- результат тот же, фантомов нет COMMIT;
Большинство СУБД по умолчанию работают на уровне READ COMMITTED. PostgreSQL дополнительно предлагает уровень REPEATABLE READ через механизм MVCC (Multi-Version Concurrency Control) - каждая транзакция видит снимок данных на момент её начала.
Durability - надёжность фиксации
Надёжность гарантирует, что после успешного выполнения COMMIT данные сохранятся даже при сбое системы - отключении питания, аварии сервера или ошибке ОС.
Механизм надёжности основан на журнале упреждающей записи (WAL):
- Изменения записываются в журнал до применения к основным файлам данных
- Журнал синхронно сбрасывается на диск перед подтверждением
COMMIT - При сбое СУБД воспроизводит журнал и восстанавливает состояние
-- PostgreSQL: настройка уровня надёжности ALTER SYSTEM SET synchronous_commit = 'on'; -- ждём подтверждения от диска ALTER SYSTEM SET fsync = 'on'; -- принудительная запись на диск
Некоторые СУБД позволяют снизить надёжность ради скорости, отключая синхронную запись (synchronous_commit = off). В таком режиме данные могут потеряться при сбое в течение последних секунд, но вероятность потери фиксированных транзакций минимальна. Для критичных данных fsync отключать нельзя.
ACID в NoSQL и распределённых системах
Классические реляционные СУБД (PostgreSQL, MySQL, Oracle) полностью поддерживают ACID. В распределённых системах и NoSQL-базах ситуация сложнее.
Теорема CAP утверждает, что распределённая система не может одновременно гарантировать согласованность (Consistency), доступность (Availability) и устойчивость к разделению сети (Partition tolerance). При сбое сети приходится выбирать между согласованностью и доступностью.
Альтернатива ACID в распределённых системах - модель BASE:
- Basically Available - система доступна, но без строгих гарантий
- Soft state - состояние может меняться со временем
- Eventual consistency - согласованность достигается в конечном счёте
Современные СУБД ищут компромисс: MongoDB добавил мультидокументные транзакции с уровнем SNAPSHOT, CockroachDB обеспечивает SERIALIZABLE через распределённый протокол.
Выбирайте уровень изоляции исходя из требований приложения: для большинства веб-приложений READ COMMITTED достаточен, для финансовых систем - SERIALIZABLE или REPEATABLE READ.
Оптимистичная и пессимистичная блокировка
Изолированность можно реализовать двумя принципиально разными способами.
Пессимистичная блокировка - транзакция захватывает блокировки при чтении данных, не давая другим транзакциям их изменить:
BEGIN; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- другие транзакции не смогут изменить эту строку UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT;
Оптимистичная блокировка - транзакция читает данные без блокировок, но при записи проверяет, не изменились ли они. Реализуется через поле версии:
-- Читаем с версией SELECT id, balance, version FROM accounts WHERE id = 1; -- version = 5, balance = 1000 -- Обновляем только если версия не изменилась UPDATE accounts SET balance = 900, version = version + 1 WHERE id = 1 AND version = 5; -- Если 0 строк обновлено - конфликт, повторяем транзакцию
Оптимистичная блокировка эффективна при низкой конкурентности - транзакции редко конфликтуют. Пессимистичная - при высокой вероятности конфликта, но создаёт риск взаимных блокировок (deadlock).
Взаимные блокировки и их обнаружение
Deadlock возникает, когда две транзакции ждут ресурсов друг друга:
- Транзакция A заблокировала строку 1 и ждёт строку 2
- Транзакция B заблокировала строку 2 и ждёт строку 1
СУБД обнаруживают deadlock через граф ожидания и автоматически прерывают одну из транзакций. Минимизировать deadlock помогают:
- Единый порядок захвата ресурсов во всех транзакциях
- Короткие транзакции с минимальным временем удержания блокировок
- Использование
NOWAITилиSKIP LOCKEDдля неблокирующих операций
Частые ошибки при работе с транзакциями
- Длинные транзакции - транзакция удерживает блокировки, снижает параллелизм и увеличивает WAL. Держите транзакции короткими.
- Транзакции с сетевыми вызовами - никогда не делайте HTTP-запросы внутри транзакции: сетевая задержка удерживает блокировки на секунды.
- Autocommit в цикле - вставка тысяч строк через
INSERTв цикле без явной транзакции создаёт отдельную транзакцию на каждую строку. Оберните в одинBEGIN/COMMIT. - Игнорирование уровня изоляции - выбор уровня по умолчанию без анализа аномалий приводит к скрытым ошибкам в финансовой логике.
- Отсутствие обработки rollback - код должен корректно реагировать на ошибки транзакций и повторять их при необходимости.
FAQ
Что такое savepoint в транзакции?
Savepoint позволяет сохранить промежуточное состояние транзакции и откатиться к нему частично, не отменяя всю транзакцию. Команда SAVEPOINT имя создаёт точку, ROLLBACK TO имя откатывает к ней, RELEASE имя удаляет. Удобно для сложных транзакций с несколькими логическими этапами.
Чем отличается MVCC от блокировок? MVCC (Multi-Version Concurrency Control) хранит несколько версий каждой строки. Читающие транзакции видят снимок данных на момент своего начала и не блокируют записывающие. PostgreSQL и Oracle используют MVCC, что даёт высокий параллелизм без блокировок при чтении. MySQL InnoDB тоже использует MVCC.
Нужен ли ACID для микросервисов? В микросервисной архитектуре ACID в классическом виде недостижим - каждый сервис управляет своей БД. Используют паттерны Saga (распределённые транзакции через события) и Two-Phase Commit (2PC). Паттерн Outbox гарантирует атомарность публикации событий вместе с записью в БД.
Коротко
ACID - это четыре гарантии надёжности транзакций: Atomicity (всё или ничего), Consistency (соблюдение ограничений), Isolation (транзакции не мешают друг другу) и Durability (зафиксированные данные не теряются). Уровни изоляции (READ UNCOMMITTED → SERIALIZABLE) регулируют компромисс между корректностью и производительностью. Правильный выбор уровня изоляции, короткие транзакции и понимание механизмов блокировки - основа надёжного SQL-приложения.
Читайте также

ACID-свойства транзакций: атомарность и изоляция
Разбираем четыре свойства ACID - атомарность, согласованность, изоляцию и долговечность. Примеры уровней изоляции, аномалий и deadlock в реляционных СУБД.

Хранимые процедуры в базе данных: зачем нужны и как писать
Хранимые процедуры в базе данных простыми словами: что это, синтаксис CREATE PROCEDURE, параметры IN OUT, отличие от функций и триггеров, плюсы и минусы, примеры на SQL.

Реляционная модель данных: основные понятия
Домен, атрибут, кортеж, схема отношения, ключ и нормальные формы - разбираем реляционную модель данных от азов до практики с примерами и калькулятором.