JavaRush /Курси /SQL SELF /Рівень ізоляції SERIALIZABLE: повна ізоляці...

Рівень ізоляції SERIALIZABLE: повна ізоляція та запобігання Phantom Read

SQL SELF
Рівень 40 , Лекція 2
Відкрита

SERIALIZABLE — це максимальний рівень ізоляції транзакцій у PostgreSQL. Цей рівень гарантує, що результати роботи паралельних транзакцій будуть такими ж, якби вони виконувались ПОСЛІДОВНО, одна за одною. При цьому жодні аномалії паралельного виконання (наприклад, Dirty Read, Non-Repeatable Read, Phantom Read) не можуть виникнути.

Простими словами, SERIALIZABLE забезпечує повний порядок і узгодженість між паралельними транзакціями. Це якби PostgreSQL казав: "Всі транзакції — в чергу, друзі!"

Навіщо потрібен рівень SERIALIZABLE? Іноді хочеться бути на 100% впевненим, що твої дані залишаються повністю узгодженими, навіть якщо паралельно щось змінюється. Уяви собі сцену з супермаркету, де касири одночасно обслуговують покупців. Якби ніхто не слідкував за чергою, на виході з магазину могло б опинитися більше товарів, ніж було куплено. З SERIALIZABLE така ситуація просто неможлива.

Приклад налаштування рівня SERIALIZABLE

Щоб встановити рівень ізоляції SERIALIZABLE, треба використати команду:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Наприклад, створимо транзакцію, яка використовує цей рівень:

BEGIN; -- Починаємо транзакцію
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Встановлюємо рівень ізоляції
SELECT * FROM products WHERE category = 'Electronics'; -- Отримуємо список товарів
UPDATE products SET stock = stock - 1 WHERE product_id = 123; -- Оновлюємо залишок
COMMIT; -- Підтверджуємо зміни

Кейс: бронювання квитків у кінотеатр

Давай розглянемо реальний приклад ситуації, де рівень SERIALIZABLE просто маст-хев. Уяви, що ти розробляєш систему онлайн-бронювання квитків у кінотеатр. Твої користувачі вибирають місця, і ти хочеш гарантувати, що одне й те саме місце не буде куплене двома клієнтами одночасно.

Спочатку створимо таблицю для місць:

CREATE TABLE seats (
    seat_id SERIAL PRIMARY KEY,
    is_booked BOOLEAN DEFAULT FALSE
);

Тепер додамо кілька місць:

INSERT INTO seats (is_booked) VALUES (FALSE), (FALSE), (FALSE);

Наводимо приклад транзакції з SERIALIZABLE.

Ось як можна реалізувати безпечне бронювання місця:

BEGIN; -- Початок транзакції
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Рівень ізоляції SERIALIZABLE

-- Перевіряємо, що місце вільне
SELECT is_booked FROM seats WHERE seat_id = 1;

-- Бронюємо місце
UPDATE seats SET is_booked = TRUE WHERE seat_id = 1;

COMMIT; -- Підтверджуємо бронь

Якщо друга паралельна транзакція спробує забронювати те саме місце, PostgreSQL не допустить жодної плутанини і видасть помилку про конфлікт серіалізації.

Запобігання Phantom Read

Тепер давай розберемо "фантомні читання", від яких ми так хотіли позбутися. Phantom Read виникає, коли транзакція бачить зміни даних, доданих іншою транзакцією під час її роботи. Наприклад, твоя транзакція очікує певну кількість рядків, але раптом інша транзакція додає або видаляє рядки, змінюючи результат.

Дивимось приклад:

Дані до початку транзакцій

id balance user
1 1000 Аліса
2 500 Боб

Транзакція 1

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Підрахунок користувачів з балансом більше 400
SELECT COUNT(*) FROM accounts WHERE balance > 400;

-- Очікуємо результат: 2 (Аліса і Боб)

Транзакція 2

В іншій сесії виконується паралельна транзакція:

BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, 'Чарлі');
COMMIT;

Повертаємось до Транзакції 1

-- Повторюємо запит
SELECT COUNT(*) FROM accounts WHERE balance > 400;

Тепер, якщо не використовувати SERIALIZABLE, результат буде 3 замість 2, бо Чарлі був доданий під час роботи Транзакції 1. Це і є Phantom Read.

Але з SERIALIZABLE PostgreSQL гарантує, що Транзакція 1 не побачить Чарлі, бо її "бачення світу" заморожене на момент початку транзакції.

Особливості та обмеження рівня SERIALIZABLE

Ми розібралися, як SERIALIZABLE допомагає досягти ідеальної ізоляції. Але що в цьому світі може бути ідеальним без недоліків? Давай чесно.

Зниження продуктивності
SERIALIZABLE вимагає значно більше ресурсів, ніж рівні READ COMMITTED чи REPEATABLE READ. Чому? PostgreSQL змушений емулювати послідовне виконання операцій, відслідковуючи всі можливі конфлікти між транзакціями.

Помилки серіалізації
Якщо PostgreSQL виявляє неможливість виконати транзакції в "ідеальній послідовності", він генерує помилку серіалізації (serialization_failure) і відкочує транзакцію.

Приклад помилки:

ERROR: не вдалося серіалізувати доступ через одночасне оновлення

Щоб впоратися з такими ситуаціями, можна повторно запускати транзакцію після невдачі:

DO $$
DECLARE
    done BOOLEAN := FALSE;
BEGIN
    WHILE NOT done LOOP
        BEGIN
            -- Починаємо транзакцію
            BEGIN;
            SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

            -- Виконуємо операції
            UPDATE accounts SET balance = balance - 100 WHERE id = 1;

            -- Підтверджуємо зміни
            COMMIT;
            done := TRUE; -- Виходимо з циклу, якщо все успішно
        EXCEPTION WHEN serialization_failure THEN
            ROLLBACK; -- Відкат при помилці
        END;
    END LOOP;
END;
$$;

Це звичний підхід у системах, де використовується SERIALIZABLE.

Важливо!

Цей код написаний з використанням PL-SQL. Ми повернемось до нього пізніше. Просто хотілося дати тобі красивий і робочий код. Ну і показати, навіщо потрібен PL-SQL :)

Коли використовувати SERIALIZABLE?

Використання цього рівня ізоляції виправдане там, де ціна помилки дуже висока:

  • Фінансові транзакції, такі як обробка платежів чи розподіл бонусів.
  • Системи управління запасами, щоб уникнути дублювання замовлень.
  • Онлайн-бронювання, де важливо виключити конфлікт при бронюванні ресурсів.

Якщо ти розробляєш систему, де дані мають бути на 100% узгодженими, а продуктивність відходить на другий план, SERIALIZABLE стане твоїм найкращим другом.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ