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 Alice
2 500 Bob

Транзакция 1

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Подсчёт пользователей с балансом больше 400
SELECT COUNT(*) FROM accounts WHERE balance > 400;

-- Ожидаем результат: 2 (Alice и Bob)

Транзакция 2

В другой сессии выполняется параллельная транзакция:

BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, 'Charlie');
COMMIT;

Возвращаемся к Транзакции 1

-- Повторяем запрос
SELECT COUNT(*) FROM accounts WHERE balance > 400;

Теперь, если не использовать SERIALIZABLE, результат будет 3 вместо 2, так как Charlie был добавлен в процессе работы Транзакции 1. Это и есть Phantom Read.

Но с SERIALIZABLE PostgreSQL гарантирует, что Транзакция 1 не увидит Charlie, потому что её "видение мира" заморожено в момент начала транзакции.

Особенности и ограничения уровня SERIALIZABLE

Мы разобрались, как SERIALIZABLE помогает достичь идеальной изоляции. Но что в этом мире может быть идеальным без недостатков? Поговорим честно.

Снижение производительности
SERIALIZABLE требует гораздо больше ресурсов, чем уровни READ COMMITTED или REPEATABLE READ. Почему? PostgreSQL вынужден эмулировать последовательное выполнение операций, отслеживая все возможные конфликты между транзакциями.

Ошибки сериализации
Если PostgreSQL обнаруживает невозможность выполнения транзакций в "идеальной последовательности", он генерирует ошибку сериализации (serialization_failure) и откатывает транзакцию.

Пример ошибки:

ERROR: could not serialize access due to concurrent update

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

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 станет вашим лучшим другом.

2
Задача
SQL SELF, 40 уровень, 2 лекция
Недоступна
Предотвращение двойного бронирования
Предотвращение двойного бронирования
Комментарии (5)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #3449047 Уровень 61
8 ноября 2025
Почему в теоретическом материале данной лекции в примерах указывается, что установка уровня изоляции происходит ПОСЛЕ begin (в отличие от предыдущих лекций по уровням изоляции read committed и repeatable read)? А валидатор просит указывать уровень изоляции ДО begin
Евгений Уровень 49 Expert
6 октября 2025
Комментарии в задаче подталкивают нас к каким-то непонятным действиям. Решайте задачу, не ориентируясь на комментарии (по-крайней мере SELECT вам тут просто не нужен).
Ra Уровень 35 Student
11 августа 2025
Задача пропускает решение без select for 🤷‍♀️ SELECT ... FOR UPDATE и SELECT ... FOR SHARE: Эти операторы позволяют выбрать строку и установить на нее блокировку для дальнейшего изменения (FOR UPDATE) или для совместного доступа (FOR SHARE). Блокировка, установленная с помощью FOR UPDATE, предотвращает изменение этой строки другими транзакциями до завершения текущей транзакции. FOR SHARE позволяет другим транзакциям читать строку, но не изменять её, пока действует блокировка.
Евгений Уровень 49 Expert
2 октября 2025
Если мы используем изоляцию serializable, то эти блокировки уже не требуются: https://ru.stackoverflow.com/a/1069811/385867
Ra Уровень 35 Student
3 октября 2025
спасибо. тут кстати пишут, что SELECT ... FOR UPDATE юзать нежелательно, надо FOR KEY SHARE и FOR NO KEY UPDATE "Если вы не планируете удалять строку или изменять ключевой столбец, всегда используйте SELECT FOR NO KEY UPDATE."