Почему нельзя всегда использовать Serializable?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Уровень изоляции Serializable и его ограничения
Serializable — это самый строгий уровень изоляции транзакций в БД, гарантирующий полную изолированность. Но его нельзя использовать везде, потому что он убивает производительность и создаёт проблемы конкурентности.
Уровни изоляции в ACID
От слабого к сильному:
1. Read Uncommitted — читаем грязные данные
2. Read Committed — читаем только закоммиченные данные
3. Repeatable Read — одна транзакция не видит изменения другой
4. Serializable — транзакции выполняются как последовательно
Serializable: проблема #1 — Deadlocks
Serializable создаёт много блокировок:
# Транзакция A
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
# Транзакция B
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE id = 2; # WAITING
UPDATE accounts SET balance = balance + 100 WHERE id = 1; # WAITING
COMMIT;
# DEADLOCK! Oба ждут друг друга
# PostgreSQL откатит одну из транзакций
DEADLOCK означает откат одной транзакции и повтор. Это замораживает приложение.
Serializable: проблема #2 — Производительность
Пример с 100 одновременных пользователей:
# Read Committed (обычный уровень)
Время выполнения 1000 запросов: 500ms
Тропускная способность: 2000 запросов/сек
# Serializable
Время выполнения 1000 запросов: 5 сек
Тропускная способность: 200 запросов/сек (в 10 раз медленнее!)
Почему? Каждая транзакция ждёт завершения других.
Почему Serializable медленный
# Транзакция читает и пишет: SELECT ... FOR UPDATE
# Это блокирует строки для других транзакций
BEGIN SERIALIZABLE;
SELECT * FROM products WHERE id = 1 FOR UPDATE; # Блокировка!
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
# За это время другие транзакции ждут
# Один процесс блокирует других → конвой
Проблема #3 — Конфликты фантомных строк
Phantom read — даже Repeatable Read не спасает:
# Транзакция A (Repeatable Read)
BEGIN;
SELECT COUNT(*) FROM orders WHERE user_id = 1; # Результат: 5
# Транзакция B вставляет новый заказ
INSERT INTO orders VALUES (6, user_id=1);
COMMIT;
# Транзакция A
SELECT COUNT(*) FROM orders WHERE user_id = 1; # Результат: 6 (появился фантом!)
COMMIT;
Serializable это решает, но требует блокировки всей таблицы:
BEGIN SERIALIZABLE;
# Блокирует весь диапазон WHERE user_id = 1
# Никто не может вставить, обновить или удалить
SELECT COUNT(*) FROM orders WHERE user_id = 1;
# Транзакция B заморозилась до COMMIT
Пример: E-commerce платёж
Задача: перевести деньги между счётами
BEGIN SERIALIZABLE;
# 1. Проверить баланс
SELECT balance FROM accounts WHERE id = user_id FOR UPDATE;
# 2. Если достаточно, вычесть
UPDATE accounts SET balance = balance - amount WHERE id = user_id;
# 3. Добавить на другой счёт
UPDATE accounts SET balance = balance + amount WHERE id = recipient_id;
COMMIT;
# Проблема: Serializable держит блокировку на ОБОИХ счётах
# Если у вас 1000 одновременных платежей — очередь в 1000 транзакций!
Почему Read Committed часто достаточно
# Read Committed гарантирует
BEGIN READ COMMITTED;
# Видимо только закоммиченные данные
SELECT balance FROM accounts WHERE id = 1; # 1000
# Другая транзакция изменяет и коммитит
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
# Я вижу новое значение
SELECT balance FROM accounts WHERE id = 1; # 900
COMMIT;
# Это нормально для большинства случаев!
Когда на самом деле нужен Serializable
1. Критичная финансовая логика (редко):
# Перевод денег между счётами
# Высокий риск race conditions
2. Сложные бизнес-правила:
# Проверить инвентарь + вычесть + создать заказ
# Все три операции должны быть атомарны
Альтернативы Serializable
Вариант 1: Оптимистичная блокировка
# Используем version в таблице
UPDATE accounts
SET balance = balance - amount, version = version + 1
WHERE id = user_id AND version = @expected_version;
# Если версия не совпадает — 0 строк изменено
# Приложение повторяет транзакцию
Вариант 2: Пессимистичная блокировка (смарт)
# Блокировать не всю таблицу, а только нужные строки
BEGIN READ COMMITTED;
SELECT balance FROM accounts WHERE id IN (1, 2) FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
Вариант 3: Message Queue
# Критичные операции обрабатываем асинхронно, последовательно
rq.enqueue(process_payment, user_id=1, amount=100)
# Worker обрабатывает платежи в очереди — гарантирует порядок
# Нет конфликтов, нет deadlocks
Практический пример: PostgreSQL
import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE
conn = psycopg2.connect("dbname=bank")
conn.set_isolation_level(ISOLATION_LEVEL_SERIALIZABLE)
cursor = conn.cursor()
try:
cursor.execute("SELECT balance FROM accounts WHERE id = 1 FOR UPDATE")
balance = cursor.fetchone()[0]
if balance >= 100:
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
conn.commit()
except psycopg2.extensions.TransactionRollbackError:
# Serialization conflict — повторить
conn.rollback()
Вывод
Serializable — это не серебряная пуля:
- Замораживает производительность (10x медленнее)
- Создаёт deadlocks
- Требует повторов на ошибках
Используйте только если:
- Действительно нужна полная изоляция
- Транзакции короткие
- Объём конкурентных запросов низкий
Чаще используйте:
- Read Committed с оптимистичной блокировкой
- Message queues для критичных операций
- Proper индексы для лучшей производительности