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

Почему SERIALIZABLE быстрый в PostgreSQL?

1.3 Junior🔥 151 комментариев
#Асинхронность и многопоточность

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

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

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

Почему SERIALIZABLE быстрый в PostgreSQL

Это обманчивый вопрос — SERIALIZABLE в PostgreSQL не обязательно быстрый. Но есть нюанс реализации, из-за которого в определённых сценариях он может быть эффективнее.

Уровни изоляции в PostgreSQL

PostgreSQL поддерживает 4 уровня изоляции транзакций:

  1. READ UNCOMMITTED — официально не отличается от READ COMMITTED в PG
  2. READ COMMITTED — по умолчанию, видит только коммиченные данные
  3. REPEATABLE READ — основан на snapshot'ах
  4. SERIALIZABLE — полная сериализуемость

Почему SERIALIZABLE может быть быстрым

1. Optimistic Concurrency Control (Оптимистичное управление конкурентностью)

PostgreSQL использует Serializable Snapshot Isolation (SSI), а не пессимистичные блокировки.

# ПЕССИМИСТИЧНЫЙ подход (медленнее в конкурентной среде)
# SELECT FOR UPDATE блокирует строки до конца транзакции
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- Ждёт, если другая транзакция уже заблокировала
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;

# ОПТИМИСТИЧНЫЙ подход SERIALIZABLE (быстрее)
# PostgreSQL не блокирует читаемые данные
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1;
-- Работает параллельно с другими транзакциями
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;  -- Здесь проверяется конфликт, если есть — откат

2. Отсутствие явных блокировок на чтение

READ COMMITTED и REPEATABLE READ используют блокировки для некоторых операций:

-- READ COMMITTED может нуждаться в блокировке на чтение
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;  -- Может требовать lock на read

-- SERIALIZABLE читает из snapshot'а, без блокировок
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1;  -- Быстро, без lock
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;

3. Меньше конфликтов в типичных случаях

SERIALIZABLE часто откатывает транзакцию вместо того, чтобы ждать блокировки. Откат быстрее, чем долгий wait.

# Два конкурентных потока

# Транзакция A
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT COUNT(*) FROM orders WHERE user_id = 1;  -- видит 10
INSERT INTO orders VALUES (...);  -- Хочет добавить заказ
COMMIT;  # Моментально, если нет конфликта

# Транзакция B (параллельно)
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT COUNT(*) FROM orders WHERE user_id = 1;  -- видит 10
INSERT INTO orders VALUES (...);
COMMIT;  # A успешна, B откатывается (конфликт SSI)

# Откат B быстрее, чем ждать блокировки от A в READ COMMITTED

Теория работы SSI (Serializable Snapshot Isolation)

┌─────────────────────────────────────────────────┐
│  PostgreSQL Snapshot (каждой транзакции)       │
├─────────────────────────────────────────────────┤
│  xmin: 100 (первая активная XID)               │
│  xmax: 150 (следующая возможная XID)           │
│  xip[]: [105, 120, 135]  (активные транзакции) │
└─────────────────────────────────────────────────┘

Когда две SERIALIZABLE транзакции:
1. Читают одни и те же строки
2. Одна модифицирует — создаётся конфликт
3. PostgreSQL отменяет одну транзакцию
4. Откат быстрее, чем долгое ожидание блокировки

Сравнение производительности

import time
import psycopg2
from concurrent.futures import ThreadPoolExecutor

def read_committed_transaction():
    """Медленнее при высокой конкурентности"""
    conn = psycopg2.connect("dbname=test")
    cur = conn.cursor()
    
    cur.execute("BEGIN ISOLATION LEVEL READ COMMITTED")
    start = time.time()
    
    # Может заблокироваться здесь
    cur.execute("SELECT balance FROM accounts WHERE id = 1 FOR UPDATE")
    balance = cur.fetchone()[0]
    
    # Длительная бизнес-логика
    time.sleep(0.01)
    
    cur.execute("UPDATE accounts SET balance = %s WHERE id = 1", (balance + 100,))
    cur.execute("COMMIT")
    
    elapsed = time.time() - start
    cur.close()
    conn.close()
    return elapsed

def serializable_transaction():
    """Быстрее в большинстве случаев"""
    conn = psycopg2.connect("dbname=test")
    cur = conn.cursor()
    
    cur.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
    start = time.time()
    
    # Не блокирует — читает из snapshot
    cur.execute("SELECT balance FROM accounts WHERE id = 1")
    balance = cur.fetchone()[0]
    
    # Длительная бизнес-логика
    time.sleep(0.01)
    
    try:
        cur.execute("UPDATE accounts SET balance = %s WHERE id = 1", (balance + 100,))
        cur.execute("COMMIT")
    except psycopg2.errors.SerializationFailure:
        cur.execute("ROLLBACK")  # Быстро откатывается
        # Переретрай транзакцию
    
    elapsed = time.time() - start
    cur.close()
    conn.close()
    return elapsed

# При 10 параллельных потоках
with ThreadPoolExecutor(max_workers=10) as executor:
    times_rc = list(executor.map(lambda _: read_committed_transaction(), range(10)))
    times_ser = list(executor.map(lambda _: serializable_transaction(), range(10)))
    
print(f"READ COMMITTED avg: {sum(times_rc) / len(times_rc):.3f}s")
print(f"SERIALIZABLE avg: {sum(times_ser) / len(times_ser):.3f}s")
# SERIALIZABLE часто быстрее при конкурентности

Когда SERIALIZABLE МЕДЛЕННЫЙ

SERIALIZABLE может быть медленным в этих случаях:

  1. Высокая конкурентность с конфликтами — много откатов
  2. Долгие транзакции — держат snapshot дольше
  3. Много read-write конфликтов — SSI их обнаруживает и откатывает
-- Плохой сценарий для SERIALIZABLE
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(balance) FROM accounts;  -- Читает все строки
-- Тысяча других транзакций обновляет accounts
UPDATE accounts SET balance = balance * 1.01;
COMMIT;  -- Вероятен откат из-за конфликта

Ключевые выводы

  1. SERIALIZABLE в PostgreSQL быстрый потому что:

    • Использует Optimistic Concurrency Control (SSI)
    • Не блокирует на чтение, читает из snapshot
    • Откат быстрее, чем ожидание блокировки
  2. Но это верно только если:

    • Транзакции короткие
    • Конфликты редкие
    • Конкурентность умеренная
  3. На практике:

    • READ COMMITTED часто быстрее для OLTP (много мелких транзакций)
    • SERIALIZABLE лучше для гарантий корректности
    • Выбор зависит от нагрузки
# Правильный подход — с retry логикой
def execute_serializable_with_retry(operation, max_retries=3):
    for attempt in range(max_retries):
        conn = psycopg2.connect("dbname=test")
        cur = conn.cursor()
        
        try:
            cur.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
            operation(cur)  # Выполни бизнес-логику
            cur.execute("COMMIT")
            return
        except psycopg2.errors.SerializationFailure:
            cur.execute("ROLLBACK")
            if attempt == max_retries - 1:
                raise
            # Иначе retry
        finally:
            cur.close()
            conn.close()
Почему SERIALIZABLE быстрый в PostgreSQL? | PrepBro