Транзакции и ACID
Транзакция — это группа запросов, которая выполняется как единое целое: либо все изменения применяются, либо ни одного.
Транзакция — логическая единица работы с базой данных, которая переводит её из одного согласованного состояния в другое. Между
BEGINиCOMMITвсе запросы образуют один неделимый блок.
Представьте перевод денег: со счёта Алисы списали 100 рублей, а зачислить Бобу не успели — упало соединение, кончилось место на диске, сервер перезагрузился. Деньги исчезли. Без транзакций любой сбой между двумя UPDATE оставляет данные в «полусогласованном» состоянии, которое потом мучительно чинят руками. Транзакции дают гарантию: либо обе операции случились, либо ни одной.
Этот раздел — про то, как СУБД защищает данные при сбоях и при одновременной работе многих пользователей. Начнём с фундамента: что такое транзакция, какими командами ею управляют и какие гарантии стоят за аббревиатурой ACID.
Зачем это на практике
Любая операция, которая меняет несколько строк или таблиц и должна остаться целостной, — кандидат на транзакцию. Типичные сценарии:
- Финансы: перевод, оплата заказа, начисление бонусов — деньги не должны «теряться» или «удваиваться».
- Заказ в магазине: создать заказ, списать товар со склада, уменьшить остаток — всё вместе или никак.
- Регистрация: создать пользователя, профиль и стартовые настройки тремя
INSERT— чтобы не осталось пользователя без профиля.
BEGIN, COMMIT, ROLLBACK
Управление транзакцией — это три команды. BEGIN (или START TRANSACTION) открывает блок, COMMIT подтверждает и сохраняет всё разом, ROLLBACK отменяет все изменения с момента BEGIN, будто их не было.
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
owner TEXT NOT NULL,
balance INTEGER NOT NULL
);
INSERT INTO accounts (id, owner, balance) VALUES (1, 'Алиса', 500), (2, 'Боб', 100);
-- перевод 100 рублей от Алисы к Бобу как единое целое
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Алиса';
UPDATE accounts SET balance = balance + 100 WHERE owner = 'Боб';
COMMIT;
SELECT owner, balance FROM accounts ORDER BY id;
Вывод:
Алиса|400 Боб|200
Если бы между двумя UPDATE что-то сломалось, незакоммиченные изменения откатились бы автоматически — баланс Алисы остался бы 500. А вот пример осознанной отмены: проверили условие и решили не продолжать.
CREATE TABLE accounts (id INTEGER PRIMARY KEY, owner TEXT, balance INTEGER);
INSERT INTO accounts VALUES (1, 'Алиса', 50);
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Алиса';
-- баланс ушёл в минус -50, передумали и откатываем
ROLLBACK;
SELECT owner, balance FROM accounts;
Вывод:
Алиса|50
После ROLLBACK баланс снова 50 — изменение как будто не происходило.
Автокоммит: почему обычный UPDATE и так «сохраняется»
Когда вы пишете одиночный INSERT без BEGIN, он всё равно выполняется в транзакции — просто СУБД сама оборачивает каждую команду в BEGIN ... COMMIT. Это режим автокоммита. Поэтому отдельный запрос либо отрабатывает целиком, либо никак. Явный BEGIN нужен, когда в один атомарный блок надо собрать несколько команд.
SAVEPOINT: точки сохранения внутри транзакции
SAVEPOINT — это «закладка» внутри открытой транзакции. К ней можно откатиться через ROLLBACK TO, не отменяя всю транзакцию целиком. Удобно, когда часть работы рискованная, а остальное надо сохранить.
CREATE TABLE accounts (id INTEGER PRIMARY KEY, owner TEXT, balance INTEGER);
INSERT INTO accounts VALUES (1, 'Алиса', 500), (2, 'Боб', 100);
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Алиса';
SAVEPOINT after_debit;
UPDATE accounts SET balance = balance + 999 WHERE owner = 'Боб'; -- ошиблись в сумме
ROLLBACK TO after_debit; -- откат только этого шага
UPDATE accounts SET balance = balance + 100 WHERE owner = 'Боб';-- правильная сумма
COMMIT;
SELECT owner, balance FROM accounts ORDER BY id;
Вывод:
Алиса|400 Боб|200
Списание у Алисы сохранилось, ошибочное зачисление откатилось до закладки, а финальный COMMIT зафиксировал корректный итог.
ACID простыми словами
Гарантии транзакций описывают четырьмя свойствами — ACID. Это не магия, а контракт, который СУБД старается соблюдать.
| Свойство | Смысл одним предложением |
| A — Atomicity (атомарность) | «всё или ничего»: либо применяются все изменения транзакции, либо ни одного. |
| C — Consistency (согласованность) | транзакция переводит базу из одного валидного состояния в другое, не нарушая ограничений (CHECK, внешние ключи, уникальность). |
| I — Isolation (изоляция) | параллельные транзакции не видят «полуготовых» изменений друг друга — будто выполняются по очереди. |
| D — Durability (долговечность) | после COMMIT данные переживут падение сервера — они уже на диске. |
В примере с переводом: атомарность не даст списать без зачисления; согласованность запретит уйти в минус, если есть CHECK (balance >= 0); изоляция скроет промежуточное состояние от других; долговечность сохранит результат после COMMIT, даже если через секунду выключат питание.
Как это работает под капотом
Главный механизм долговечности и атомарности — журнал упреждающей записи (write-ahead log, WAL). Прежде чем изменить страницу данных, СУБД записывает в журнал на диск намерение: «строку X меняем так-то». Только после того как запись в журнале гарантированно лежит на диске, изменение считается зафиксированным.
Отсюда обе гарантии. Atomicity: если транзакция оборвалась, при восстановлении СУБД по журналу откатывает её незавершённые изменения. Durability: если сервер упал сразу после COMMIT, при старте система «доигрывает» из журнала подтверждённые, но ещё не записанные в основные файлы изменения. Поэтому COMMIT иногда заметно медленнее обычного запроса — он ждёт физической записи журнала (fsync) на диск.
Частые ошибки
- Забыть COMMIT. Открыли
BEGIN, выполнили запросы и ушли — транзакция висит, держит блокировки, тормозит остальных. Незакрытая транзакция при разрыве соединения откатывается, и работа теряется. - Длинные транзакции. Чем дольше открыта транзакция, тем дольше держатся блокировки и копятся старые версии строк. Делайте транзакции короткими: только запросы, между ними — никаких сетевых вызовов и ожиданий ввода пользователя.
- Бизнес-логика между запросами. Не держите транзакцию открытой, пока ходите в платёжный шлюз или ждёте ответа API: внешний сервис может зависнуть на минуты, а ваша транзакция всё это время блокирует строки.
- Расчёт на «само сохранится». В клиентах с выключенным автокоммитом одиночный
INSERTне попадёт в базу без явногоCOMMIT— частый сюрприз при работе через драйвер.
Итоги
- Транзакция — неделимая группа запросов:
BEGINоткрывает,COMMITфиксирует,ROLLBACKотменяет всё с моментаBEGIN. SAVEPOINT+ROLLBACK TOпозволяют откатить часть транзакции, сохранив остальное.- ACID: атомарность (всё или ничего), согласованность (без нарушения ограничений), изоляция (параллельные не мешают), долговечность (переживает сбой).
- За долговечность и атомарность отвечает журнал упреждающей записи (WAL); поэтому
COMMITждёт записи на диск. - Держите транзакции короткими — это меньше блокировок и меньше конфликтов.