Изоляция транзакций и история записей в MVCC Pangolin
Атомарная единица, по которой учитываются модификации (версии одной записи) в MVCC - одна транзакция.
Чтобы организовать очередь согласованных транзакций, сервер имеет внутренний счетчик транзакций — общий для экземпляра (кластера баз) системный объект, который работает как последовательность. Счетчик с момента инициализации кластера баз вначале монотонно возрастает, а затем допускает повторное использование значений. Почти как последовательность с атрибутом CYCLE
.
Каждая транзакция на старте выбирает значение этого счетчика как следующий идентификатор из последовательности. Его и называют идентификатором транзакции или XID. Оно сохраняется за транзакцией и всеми данными, которые она изменила, от старта до «заморозки» — переноса транзакции в исторический архив.
Когда транзакция выполняется над записью DML, она завершает историю старой версии и, возможно, добавляет обновленную копию в виде новой версии записи. Каждая из версий автоматически помечается «сроком действия»: от XID транзакции, которая добавила версию (xmin
) и до XID транзакции, которая ее удалила (xmax
). Если версия активна (действительна, не удалена), то во втором поле xmax
у нее может быть 0 или идентификатор такой удаляющей транзакции, которая уже отменена или еще активна, не зафиксирована.
Сроки действия разных версий одной записи не пересекаются никогда.
Номера транзакций невозможно напрямую сравнить между собой. Например, транзакция № 100 моложе транзакции с номером 3 миллиарда. Поэтому для хранения номеров транзакций определен отдельный служебный тип данных: xid. Значения этого типа нельзя напрямую сравнивать, складывать или вычитать. Стандартная функция age(<идентификатор: xid>)
возвращает «возраст» транзакции в единицах счетчика - количество номеров, выданных от ее старта до последнего значения счетчика транзакций на текущий момент. Возраст транзакций имеет тип integer
, эти значения можно сравнивать между собой, складывать и вычитать.
Горизонты событий и снимки
При сессии, выполняющей транзакцию, хранится идентификатор самой старой из соседних транзакций, активных на момент старта этой транзакции. Он называется «горизонтом событий» транзакции. Эта граница отделяет в представлении транзакции «древнюю историю» («все умерли до моего появления») от «современной истории» («когда я появился, кто-то из них уже существовал и мог действовать параллельно со мной»). Все активности транзакций старше горизонта с точки зрения транзакции полностью завершены, а их статусы для транзакции безразличны, потому что любая из ее операций одинаково увидит все их результаты.
Дополнительно строится свой согласованный образ базы на каждый из запросов (при уровне изоляции READ COMMITTED
) или на первый запрос в транзакции (уровень REPEATABLE READ
и выше). Для этого сессия на старте запроса/транзакции запоминает:
- xid старшей из транзакций, активных прямо сейчас -
xmin
(можно понимать как локальный «горизонт» этой операции); - следующий, еще никому не выделенный сейчас xid -
xmax
(этот xid когда-то будет присвоен следующей транзакции в очереди); - список xid всех активных сейчас транзакций -
xip_list
.
В продолжение всей операции (запроса в REPEATABLE READ
или транзакции в READ COMMITTED
) правила видимости результатов между транзакциями таковы:
- результаты транзакций до
xmin
- принять; - активности транзакций от
xmin
доxmax
- просматриваяxip_list
, принять активности зафиксированных транзакций и отбросить активности транзакций, которые не завершены или отменены; - активности самой операции - принять;
- активности более молодых транзакций (
xid >= xmax
) - отбрасывать, чтобы не разрушить представление о данных, которое было согласовано на момент старта операции.
Для того чтобы отбрасывать эффекты соседних транзакций, не требуется, как в Oracle, по каждому блоку данных читать векторы отмены (undo records
) и формировать в памяти историч еский образ блока. Поскольку здесь исторические версии записей хранятся вместе с текущими, достаточно из версий записи выбрать ту, которая действовала на момент старта операции.
Для операции (запроса/транзакции) совокупность (xmin
, xmax
, xip_list
) называется «снимком» (snapshot). Каждый снимок исчерпывающе определяет согласованное состояние базы, видимое в заданной операции.
Пример просмотра горизонта событий транзакции:
First_db=# BEGIN;
BEGIN
First_db=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin
--------------
19838
(1 row)