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

Для чего нужен MVCC в PostgreSQL?

3.0 Senior🔥 151 комментариев
#Базы данных (SQL)

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

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

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

# MVCC в PostgreSQL

MVCC (Multi-Version Concurrency Control) — это механизм, который позволяет PostgreSQL обеспечивать параллельный доступ к данным без блокировок читателей и писателей друг на друга. Это одна из ключевых особенностей PostgreSQL.

Проблема, которую решает MVCC

Без MVCC при одновременном чтении и записи происходит следующее:

  • Транзакция 1 читает строку (удерживает READ LOCK)
  • Транзакция 2 пытается обновить эту строку (нужен WRITE LOCK)
  • Транзакция 2 ждёт, пока Транзакция 1 закончит читать

С MVCC этого не происходит: читатели видят старую версию, писатель создаёт новую версию.

Как работает MVCC

Каждая строка в PostgreSQL имеет две системные колонки:

xmin — идентификатор транзакции, которая создала версию
xmax — идентификатор транзакции, которая удалила версию

Пример:

TID | ID | Name  | xmin | xmax
----|----|---------|----|------
1   | 1  | Alice | 100 | 105
2   | 1  | Alice | 105 | NULL   <- новая версия
3   | 2  | Bob   | 101 | NULL

Когда Транзакция 106 читает таблицу:

  • Видит строку с ID=1, Name=Alice (xmin=105, xmax=NULL) — текущая версия
  • Не видит старую версию (xmin=100, xmax=105) — удалена в т-ции 105

Уровни изоляции транзакций

MVCC позволяет реализовать разные уровни:

1. READ UNCOMMITTED (не поддерживается в PostgreSQL)

Видит грязные чтения (uncommitted changes)

2. READ COMMITTED (по умолчанию)

from sqlalchemy import create_engine

engine = create_engine(
    "postgresql://user:pass@localhost/db",
    isolation_level="READ_COMMITTED"  # по умолчанию
)

with engine.connect() as conn:
    # Видит changes других транзакций после commit
    result = conn.execute("SELECT * FROM users")

Визуальный пример:

Время | Транзакция A (READ COMMITTED) | Транзакция B
-----|--------------------------------|----------------
T1   | BEGIN                          | 
T2   |                                | UPDATE users SET balance=100
T3   | SELECT balance FROM users      | (видит 100)
     | WHERE id=1                     |
T4   |                                | ROLLBACK
T5   | SELECT balance FROM users      |
     | WHERE id=1                     | (видит старое значение)

3. REPEATABLE READ

engine = create_engine(
    "postgresql://user:pass@localhost/db",
    isolation_level="REPEATABLE_READ"
)

Возвращает одинаковый снимок данных на весь duration транзакции:

Время | Транзакция A              | Транзакция B
-----|--------------------------|----------------
T1   | BEGIN                    | 
T2   | SELECT * FROM users      | (видит 5 пользователей)
T3   |                          | INSERT INTO users...
T4   | SELECT * FROM users      |
     | (всё ещё видит 5)        | (B добавила 6-го)
T5   | COMMIT                   |

4. SERIALIZABLE

Обеспечивает полную изоляцию, как если бы транзакции выполнялись по одной.

Phantom Reads и MVCC

phantom read — это когда две транзакции видят разные наборы строк:

# Транзакция A: REPEATABLE READ
with engine.begin() as conn:
    # Снимок T1
    count = conn.execute(
        "SELECT COUNT(*) FROM orders WHERE status='pending'"
    ).scalar()
    # count = 5
    
    # Тем временем Транзакция B добавила новый заказ
    # Снимок T1 — всё ещё видит 5
    
    # Но этот запрос вернёт 6:
    new_count = conn.execute(
        "SELECT COUNT(*) FROM orders WHERE status='pending'"
    ).scalar()

Для защиты от phantom reads нужен SERIALIZABLE.

Практический пример: Race Condition

Без правильного уровня изоляции:

from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session

engine = create_engine("postgresql://user:pass@localhost/db")

# Сценарий: два процесса пытаются купить последний товар

def buy_item(user_id: int, item_id: int):
    with Session(engine) as session:
        # READ COMMITTED (по умолчанию)
        
        # Оба процесса видят stock=1
        item = session.query(Item).filter(
            Item.id == item_id
        ).first()
        
        if item.stock > 0:
            item.stock -= 1  # оба уменьшают на 1
            session.commit()
        else:
            raise ValueError("Out of stock")

# Результат: stock=0 (вместо -1), но оба заказа приняты!

Решение — REPEATABLE READ или SERIALIZABLE:

from sqlalchemy import text

def buy_item_safe(user_id: int, item_id: int):
    with Session(engine) as session:
        session.execute(
            text("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
        )
        
        item = session.query(Item).filter(
            Item.id == item_id
        ).first()
        
        if item.stock > 0:
            item.stock -= 1
            session.commit()  # commit в конце снимка
        else:
            raise ValueError("Out of stock")

Или с SELECT FOR UPDATE:

def buy_item_locked(user_id: int, item_id: int):
    with Session(engine) as session:
        item = session.query(Item).filter(
            Item.id == item_id
        ).with_for_update().first()  # LOCK эту строку
        
        if item.stock > 0:
            item.stock -= 1
            session.commit()

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

MVCC требует хранения старых версий строк, что занимает место:

-- Проверить размер таблицы
SELECT pg_size_pretty(pg_total_relation_size('users'));
-- 2500 MB (включает старые версии)

-- VACUUM очищает мёртвые версии
VACUUM users;  -- пересчитает space

-- VACUUM ANALYZE + обновляет статистику
VACUUM ANALYZE users;

Автоматическая чистка (autovacuum) в PostgreSQL:

-- Настройка в postgresql.conf
autovacuum = on
autovacuum_naptime = '10s'         -- проверяет каждые 10 сек
autovacuum_vacuum_threshold = 50   -- после 50 изменений

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

  • MVCC даёт параллелизм без блокировок читателей и писателей
  • READ COMMITTED — стандарт, хорош для большинства приложений
  • REPEATABLE READ — защищает от phantom reads в одной транзакции
  • SERIALIZABLE — полная изоляция, но медленнее
  • SELECT FOR UPDATE — явная блокировка для critical sections
  • VACUUM — необходим для очистки старых версий и производительности