Многоверсионность
Снимки данных
13
Авторские права
© Postgres Professional, 2016–2022.
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Видимость версий строк
Снимок данных
Виртуальный номер транзакции
Горизонт транзакции и базы данных
Экспорт снимка
3
Снимок
Дает согласованную картину данных на момент времени
неформально: видны только зафиксированные на этот момент данные
Создается:
Read Committed —
в начале каждого оператора
Repeatable Read, Serializable —
в начале первого оператора транзакции
снимок
время
В теме «Страницы и версии строк» мы видели, что в страницах данных
физически может находиться несколько версий одной и той же строки.
Транзакции работают со снимком данных, который определяет, какие
версии должны быть видны, а какие — нет, чтобы обеспечить
согласованную картину данных на определенный момент времени
(на момент создания снимка — показан на рисунке синим цветом).
Изоляция на основе снимков данных (snapshot isolation) требует, чтобы
транзакции были видны только те данные, которые были
зафиксированы на момент создания снимка. Это гарантирует, что
данные снимка будут согласованными (в обычном 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). Номер состоит из внутреннего
идентификатора процесса и последовательного числа.
Выдача этого номера не требует синхронизации между всеми
процессами и поэтому выполняется очень быстро. С другой причиной
использования виртуальных номеров мы познакомимся в теме
«Заморозка» этого модуля.
Такой виртуальный номер нельзя записывать в страницы данных,
потому что при следующем обращении к странице он может потерять
всякий смысл.
Как только транзакция начинает менять данные, ей сразу же выдается
настоящий номер, который может появляться в страницах данных
(в полях 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
old_snapshot_threshold
idle_in_transaction_session_timeout
Если описанная ситуация действительно создает проблемы и нет
способа избежать ее на уровне приложения, помогут два параметра:
- old_snapshot_threshold определяет максимальное время жизни
снимка. После этого времени сервер получает право удалять
неактуальные версии строк, а если они понадобятся «долгоиграющей»
транзакции, то она получит ошибку snapshot too old.
- 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 возвращает идентификатор снимка,
который может быть передан (внешними по отношению к СУБД
средствами) в другую транзакцию.
Пока экспортирующая транзакция выполняется, другая транзакция
может импортировать снимок с помощью команды 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 AS $$
SELECT count(*) FROM t;
$$
VOLATILE -- или STABLE
LANGUAGE sql;
Также может пригодиться функция pg_sleep(n), вызывающая задержку
на n секунд.