Транзакции: ACID на пальцах
Транзакции: ACID на пальцах
Транзакция - это когда несколько операций должны выполниться как единое целое. Или всё, или ничего. Это страховка для твоих данных.
Пример из жизни
Перевод денег:
- списали со счёта A (-100)
- зачислили на счёт B (+100)
Если между шагом 1 и 2 какой-то краш - 100 денежных единиц исчезают в чёрной дыре. С транзакцией: либо оба шага выполнены, либо оба откачены. Третьего не дано.
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 - нет.
Когда ты НУЖНЫ транзакции
- Денежные переводы - классика, смотри выше
- Инвентарь - отправили товар со склада, одновременно создали заказ
- Многошаговые бизнес-процессы - оплата → регистрация → отправка email (вау, если email упадёт, мы откатим всё!)
- Обновление связанных таблиц - если одна операция упадёт, остальные откатятся
Схему таблиц под такой сценарий целиком собираем в мини-проекте трека.
-- Пример: покупка товара
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;
Практические задания
- Создай две таблицы:
accounts(id, balance)иtransfers(id, from_id, to_id, amount) - Напиши транзакцию для перевода 50 денег со счёта 1 на счёт 2 (+ запись о трансфере)
- Сломай транзакцию нарочно (неправильный ID) и проверь, что оба UPDATE откатились
- Добавь
SAVEPOINTв большую транзакцию с несколькими UPDATE - Посмотри в логи: когда PostgreSQL пишет на диск (после COMMIT)
- Повтори тот же перевод из кода: транзакции в Go - в уроке про database/sql