Транзакции: ACID на пальцах

Транзакции: ACID на пальцах

Транзакция - это когда несколько операций должны выполниться как единое целое. Или всё, или ничего. Это страховка для твоих данных.

Пример из жизни

Перевод денег:

  1. списали со счёта A (-100)
  2. зачислили на счёт B (+100)

Если между шагом 1 и 2 какой-то краш - 100 денежных единиц исчезают в чёрной дыре. С транзакцией: либо оба шага выполнены, либо оба откачены. Третьего не дано.

BEGIN / COMMIT / ROLLBACK

Жизненный цикл транзакции: BEGIN, операции, развилка COMMIT или ROLLBACK

Это основная триада:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
  • BEGIN - начало транзакции
  • COMMIT - фиксируем все изменения в базе
  • ROLLBACK - отменяем всё

Если где-то произойдёт ошибка:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Ошибка! Счёт 2 не существует
UPDATE accounts SET balance = balance + 100 WHERE id = 999;
-- База автоматически вернёт первый UPDATE
ROLLBACK;

После ROLLBACK счёт 1 вернёт свой баланс, как будто ничего не происходило.

Autocommit: неявные транзакции

PostgreSQL (и большинство СУБД) работает в режиме autocommit по умолчанию. Это значит:

UPDATE accounts SET balance = 100 WHERE id = 1; - автоматически COMMIT после этой строки

Каждый одиночный statement оборачивается в неявную транзакцию. Это удобно для простых операций, но опасно для сложных сценариев.

Если ты хочешь явный контроль - всегда начинай с BEGIN:

BEGIN;
-- твой код здесь
COMMIT;

SAVEPOINT: частичные откаты

Иногда нужно откатить только часть операций внутри большой транзакции. Для этого есть SAVEPOINT:

BEGIN;
INSERT INTO users (email) VALUES ('alice@example.com'); - успех
SAVEPOINT sp1;
INSERT INTO users (email) VALUES ('bob@example.com'); - успех
INSERT INTO users (email) VALUES ('alice@example.com'); - ОШИБКА (дубликат email, спасибо [UNIQUE](./09-constraints.md))
ROLLBACK TO sp1; - откатываем только до sp1, первый INSERT остаётся
COMMIT; - коммитим только alice

В этом примере alice@example.com окажется в базе, а bob - нет.

Когда ты НУЖНЫ транзакции

  1. Денежные переводы - классика, смотри выше
  2. Инвентарь - отправили товар со склада, одновременно создали заказ
  3. Многошаговые бизнес-процессы - оплата → регистрация → отправка email (вау, если email упадёт, мы откатим всё!)
  4. Обновление связанных таблиц - если одна операция упадёт, остальные откатятся

Схему таблиц под такой сценарий целиком собираем в мини-проекте трека.

-- Пример: покупка товара
BEGIN;
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 42;
INSERT INTO orders (user_id, product_id, status) VALUES (10, 42, 'pending');
INSERT INTO payments (order_id, amount) VALUES (currval('orders_id_seq'), 99.99);
COMMIT; - либо всё, либо ничего

Какой откат произойдёт, если забыть COMMIT

Если ты открыл транзакцию (BEGIN) и закрыл соединение без COMMIT:

BEGIN;
UPDATE accounts SET balance = 999999 WHERE id = 1;
-- закрыл окно / соединение упало / приложение краснулось

Результат: автоматический ROLLBACK. Все твои изменения исчезнут. Это защита от незаконченных операций.

Проблема: долгие транзакции

Это частая ошибка:

BEGIN;
-- читаем все данные в приложении
SELECT * FROM huge_table; - 1 миллион строк
-- обрабатываем 5 минут в коде приложения
-- пишем результат
UPDATE accounts SET balance = ... WHERE id = 1;
COMMIT;

Проблема: 5 минут база держит блокировки на строках, другие соединения ждут. Кто кого и насколько жёстко блокирует, зависит от уровня изоляции. Performance падает. Правило: открывай транзакцию как можно позже, закрывай как можно раньше.

-- Правильно:
-- 1. Читаем данные БЕЗ BEGIN
SELECT * FROM huge_table;
-- 2. Обрабатываем в коде
-- 3. ПОТОМ открываем транзакцию
BEGIN;
UPDATE accounts SET balance = ... WHERE id = 1;
COMMIT;
- **A**tomicity (атомарность): всё или ничего - если что-то упадёт, откатимся полностью - **C**onsistency (согласованность): данные не ломаются, правила целостности соблюдаются - **I**solation (изоляция): параллельные транзакции не мешают друг другу - насколько именно, задаёт [уровень изоляции](./08-isolation.md) - **D**urability (надёжность): после `COMMIT` данные точно на диске, даже если БД упадёт

Практические задания

  • Создай две таблицы: accounts(id, balance) и transfers(id, from_id, to_id, amount)
  • Напиши транзакцию для перевода 50 денег со счёта 1 на счёт 2 (+ запись о трансфере)
  • Сломай транзакцию нарочно (неправильный ID) и проверь, что оба UPDATE откатились
  • Добавь SAVEPOINT в большую транзакцию с несколькими UPDATE
  • Посмотри в логи: когда PostgreSQL пишет на диск (после COMMIT)
  • Повтори тот же перевод из кода: транзакции в Go - в уроке про database/sql

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.