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).
См. также
Источники
- PostgreSQL Documentation — Concurrency Control: Introduction
- PostgreSQL Documentation — Routine Vacuuming
- P. Bernstein, N. Goodman — Concurrency Control in Distributed Database Systems (ACM Computing Surveys, 1981)
- MySQL — InnoDB Multi-Versioning
- Y. Wu et al. — An Empirical Evaluation of In-Memory Multi-Version Concurrency Control (VLDB 2017)
- PostgreSQL Documentation — Database Page Layout