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 станет вашим лучшим другом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ