Давай ще раз пройдемось по типах транзакцій! У світі транзакцій у базах даних є три головних "злодія", які можуть зіпсувати тобі настрій: Dirty Read, Non-Repeatable Read і Phantom Read. Ці "аномалії" з'являються через недостатній рівень ізоляції транзакцій. Сьогодні розберемо, хто ці злодії, як вони проявляються і — що найважливіше — як з ними боротися.
Перш ніж перейти до прикладів, давай згадаємо, що вони означають.
Dirty Read (Брудне читання):
Ти читаєш дані, які вже змінені, але транзакція, що їх змінила, ще не зафіксована (COMMIT) або, що ще гірше, може бути відкотана (ROLLBACK). Це якби ти перекинув гроші другу, подивився на свій баланс і побачив, що ти вже бідний, але потім передумав і повернув гроші собі. Магія!Non-Repeatable Read (Неповторюване читання):
Ти читаєш одні й ті ж дані двічі в межах однієї транзакції, але між цими читаннями інша транзакція змінила ці дані, і ти бачиш два різних результати. Це якби ти подивився дату народження у паспорті, потім дав його другу, який підправив цифри, і ти знову подивився — а дата вже інша.Phantom Read (Фантомне читання):
Ти робиш один і той самий запит двічі, але вдруге бачиш додаткові рядки, які додала інша транзакція. Це якби ти рахував людей у кімнаті, а хтось тихенько привів ще друзів.
Проблема Dirty Read
Припустимо, у нас є таблиця accounts:
CREATE TABLE accounts (
account_id SERIAL PRIMARY KEY,
owner TEXT NOT NULL,
balance NUMERIC(10, 2) NOT NULL
);
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000), ('Bob', 500);
Транзакція 1 змінює баланс, але ще не завершилась, а Транзакція 2 намагається одночасно прочитати ці дані.
Транзакція 1:
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE owner = 'Alice';
-- Баланс Alice став 800, але транзакція ще не завершена.
Транзакція 2:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Alice'; -- Бачимо баланс: 800 (брудне читання).
ROLLBACK; -- Транзакція 1 відкочується.
Тепер Транзакція 2 працює з некоректними даними, бо Транзакція 1 відмінила зміни. Цього можна уникнути, якщо використовувати рівень ізоляції READ COMMITTED, який не дозволяє бачити зміни незафіксованих транзакцій.
Проблема Non-Repeatable Read
Уяви, що Транзакція 1 читає дані, інша транзакція їх змінює, і Транзакція 1 знову читає дані. Дані вже інші.
Транзакція 1:
BEGIN;
SELECT balance FROM accounts WHERE owner = 'Bob'; -- Бачимо баланс: 500.
Транзакція 2 паралельно:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Bob';
COMMIT;
Транзакція 1 знову:
SELECT balance FROM accounts WHERE owner = 'Bob'; -- Бачимо баланс: 400.
COMMIT;
Зверни увагу, як дані в межах однієї транзакції змінилися. У реальних сценаріях це може бути критично, наприклад, для фінансових звітів. Вирішується ця проблема використанням більш високого рівня ізоляції, наприклад, REPEATABLE READ.
Проблема Phantom Read
Припустимо, у нас є таблиця orders:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer TEXT NOT NULL,
total NUMERIC(10, 2) NOT NULL
);
INSERT INTO orders (customer, total) VALUES ('Alice', 100), ('Bob', 200);
Транзакція 1 рахує кількість замовлень, інша транзакція додає нове замовлення, і Транзакція 1 рахує знову.
Транзакція 1:
BEGIN;
SELECT COUNT(*) FROM orders; -- Бачимо: 2.
Транзакція 2 паралельно:
BEGIN;
INSERT INTO orders (customer, total) VALUES ('Charlie', 300);
COMMIT;
Транзакція 1 знову:
SELECT COUNT(*) FROM orders; -- Бачимо: 3. Нове "фантомне" замовлення з'явилось!
COMMIT;
Щоб уникнути фантомних читань, потрібен рівень ізоляції SERIALIZABLE, який повністю блокує паралельні зміни, що впливають на результат.
Способи уникнення аномалій
Рівні ізоляції vs. Аномалії
| Рівень ізоляції | Dirty Read |
Non-Repeatable Read |
Phantom Read |
|---|---|---|---|
Read Uncommitted |
❌ Так | ❌ Так | ❌ Так |
Read Committed |
✅ Ні | ❌ Так | ❌ Так |
Repeatable Read |
✅ Ні | ✅ Ні | ❌ Так |
Serializable |
✅ Ні | ✅ Ні | ✅ Ні |
Як обрати правильний рівень ізоляції?
- Якщо потрібен швидкий доступ до даних і аномалії не критичні (наприклад, аналітика на застарілих даних) — використовуй
READ COMMITTED. - Якщо важлива стабільність даних у межах однієї транзакції — використовуй
REPEATABLE READ. - Якщо потрібна максимальна ізоляція і узгодженість — обирай
SERIALIZABLE. Пам'ятай: продуктивність може просісти.
Практичні поради
Використовуй транзакції з рівнями ізоляції, які підходять саме для твого кейсу. Наприклад:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT ...;
COMMIT;
Додавай індекси, щоб мінімізувати блокування таблиць і прискорити виконання запитів.
Використовуй оптимізовані запити, щоб уникати довгих блокувань, особливо на рівні SERIALIZABLE.
Специфічні помилки і як їх уникнути
Іноді неправильно обраний рівень ізоляції викликає конфлікти і зниження продуктивності. Наприклад, використання SERIALIZABLE для системи, де виконується багато паралельних транзакцій, може призвести до блокувань і "голодування" транзакцій.
Щоб цього уникнути, аналізуй свої запити, тестуй продуктивність на різних рівнях ізоляції і використовуй правильні індекси.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ