<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
SQL · JUNIOR ЧАСТО ПИТАЮТЬ

Що таке транзакція, що гарантує ACID і що з цього не гарантується за умовчанням?

Транзакція - це група команд, які застосовуються цілком або не застосовуються зовсім; ACID гарантує атомарність, дотримання оголошених обмежень, ізоляцію на заданому рівні й збереження закомічених даних після падіння, але за умовчанням ізоляція не серіалізовна (PostgreSQL - READ COMMITTED, MySQL - REPEATABLE READ), а ROLLBACK не скасовує нічого поза базою.

Посеред транзакції впав PHP. Що станеться з уже виконаними INSERT?
Ми надіслали лист усередині транзакції, а потім був ROLLBACK. Чому клієнт отримав лист про замовлення, якого немає в базі?
Розшифруйте ACID і скажіть, яка літера на практиці найслабша.
Який рівень ізоляції у вашої бази за умовчанням і що це означає для коду?
транзакції ACID PostgreSQL MySQL

Транзакція задає межу, усередині якої база бачить вашу послідовність команд як одну операцію. Між BEGIN і COMMIT інші сесії не бачать нічого з написаного, а після ROLLBACK не побачать і поготів. Без явного BEGIN кожна команда сама собі транзакція: autocommit увімкнений і в PostgreSQL, і в MySQL, тому одиночний UPDATE атомарний, а три пов'язані UPDATE підряд уже ні. Механіка відкату різна: InnoDB програє назад undo log, PostgreSQL просто позначає транзакцію перерваною, а зайві версії рядків згодом прибирає autovacuum.

ACID розкладається на чотири різні за вагою обіцянки. Atomicity дає «все або нічого» і саме вона рятує, коли посеред створення замовлення вилітає виняток. Consistency означає, що після COMMIT не порушене жодне оголошене обмеження: NOT NULL, UNIQUE, FOREIGN KEY, CHECK. База перевіряє тільки те, що ви описали в схемі; правило «сума позицій дорівнює сумі замовлення» вона не вигадає. Isolation задає, наскільки паралельні транзакції бачать роботу одна одної, і тут ховається головний сюрприз: дефолт у PostgreSQL READ COMMITTED, в InnoDB REPEATABLE READ, і жоден з них не еквівалентний послідовному виконанню. Durability обіцяє, що закомічене переживе падіння процесу й сервера, бо запис у WAL скидається на диск до відповіді клієнту; рівень цієї гарантії регулюють innodb_flush_log_at_trx_commit разом із sync_binlog у MySQL та synchronous_commit у PostgreSQL, і зниження заради швидкості коштує останніх секунд транзакцій.

Найчастіша практична помилка зовсім не про літери абревіатури. Транзакція керує лише вмістом бази. Лист, який Mailgun уже прийняв, платіж, який шлюз уже провів, задача, яку воркер уже витягнув з Redis, файл, записаний на диск, скинутий кеш - усього цього ROLLBACK не повертає. Так з'являється класичний баг: клієнт отримав підтвердження замовлення, а замовлення в базі немає. Ліки два. Побічні ефекти виносять за COMMIT (DB::afterCommit, dispatch()->afterCommit(), параметр after_commit у конфігу черги) або пишуть подію в таблицю outbox тією ж транзакцією, а надсилає її окремий процес. Оскільки повтор при такій схемі неминучий, кожна зовнішня дія має ключ ідемпотентності, який приймач звіряє перед виконанням.

Друга біда - довжина транзакції. Поки вона відкрита, змінені рядки заблоковані, у PostgreSQL autovacuum не може прибрати старі версії по всій базі, а в InnoDB росте undo-історія. Тримати транзакцію на час запиту до платіжного API означає тримати блокування три секунди на кожен виклик, і в пікові хвилини це перетворюється на чергу з lock wait timeout. Тому транзакція починається перед першим записом і закінчується одразу після останнього, а мережеві виклики й важкі обчислення лишаються ззовні. Корисно одразу поставити idle_in_transaction_session_timeout у PostgreSQL: сесія, яка забула закомітити, буде вбита, а не проживе до перезапуску воркера.

І ще одна річ, про яку на junior-рівні забувають: відкат повертає рядки, але не лічильники. Значення AUTO_INCREMENT і sequence після ROLLBACK не перевикористовуються, тож діри в нумерації - нормальна поведінка, а не пошкоджені дані. А якщо таблиця раптом на MyISAM, ніякої транзакції там немає взагалі: рушій мовчки виконає команди й проігнорує ROLLBACK.

-- Одна бізнес-дія = одна транзакція: або є замовлення, позиції й резерв, або немає нічого
BEGIN;

-- request_id приходить із запиту, UNIQUE(request_id) не дає створити дубль при ретраї
INSERT INTO orders (user_id, request_id, status)
VALUES (42, '8f3c1d', 'new')
ON CONFLICT (request_id) DO NOTHING
RETURNING id;          -- порожньо = замовлення вже створила попередня спроба

INSERT INTO order_items (order_id, sku, qty) VALUES (1001, 'kbd-87', 2);

-- Перевірка залишку й резерв однією командою, без читання в PHP
UPDATE stock SET reserved = reserved + 2
WHERE sku = 'kbd-87' AND quantity - reserved >= 2;
-- 0 оновлених рядків -> застосунок робить ROLLBACK, і обидва INSERT зникають разом із ним

COMMIT;                -- тільки тепер зміни видно іншим сесіям і вони переживуть падіння сервера

-- Антипатерн: зовнішній виклик усередині транзакції
BEGIN;
UPDATE orders SET status = 'paid' WHERE id = 1001;
--  HTTP до платіжного шлюзу на три секунди: рядок заблокований увесь цей час,
--  а якщо шлюз списав гроші й після цього впав PHP, ROLLBACK прибере лише запис у базі
COMMIT;

-- PostgreSQL: DDL відкочується нарівні з даними
BEGIN; ALTER TABLE orders ADD COLUMN note text; ROLLBACK;   -- колонки не буде
-- MySQL: цей самий ALTER робить неявний COMMIT, відкотити його вже неможливо
Що атомарність працює в обидва боки: або всі записи транзакції видно іншим після COMMIT, або їх немає зовсім після ROLLBACK, і проміжного стану ззовні не існує.
Дефолтні рівні ізоляції: READ COMMITTED у PostgreSQL, REPEATABLE READ в InnoDB, і розуміння, що жоден з них не робить паралельні транзакції послідовними.
Що літера C означає дотримання обмежень, які ви самі оголосили (NOT NULL, UNIQUE, FOREIGN KEY, CHECK), а не автоматичну правильність бізнес-логіки.
Що D залежить від налаштувань: innodb_flush_log_at_trx_commit і sync_binlog у MySQL, synchronous_commit у PostgreSQL; знижений рівень дає швидкість ціною втрати останніх транзакцій при аварії.
Що листи, HTTP-виклики, записані файли, черги й інвалідований кеш транзакція не відкочує, тому такі дії виносять після COMMIT і роблять ідемпотентними.
Вважати, що транзакція сама по собі захищає від гонок: два паралельні процеси на дефолтному рівні спокійно прочитають те саме значення й затруть записи один одного.
Відправляти лист, платіж або подію в чергу всередині транзакції, а потім дивуватися розбіжності між базою й зовнішнім світом.
Ловити виняток, логувати його й забувати про ROLLBACK: з'єднання лишається з відкритою транзакцією і тримає блокування.
Відкривати транзакцію на початку HTTP-запиту й закривати в кінці, загортаючи в неї валідацію, рендер і походи в API.
Розраховувати на ROLLBACK для послідовностей: після відкату значення sequence у PostgreSQL і AUTO_INCREMENT у MySQL назад не повертаються, у нумерації будуть діри.
Писати в таблиці MyISAM і чекати на відкат: транзакції підтримує InnoDB, MyISAM їх мовчки ігнорує.
ПОРАДА

Скажіть одним реченням, де проходить межа: усе, що база записала, вона вміє відкотити; усе, що покинуло базу, - ні. Далі наведіть приклад з листом після ROLLBACK, і співбесідник почує досвід, а не вивчену абревіатуру.

оновлено 30 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

База відкочує тільки власні зміни, а дефолтні рівні ізоляції залишають простір для гонок: ні READ COMMITTED, ні REPEATABLE READ не роблять транзакції послідовними.