← Назад к вопросам

Почему нельзя всегда использовать Serializable?

1.8 Middle🔥 161 комментариев
#Другое

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Уровень изоляции 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 индексы для лучшей производительности