Многоверсионность
Снимки данных
16
Авторские права
© Postgres Professional, 2016–2025
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Видимость версий строк
Снимок данных
Виртуальный номер транзакции
Горизонт транзакции и базы данных
Экспорт снимка
3
Снимок
Дает согласованную картину данных на момент времени
видны только зафиксированные на этот момент данные
Создается:
Read Committed —
в начале каждого оператора
Repeatable Read, Serializable —
в начале первого оператора транзакции
снимок
xid
В теме «Страницы и версии строк» мы видели, что в страницах данных
физически может находиться несколько версий одной и той же строки.
Транзакции работают со снимком данных, который определяет, какие
версии должны быть видны, а какие — нет, чтобы обеспечить
согласованную картину данных на определенный момент времени
(на момент создания снимка — показан на рисунке синим цветом).
Согласованность здесь понимается в обычном ACID-смысле: данные
должны быть корректны, что, по сути, означает, что видеть нужно
только те данные, которые были зафиксированы на момент создания
снимка. Это полностью исключает аномалию грязного чтения на любом
уровне изоляции.
На уровне изоляции Read Committed снимок создается в начале
каждого оператора транзакции. Снимок активен, пока выполняется
оператор. Таким образом каждый оператор видит согласованные
данные, но если между двумя операторами другие транзакции
зафиксируют свои изменения, возможны неповторяемое и фантомное
чтения.
На уровне Repeatable Read (и Serializable) снимок создается один раз
в начале первого оператора транзакции. Такой снимок остается
активным до самого конца транзакции. В этом случае неповторяемое
и фантомное чтения исключаются.
4
Видимость версий строк
Видимость версии строки ограничена xmin и xmax
Версия попадает в снимок, когда
изменения транзакции xmin видны для снимка,
изменения транзакции xmax не видны для снимка
Изменения транзакции видны, когда
либо это та же самая транзакция, что создала снимок,
либо она завершилась фиксацией до момента создания снимка
Отдельные правила для видимости собственных изменений
учитывается порядковый номер операции в транзакции (cmin/cmax)
Снимок данных не является физической копией версий строк,
принадлежность версии строки к снимку определяют правила
видимости.
Будет или нет данная версия строки видна в снимке, определяется
двумя полями ее заголовка — xmin и xmax, — то есть номерами
создавшей и удалившей транзакций. Интервалы для разных версий
одной строки не пересекаются, поэтому строка таблицы представлена
в любом снимке максимум одной своей версией.
Точные правила видимости довольно сложны и учитывают различные
«крайние» случаи. Чуть упрощая, можно сказать, что версия строки
видна, когда в снимке видны изменения, сделанные транзакцией xmin,
и не видны изменения, сделанные транзакцией xmax. Иными словами,
создание версии строки должно быть видно, а удаление (если оно
было) — нет.
В свою очередь, изменения транзакции видны в снимке, если либо это
та же самая транзакция, что создала снимок (она сама видит свои же
изменения), либо транзакция была зафиксирована до создания снимка.
Несколько усложняет картину случай определения видимости
собственных изменений транзакции. Здесь может потребоваться
видеть только часть таких изменений. Например, курсор, открытый
в определенный момент, ни при каком уровне изоляции не должен
увидеть изменений, сделанных после этого момента. Для этого
в заголовке версии строки есть специальное поле (псевдостолбцы cmin
и cmax), показывающее номер операции внутри транзакции, и этот
номер тоже принимается во внимание.
5
Видимость версий строк
время
снимок
T3
T2
T1
Рассмотрим, как это работает, на простом примере.
На рисунке синим цветом отмечен момент создания снимка. Черные
линии показывают транзакции от момента начала до момента
фиксации (оборванные транзакции никогда не попадают в снимок).
В данном случае в снимке должны быть видны только изменения,
сделанные транзакцией T2, поскольку она зафиксирована до того,
как создан снимок.
Изменения транзакции T1 не должны быть видны, поскольку на момент
создания снимка она еще не была зафиксирована. А изменения
транзакции T3 не должны быть видны, поскольку она еще не началась
в момент создания снимка.
7
как отличить T1 от T2 после того, как снимок создан?
Видимость версий строк
время
снимок
момент окончания
транзакции неизвестен —
знаем лишь текущий статус
при создании снимка
T3
T2
T1
Сложность реализации состоит в том, что нам известен только
момент начала транзакции. Он определяется номером транзакции
(xid), с которым можно сравнить номер xmin или xmax версии строки.
Но момент завершения транзакции нигде не записывается. Все, что
мы можем узнать — это текущий статус транзакций.
То есть после того, как транзакция T1 завершится фиксацией, она
ничем не будет отличаться от транзакции T2: нет способа выяснить,
что одна из них была зафиксирована до создания снимка, а другая —
после.
Поскольку историческая информация об активности транзакций нигде
не хранится, снимок можно создать только в текущий момент. Нельзя,
например, создать снимок, показывающий согласованные данные по
состоянию на пять минут назад, даже если все необходимые версии
строк существуют в табличных страницах.
8
Снимок данных
xmin — номер самой ранней активной транзакции
xmax — номер, следующий за последней зафиксированной транзакцией
xip_list — список активных транзакций
xmaxxmin
xid
снимок
T1
T2
T3
видимость изменений
проверяется по списку
изменения
безусловно не видны
изменения
безусловно видны
xip_list
В момент создания снимка данных в локальной памяти
обслуживающего процесса запоминаются несколько значений, которые
и определяют снимок:
xminнижняя граница снимка, в качестве которой выступает номер
самой ранней активной транзакции.
Все транзакции с меньшими номерами либо зафиксированы, и тогда
их изменения безусловно видны в снимке, либо отменены, и тогда
изменения игнорируются.
xmaxверхняя граница снимка, в качестве которой берется номер,
следующий за номером последней зафиксированной транзакции.
Верхняя граница определяет момент, в который был сделан снимок.
Обратите внимание, что момент задается не временем (как было
условно показано на предыдущих слайдах), а увеличивающимися
номерами транзакций.
Все транзакции с номерами, большими или равными xmax, в момент
создания снимка заведомо не были зафиксированы — они либо
были активны, либо отменены, либо еще не начались.
xip_list (xid-in-progress list) — список активных транзакций.
Этот список используется для того, чтобы не учитывать в снимке
изменения транзакций, которые уже завершились, но в момент
создания снимка еще были активны.
Информацию о снимке можно получить с помощью функции
pg_current_snapshot() и нескольких других, см.
10
Виртуальные транзакции
Только читающая транзакция никак не влияет на видимость
обслуживающий процесс выделяет виртуальный номер
виртуальный номер не учитывается в снимках
виртуальный номер никогда не попадает в страницы
настоящий номер выделяется при первом изменении данных
На практике PostgreSQL использует оптимизацию, позволяющую
«экономить» номера транзакций.
Если транзакция только читает данные, то она никак не влияет на
видимость версий строк и ее номер не надо учитывать в снимках
данных.
Поэтому вначале обслуживающий процесс выдает транзакции
виртуальный номер (virtual xid). Номер состоит из внутреннего
идентификатора процесса и последовательного числа.
Выдача этого номера не требует синхронизации между всеми
процессами и поэтому выполняется очень быстро. С другой причиной
использования виртуальных номеров мы познакомимся в теме
«Заморозка» этого модуля.
Такой виртуальный номер нельзя записывать в страницы данных,
потому что при следующем обращении к странице он может потерять
всякий смысл.
Как только транзакция начинает изменять данные, ей сразу же
выдается настоящий номер (xid), который может появляться
в страницах данных — в полях xmin и xmax версий строк.
12
Горизонт снимка
xid
неактуальные
версии строк
не нужны снимку
снимок
xmaxxmin
xip list
Номер самой ранней из активных транзакций (xmin снимка) имеет
важный смысл — он определяет горизонт снимка.
Горизонт событий в астрофизике — это граница, за которой
наблюдатель не может увидеть никакие события. А в нашем случае
горизонт снимка — это граница, за которой оператор, использующий
снимок, не может увидеть неактуальные версии строк.
Действительно, при построении снимка ему требуется видеть
неактуальную версию только если она удалена (заменена) еще не
завершившейся транзакцией, поэтому снимок этого изменения не
видит. Но за горизонтом все транзакции уже гарантированно
завершились.
13
Горизонт транзакции
BEGIN SELECT
Repeatable Read, Serializable
xid
неактуальные
версии не нужны
транзакции
UPDATE
INSERT
виртуальный
номер
настоящий
номер
снимок
На разных уровнях изоляции транзакции по-разному используют снимки
данных.
Для уровней Repeatable Read и Serializable транзакция строит снимок
при первом обращении к серверу и использует его до своего окончания.
Левая граница снимка (начало горизонтального отрезка на слайде)
определяющая горизонт транзакции, равна номеру самой ранней
активной транзакции. Поскольку текущая транзакция сама является
активной, горизонт никогда не может оказаться правее настоящего
номера данной транзакции.
14
SELECT
Горизонт транзакции
Read Committed
неактуальные
версии не нужны
транзакции
xid
виртуальный
номер
BEGIN
снимок
В случае уровня изоляции Read Committed снимков может быть
несколько.
Сначала транзакция получает виртуальный номер. При первом
обращении к базе данных строится снимок.
15
Горизонт транзакции
Read Committed
xid
SELECTBEGIN
виртуальный
номер
По окончании запроса снимок освобождается. Поскольку в нашем
примере первый оператор транзакции не менял данные, транзакция
пока не получила настоящий номер и не будет удерживать горизонт
до начала следующего оператора.
16
Горизонт транзакции
Read Committed
неактуальные
версии не нужны
транзакции
xid
SELECT UPDATE
виртуальный
номер
настоящий
номер
снимок
BEGIN
Если транзакция начинает менять данные, ей выдается настоящий
номер. В этот же момент строится новый снимок, горизонт которого
(xmin), как и всегда, находится на уровне самой ранней активной
транзакции.
Поскольку текущая транзакция сама является активной, горизонт
никогда не может оказаться правее номера данной транзакции.
17
Горизонт транзакции
Read Committed
неактуальные
версии строк
не нужны транзакции
xid
SELECT UPDATE
виртуальный
номер
настоящий
номер
BEGIN
Когда оператор завершает работу, снимок освобождается, а транзакция
держит горизонт на уровне своего номера на случай отката.
Если транзакция будет долго находиться в таком состоянии (оно
называется idle in transaction), не выполняя никаких команд, горизонт
будет оставаться на месте.
18
Горизонт транзакции
Read Committed
xid
неактуальные
версии строк
не нужны транзакции
снимок
BEGIN SELECT UPDATE
INSERT
виртуальный
номер
настоящий
номер
Следующий оператор использует новый снимок, горизонт которого
учитывает номера других активных транзакций. Текущая транзакция
по-прежнему активна, поэтому горизонт снимка (xmin) не может быть
больше ее номера.
19
Горизонт транзакции
Read Committed
xid
неактуальные
версии строк
не нужны транзакции
SELECT UPDATE
INSERT
виртуальный
номер
настоящий
номер
BEGIN
Между операторами, когда нет активного снимка, горизонт опять
смещается к номеру транзакции. Если транзакция так и не получила
настоящий номер (не начала изменять данные), она вообще не будет
держать горизонт.
20
Горизонт базы данных
неактуальные
версии можно
вычищать
xid
неактуальные
версии строк
еще нужны снимкам
T3
T2
T1
UPDATE
настоящий
номер T1
Также можно определить и горизонт базы данных как минимальный
из горизонтов всех транзакций и снимков. При освобождении снимка
горизонт базы данных смещается вправо к горизонту следующего
активного снимка. Обратного движения быть не может, поскольку xmin
вновь возникающих снимков — минимальный номер активной
транзакции — сдвигается только вправо.
Неактуальные версии строк за горизонтом базы данных уже никогда
не будут нужны ни одной транзакции. Такие версии строк могут быть
безопасно вычищены.
На слайде показан пример трех транзакций с уровнем изоляции Read
Committed. Для оператора UPDATE транзакции T1 был построен
снимок. Когда оператор отрабатывает, снимок удаляется и перестает
удерживать горизонт. Однако транзакция T1 по-прежнему активна, и
поэтому горизонты снимков транзакций T2 и T3 не сдвинутся правее
номера транзакции T1. Тем самым активная транзакция, если у нее
есть настоящий номер, удерживает горизонт самим фактом своего
существования.
А это означает, что неактуальные версии строк в этой БД не могут быть
очищены. При этом «долгоиграющая» транзакция может никак не
пересекаться по данным с другими транзакциями — это совершенно
не важно, горизонт базы данных один на всех.
Это одна из причин, по которым транзакции не следует делать длиннее,
чем абсолютно необходимо.
21
Горизонт базы данных
неактуальные
версии можно
вычищать
xid
неактуальные
версии строк
еще нужны снимкам
T3
T2
T1
UPDATE
настоящий
номер T1
idle_in_transaction_session_timeout
Если описанная ситуация действительно создает проблемы и нет
способа избежать ее на уровне приложения, может помочь параметр
idle_in_transaction_session_timeout. Он определяет максимальное время
бездействия активной транзакции. После этого времени сеанс
прерывается.
23
BEGIN ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION SNAPSHOT идентификатор;
Экспорт снимка
Задача
распределить работу между несколькими одновременно работающими
транзакциями так, чтобы они видели одни и те же данные
пример: pg_dump --jobs=N
BEGIN ISOLATION LEVEL REPEATABLE READ;
...
SELECT pg_export_snapshot();
BEGIN ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION SNAPSHOT идентификатор;
идентификатор
снимка
можно
использовать, пока
активна экспортировавшая
транзакция
имеет смысл для
Repeatable Read
или Serializable
Бывают ситуации, когда несколько транзакций должны гарантированно
видеть одну и ту же картину данных. Разумеется, нельзя полагаться на
то, что картины совпадут просто из-за того, что транзакции запущены
«одновременно». Для этого есть механизм экспорта и импорта снимка.
Функция pg_export_snapshot сохраняет информацию о снимке в виде
файла в каталоге PGDATA/pg_snapshots и возвращает идентификатор
снимка, который может быть передан (внешними по отношению к СУБД
средствами) в другую транзакцию.
Экспортированный снимок существует до окончания экспортировавшей
его транзакции. В это время другая транзакция может импортировать
снимок с помощью команды SET TRANSACTION SNAPSHOT до
выполнения первого запроса в ней. Предварительно надо установить
и уровень изоляции Repeatable Read или Serializable, потому что на
уровне Read Committed операторы будут использовать собственные
снимки.
Экспорт снимка применяется, например, утилитой pg_dump при
параллельном режиме работы или логической репликацией для
получения начального состояния таблиц, входящих в подписку
(подробнее см. курс DBA3 «Резервное копирование и репликация»).
24
Итоги
Снимок содержит информацию,
необходимую для определения видимости версий строк
Время создания снимка определяется уровнем изоляции
Снимок определяет горизонт транзакции, влияющий
на возможность удаления неактуальных версий
25
Практика
1. Воспроизведите ситуацию, при которой одна транзакция
еще видит удаленную строку, а другая — уже нет.
Посмотрите снимки данных этих транзакций и значения
полей xmin и xmax удаленной строки.
Объясните видимость на основании этих данных.
2. Если в запросе вызывается функция, содержащая другой
запрос, какой снимок данных будет использоваться для
«вложенного» запроса?
Проверьте уровни изоляции Read Committed и Repeatable
Read и категории изменчивости функций volatile и stable.
3. В одной транзакции экспортируйте снимок, затем в другой
транзакции измените данные. Импортируйте снимок и
проверьте, что в нем видны еще не измененные данные.
2. Можно воспользоваться следующим шаблоном функции,
в предположении, что создана таблица t:
CREATE FUNCTION test() RETURNS bigint
VOLATILE -- или STABLE
LANGUAGE sql
BEGIN ATOMIC
SELECT count(*) FROM t;
END;
Также может пригодиться функция pg_sleep(n), вызывающая задержку
на n секунд.