MVCC

Материал из llmdocs — справочник разработчика
Портал: Базы данных
MVCC
Полное названиеMultiversion Concurrency Control
Идеякаждая запись создаёт новую версию, старые остаются для текущих читателей
ТеорияДэвид Рид (1978), Бернстайн и Гудман (1981)
Главное свойствочитатели не блокируют писателей, писатели — читателей
Хранение версийв таблице (PostgreSQL) или в журнале отката (Oracle, MySQL InnoDB)
ОчисткаVACUUM (PostgreSQL), purge (InnoDB)

MVCC (англ. Multiversion Concurrency Control — многоверсионное управление параллельным доступом) — метод, при котором СУБД хранит несколько версий одной и той же строки. Изменение не перезаписывает данные на месте, а создаёт новую версию; старая остаётся видимой транзакциям, которые начались раньше. Каждая транзакция работает со своим согласованным снимком базы, и главное следствие: читатели не блокируют писателей, а писатели — читателей.[1]

MVCC используют PostgreSQL, Oracle, MySQL InnoDB, SQL Server (в режиме снимков), CockroachDB и многие другие системы. На нём основаны уровни изоляции Read Committed и Repeatable Read в этих СУБД.

Зачем

Альтернатива — блокировки: чтобы прочитать строку, транзакция берёт разделяемую блокировку, чтобы изменить — исключительную. Тогда долгий аналитический запрос блокирует обновления, а массовое обновление останавливает отчёты. MVCC снимает этот конфликт: отчёт читает данные на момент своего начала, не мешая текущей работе. Блокировки остаются только между пишущими транзакциями, которые меняют одни и те же строки.

Теоретические основы заложили диссертация Дэвида Рида (MIT, 1978) и обзор Филипа Бернстайна и Натана Гудмана (1981);[3] одной из первых коммерческих систем с MVCC стала InterBase в 1980-х.

Как это устроено в PostgreSQL

Каждая версия строки (кортеж) хранит в заголовке служебные поля:[6]

  • xmin — номер транзакции, создавшей версию;
  • xmax — номер транзакции, удалившей или заменившей её (0, если версия актуальна).

UPDATE в PostgreSQL — это пометка старой версии как удалённой (запись xmax) и вставка новой. При чтении транзакция по своему снимку — списку завершённых и ещё активных транзакций — решает, какая версия ей видна: созданная до начала снимка зафиксированной транзакцией и не удалённая в пределах видимого.

SELECT xmin, xmax, * FROM accounts WHERE id = 1;
-- xmin | xmax | id | balance
-- 7381 |    0 |  1 |   1000
UPDATE accounts SET balance = 900 WHERE id = 1;
SELECT xmin, xmax, * FROM accounts WHERE id = 1;
-- 7382 |    0 |  1 |    900     -- новая версия; старая осталась в таблице

Очистка: VACUUM

Старые версии, которые уже не нужны ни одной транзакции, занимают место — это «мёртвые» кортежи. Их убирает процесс VACUUM, в нормальном режиме — фоновый autovacuum.[2] Если очистка не успевает, таблицы и индексы раздуваются (bloat): занимают в разы больше места и медленнее читаются.

Вторая задача VACUUM — заморозка старых строк. Номера транзакций в PostgreSQL 32-битные и идут по кругу; если строки вовремя не пометить как «видимые всем», произойдёт переполнение счётчика (transaction ID wraparound), и СУБД остановит запись, чтобы не потерять данные. Поэтому autovacuum никогда не стоит отключать.

Оптимизация HOT (heap-only tuples): если обновление не затрагивает индексированные столбцы и на странице есть место, новая версия кладётся рядом, и индексы не меняются.

Другой подход: журнал отката

В Oracle и MySQL InnoDB актуальная версия строки хранится на месте, а предыдущие — в отдельном журнале отката (undo log). Транзакция, которой нужна старая версия, восстанавливает её, проходя по цепочке записей отката.[4]

В таблице (PostgreSQL) В журнале отката (Oracle, InnoDB)
Обновление новая версия в таблице изменение на месте + запись в журнал
Откат транзакции мгновенный нужно применить журнал
Чтение старых версий напрямую восстановление из журнала
Обслуживание VACUUM, раздувание таблиц очистка журнала, долгие транзакции раздувают его

Сравнение разных реализаций MVCC с замерами — в исследовании Ву и соавторов.[5]

Практические следствия

  • Долгие транзакции вредны. Пока открыта старая транзакция (в том числе «зависшая» idle in transaction), VACUUM не может удалить версии, которые она теоретически может увидеть, и таблицы раздуваются. Их стоит отслеживать в pg_stat_activity и ограничивать параметром idle_in_transaction_session_timeout.
  • Частые обновления одной строки (счётчики, статусы) порождают много мёртвых версий — для таких таблиц настраивают более агрессивный autovacuum.
  • count(*) по большой таблице в PostgreSQL требует проверки видимости строк и не бывает мгновенным.
  • Конфликты записи остаются. MVCC не отменяет блокировок между пишущими транзакциями; для правил, затрагивающих несколько строк, нужны явные блокировки или уровень Serializable (см. Транзакция и ACID).

См. также

Источники

  1. PostgreSQL Documentation — Concurrency Control: Introduction
  2. PostgreSQL Documentation — Routine Vacuuming
  3. P. Bernstein, N. Goodman — Concurrency Control in Distributed Database Systems (ACM Computing Surveys, 1981)
  4. MySQL — InnoDB Multi-Versioning
  5. Y. Wu et al. — An Empirical Evaluation of In-Memory Multi-Version Concurrency Control (VLDB 2017)
  6. PostgreSQL Documentation — Database Page Layout