linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть II — Многоверсионность (MVCC)

$ глава 9 · 50 минут

Зачем многоверсионность: транзакции наблюдаемо

Мы разобрали, как строка устроена на диске: заголовок, версионные поля, цепочка ctid. Теперь поймём, зачем всё это. Зачем UPDATE плодит версии вместо того, чтобы поменять строку на месте. Ответ - в одной задаче, которую без многоверсионности решить дорого: дать многим работать с данными одновременно, не мешая друг другу.

Эту часть мы начинаем с наблюдаемого опыта. В лабе ты откроешь две сессии и увидишь, как одна не видит незакоммиченных изменений другой - не из теории, а на живых данных, своими глазами.

9.1 Сценарий: отчёт во время перевода

Представь банковскую таблицу. Один запрос считает суммарный баланс по всем счетам - читает миллион строк, и это занимает время. В этот момент идёт перевод денег: списать с одного счёта, зачислить на другой.

Вопрос: что увидит отчёт, если перевод пройдёт прямо посреди его чтения? Если отчёт прочитает счёт-отправитель уже после списания, а счёт-получатель ещё до зачисления, он недосчитается денег. Сумма получится неверной не из-за ошибки в данных, а из-за того, что отчёт поймал систему в промежуточном состоянии.

Это и есть проблема согласованности при конкурентном доступе. Её надо решить так, чтобы отчёт видел целостную картину, а перевод не ждал окончания отчёта.

9.2 Наивно: всех блокировать

Простое решение - блокировки. Пишущий запирает строки, читающие ждут, пока он закончит. Тогда отчёт либо целиком до перевода, либо целиком после - промежуточного состояния он не увидит.

Просто и надёжно, но медленно. Отчёт на миллион строк держит блокировки долго, и все переводы на это время встают. А если наоборот - перевод ждёт, пока отчёт дочитает, то быстрые операции записи зависят от медленных операций чтения. Чтение и запись начинают мешать друг другу, и система буксует именно тогда, когда нагрузка высокая.

Хочется иначе: чтобы читающие не блокировали пишущих, а пишущие - читающих. Для этого нужно, чтобы старое и новое состояние строки какое-то время существовали одновременно.

9.3 Многоверсионность: не менять, а добавлять

Решение PostgreSQL - многоверсионность (MVCC). Строка не правится на месте. Вместо этого каждое изменение порождает новую версию, а старая остаётся жить, пока её кто-то видит.

ОперацияЧто физически происходит
INSERTпоявляется новая версия (xmin её создателя)
UPDATEстарая версия получает xmax, рядом пишется новая
DELETEверсия получает xmax, новой не пишется

Это ровно те поля xmin/xmax, что мы видели в заголовке кортежа (tuple-header). Теперь понятно, зачем они: по ним каждая транзакция решает, какую версию ей показать. Отчёт видит счета такими, какими они были на его старте, а перевод спокойно создаёт новые версии рядом. Подробнее идея разобрана в why-mvcc.

9.4 Две сессии: видно своими глазами

Лучший способ поверить - посмотреть. Откроем две сессии к одной базе.

Сессия 1 начинает транзакцию и меняет бронь, но не коммитит:

sql
-- сессия 1
BEGIN;
UPDATE bookings SET total = 999 WHERE book_ref = '000001';
-- приглашение сменилось на lab=*# : звёздочка значит «идёт транзакция»

Сессия 2 в этот момент читает ту же бронь:

sql
-- сессия 2
SELECT total FROM bookings WHERE book_ref = '000001';
--  total
-- --------
--  101.00     <- старое значение! изменения сессии 1 не видны

Сессия 2 видит старую версию: незакоммиченное изменение для неё не существует. Теперь сессия 1 фиксирует:

sql
-- сессия 1
COMMIT;

И только теперь сессия 2 увидит новое значение:

sql
-- сессия 2
SELECT total FROM bookings WHERE book_ref = '000001';
--  total
-- --------
--  999.00     <- после COMMIT видна новая версия

Звёздочка в приглашении lab=*# - удобный индикатор: пока она есть, твоя транзакция открыта и её изменения никто, кроме тебя, не видит.

9.5 Читатели и писатели не ждут друг друга

В этом опыте есть главное свойство MVCC: сессия 2 читала, пока сессия 1 держала открытую запись, и никто никого не ждал. Сессия 2 не зависла в ожидании коммита - она просто увидела старую версию. Сессия 1 не ждала, пока сессия 2 дочитает.

Это и есть ответ на задачу из начала главы. Отчёт по балансу видит согласованный срез данных на момент своего старта (его снимок - тема snapshot и следующей главы). Перевод тем временем пишет новые версии. Чтение не блокирует запись, запись не блокирует чтение - именно то, чего не давали голые блокировки.

9.6 Цена: мусор и его уборка

Бесплатно это не даётся. Каждый UPDATE и DELETE оставляет мёртвые версии. В опыте выше после коммита сессии 1 старая версия брони с total = 101 осталась лежать на странице - её больше никто не увидит, но место она занимает.

Пока такую версию видит хоть одна транзакция, тронуть её нельзя. Когда последняя такая транзакция завершится, версия становится мусором. Убирает мусор vacuum - ему посвящена целая часть книги. Поэтому активно изменяемая таблица в PostgreSQL всегда чуть больше «чистого» объёма данных. Это не дефект, а обратная сторона того, что читатели и писатели не мешают друг другу.

9.7 Подводный камень: долгая транзакция держит мусор

Раз мёртвую версию нельзя убрать, пока её кто-то видит, появляется коварное следствие. Одна долгая транзакция - открытая и забытая, или огромный отчёт на часы - заставляет систему хранить все версии, которые существовали на её старте. Vacuum не может их убрать: вдруг эта транзакция на них посмотрит.

В итоге одна забытая транзакция в углу способна раздуть таблицы по всей базе: мусор копится, потому что его не дают убрать. Мы вернёмся к этому подробно в части про vacuum и горизонт транзакции. Пока запомни связь: долгая транзакция - это удерживаемый мусор, и BEGIN без своевременного COMMIT обходится дороже, чем кажется.

Уроки в sandbox

lab-9.1. Две сессии

Открой две сессии к одной базе и увидь многоверсионность в действии: одна сессия не видит незакоммиченных изменений другой. Перед каждым чтением во второй сессии предскажи, какое значение она покажет - старое или новое.

  1. В сессии A начни транзакцию и измени бронь, не коммить: BEGIN; UPDATE bookings SET total = 999 WHERE book_ref = '000001'; - обрати внимание на звёздочку в приглашении.

  2. Предскажи: что покажет сессия B при чтении этой же брони прямо сейчас?

  3. В сессии B прочитай: SELECT total FROM bookings WHERE book_ref = '000001'; - убедись, что видно старое значение 101.00.

  4. В сессии A зафиксируй: COMMIT; - звёздочка в приглашении пропадёт.

  5. Предскажи, что теперь покажет сессия B, и проверь повторным SELECT - значение стало 999.00.

  6. Объясни своими словами, почему сессия B ни разу не ждала сессию A.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Проблема: при конкурентном доступе читающий запрос может поймать данные в промежуточном состоянии.
  • Блокировки решают это, но заставляют чтение и запись ждать друг друга - медленно.
  • MVCC не меняет строку на месте: UPDATE и DELETE создают новые версии, старые живут, пока их кто-то видит.
  • На двух сессиях видно: вторая не видит незакоммиченных изменений первой и не ждёт её.
  • Звёздочка в приглашении psql (lab=*#) означает открытую транзакцию.
  • Цена MVCC - мёртвые версии (мусор); долгая транзакция мешает их убрать и раздувает таблицы.

Контрольные вопросы

  1. Какую задачу решает многоверсионность, чего не дают обычные блокировки?

    Показать ответ

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

  2. Почему сессия B в опыте видела старое значение, пока сессия A не закоммитила?

    Показать ответ

    Потому что незакоммиченные изменения видны только той транзакции, которая их сделала. UPDATE в сессии A создал новую версию строки, но пометил её своим ещё не зафиксированным xid. Правило видимости в сессии B такую версию отвергает и показывает предыдущую, уже закоммиченную. После COMMIT транзакция A становится зафиксированной, и её версия становится видимой остальным.

  3. В чём состоит главное преимущество MVCC, показанное на двух сессиях?

    Показать ответ

    В том, что читатель и писатель не ждут друг друга. Сессия B читала, пока сессия A держала открытую незакоммиченную запись, и не зависла в ожидании - она просто увидела старую версию. Сессия A тоже не ждала сессию B. Чтение не блокирует запись, запись не блокирует чтение, чего и не давали обычные блокировки.

  4. Откуда в таблице берётся мусор и почему он неизбежен при MVCC?

    Показать ответ

    Из устаревших версий. Каждый UPDATE и DELETE оставляет старую версию строки, которая больше не видна новым транзакциям, но физически лежит на странице. Это неизбежно: чтобы читатели и писатели не мешали друг другу, старая версия должна жить, пока её кто-то может видеть. Когда последняя такая транзакция завершится, версия становится мусором, и его убирает vacuum.

  5. Почему долгая транзакция опасна для размера базы?

    Показать ответ

    Потому что мёртвую версию нельзя убрать, пока её видит хоть одна транзакция. Долгая транзакция видит все версии, существовавшие на её старте, поэтому vacuum не может их удалить, пока она открыта. Одна забытая или очень длинная транзакция заставляет хранить накопившийся мусор по всей базе, и таблицы распухают. Поэтому BEGIN без своевременного COMMIT обходится дороже, чем кажется.

← Предыдущая08-toastСледующая →10-snapshot
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки