← Назад к вопросам
Почему SERIALIZABLE быстрый в PostgreSQL?
1.3 Junior🔥 151 комментариев
#Асинхронность и многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему SERIALIZABLE быстрый в PostgreSQL
Это обманчивый вопрос — SERIALIZABLE в PostgreSQL не обязательно быстрый. Но есть нюанс реализации, из-за которого в определённых сценариях он может быть эффективнее.
Уровни изоляции в PostgreSQL
PostgreSQL поддерживает 4 уровня изоляции транзакций:
- READ UNCOMMITTED — официально не отличается от READ COMMITTED в PG
- READ COMMITTED — по умолчанию, видит только коммиченные данные
- REPEATABLE READ — основан на snapshot'ах
- 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 может быть медленным в этих случаях:
- Высокая конкурентность с конфликтами — много откатов
- Долгие транзакции — держат snapshot дольше
- Много read-write конфликтов — SSI их обнаруживает и откатывает
-- Плохой сценарий для SERIALIZABLE
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT SUM(balance) FROM accounts; -- Читает все строки
-- Тысяча других транзакций обновляет accounts
UPDATE accounts SET balance = balance * 1.01;
COMMIT; -- Вероятен откат из-за конфликта
Ключевые выводы
-
SERIALIZABLE в PostgreSQL быстрый потому что:
- Использует Optimistic Concurrency Control (SSI)
- Не блокирует на чтение, читает из snapshot
- Откат быстрее, чем ожидание блокировки
-
Но это верно только если:
- Транзакции короткие
- Конфликты редкие
- Конкурентность умеренная
-
На практике:
- 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()