10.1 Снимок - это не копия
Частое заблуждение: «снимок - это фотография таблицы на момент старта транзакции». Если бы это было так, каждая транзакция копировала бы базу целиком - на больших данных немыслимо дорого.
На деле снимок - это компактный набор чисел. По ним и по версионным
полям каждой строки (xmin/xmax, см. tuple-header) транзакция
вычисляет на лету, видеть ей конкретную версию или нет. Ничего не
копируется: видимость не хранится, а считается. Полный разбор - в
snapshot.
10.2 Три числа
Посмотрим на снимок своими глазами:
SELECT pg_current_snapshot();
-- 787:787:
Формат - xmin:xmax:список. Три части:
xminснимка - граница, ниже которой все транзакции уже решены (зафиксированы или откатились);xmaxснимка - граница, начиная с которой транзакций ещё не было на момент снимка;- список - идентификаторы транзакций между
xminиxmax, которые шли прямо в момент снимка.
В примере оба числа равны и список пуст - рядом никого. Под нагрузкой
это выглядело бы как 100:105:101,103: три активных границы и две
транзакции в работе.
10.3 Три границы времени
Снимок делит все транзакции на три зоны относительно момента, когда он взят:
Всё ниже xmin - прошлое: результат известен. Всё в списке - настоящее: транзакция идёт сейчас, её результат ещё не виден. Всё выше или равно xmax - будущее: на момент снимка не начиналось, поэтому невидимо. Снимок - это и есть набор границ, разделяющий эти три зоны.
10.4 Правило видимости через снимок
Теперь соберём снимок и поля строки вместе. Версия видна, если её
xmin:
- меньше
xminснимка - точно в прошлом, и если та транзакция закоммитилась, версия годится; либо - между
xminиxmaxснимка, не в списке активных, и зафиксирован по clog.
И при этом xmax версии не должен быть «уже зафиксированным в прошлом» -
иначе версия удалена с точки зрения снимка. Полные правила для пары
полей - в xmin-xmax. Главное: одни и те же версии на диске разные
транзакции видят по-разному, потому что у каждой свой снимок, а правило
одно на всех.
10.5 Снимок можно передать
Раз снимок - это просто числа, его можно отдать другой сессии, чтобы обе видели данные в одинаковом срезе времени.
-- сессия 1, внутри открытой транзакции
SELECT pg_export_snapshot();
-- 00000006-00000002-1
-- сессия 2
BEGIN ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION SNAPSHOT '00000006-00000002-1';
-- теперь сессия 2 видит данные так же, как сессия 1
Так работает параллельный pg_dump: ведущий процесс экспортирует
снимок, рабочие подхватывают его и выгружают разные таблицы из одного
среза времени, получая согласованную копию. Детали и условия - в
snapshot-export.
10.6 Когда берётся снимок
Момент взятия снимка зависит от уровня изоляции. На уровне по умолчанию (Read Committed) новый снимок берётся на каждый оператор - поэтому соседний SELECT в той же транзакции может увидеть свежие чужие коммиты. На Repeatable Read и Serializable снимок берётся один раз на транзакцию и держится до конца: сколько ни перечитывай, картина та же.
Это прямое продолжение опыта из прошлой главы и тема следующей - isolation-levels. Запомни связь: уровень изоляции - это, по сути, политика «когда обновлять снимок».
10.7 Подводный камень: цена снимка растёт с соединениями
Снимок дёшев по памяти - это несколько чисел. Но построить его не бесплатно: чтобы узнать список активных транзакций, сервер обходит структуру со всеми работающими процессами. Чем больше открытых соединений, тем дороже взять снимок, а берётся он часто - как минимум на каждую транзакцию, а на Read Committed на каждый оператор.
Отсюда неочевидное следствие: тысячи почти простаивающих соединений вредят, даже когда «ничего не делают». Каждое из них утяжеляет снимок всем остальным. Это ещё один довод за умеренный пул соединений, который мы упоминали в части про процессы. Связь прямая: много соединений - дорогие снимки - медленнее вся система.
Уроки в sandbox
lab-10.1. Снимок своими глазами
Посмотри на снимок в SQL и убедись, что на Repeatable Read он заморожен: чужой коммит, случившийся после старта транзакции, в неё не просочится. Перед каждым чтением предсказывай результат.
В сессии A посмотри текущий снимок:
SELECT pg_current_snapshot();- разбери его на три части xmin:xmax:список.В сессии A открой замороженную транзакцию:
BEGIN ISOLATION LEVEL REPEATABLE READ;и прочитай число строк:SELECT count(*) FROM tickets;. Запомни его.В сессии B добавь строку и зафиксируй:
INSERT INTO tickets VALUES ('9999999999999','000001',1,'NEW'); COMMIT;.Предскажи: что покажет повторный
SELECT count(*) FROM tickets;в сессии A - старое число или новое?Прочитай в сессии A ещё раз - число прежнее: снимок заморожен на старте транзакции.
Заверши транзакцию A (
COMMIT) и прочитай снова - теперь видна вставка сессии B.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Снимок - не копия данных, а числа xmin, xmax и список активных xid плюс правило видимости.
- pg_current_snapshot() показывает снимок в формате xmin:xmax:список.
- Снимок делит транзакции на прошлое (ниже xmin), настоящее (в списке) и будущее (от xmax).
- Видимость не хранится, а вычисляется из снимка и полей xmin/xmax строки; правило одно на всех.
- Снимок можно передать другой сессии через pg_export_snapshot и SET TRANSACTION SNAPSHOT.
- Когда берётся снимок, определяет уровень изоляции: на каждый оператор или один раз на транзакцию.
- Построение снимка дорожает с числом соединений - ещё довод за пул соединений.
Контрольные вопросы
Почему снимок - это не копия данных?
Показать ответ
Потому что он не копирует ни байта. Снимок - это несколько чисел (xmin, xmax, список активных xid) и правило, по которому из них и версионных полей строки вычисляется видимость на лету. Если бы снимок был копией, каждая транзакция дублировала бы базу - немыслимо дорого. Вместо этого видимость не хранится, а считается из компактного набора чисел.
Что означают три части снимка xmin:xmax:список?
Показать ответ
xminснимка - граница, ниже которой все транзакции уже решены (зафиксированы или откатились).xmaxснимка - граница, начиная с которой транзакций на момент снимка ещё не существовало. Список - идентификаторы транзакций между этими границами, которые шли прямо в момент снимка. Вместе они делят все транзакции на прошлое, настоящее и будущее.Как уровень изоляции связан со снимком?
Показать ответ
Уровень изоляции определяет, когда берётся снимок. На Read Committed новый снимок берётся на каждый оператор, поэтому соседние запросы в одной транзакции могут увидеть свежие чужие коммиты. На Repeatable Read и Serializable снимок берётся один раз в начале транзакции и держится до конца, поэтому повторное чтение даёт ту же картину. По сути уровень изоляции - это политика обновления снимка.
Зачем нужен экспорт снимка между сессиями?
Показать ответ
Чтобы две сессии видели данные в одинаковом срезе времени. Одна сессия вызывает
pg_export_snapshotи получает идентификатор, другая первым делом выполняетSET TRANSACTION SNAPSHOTс этим идентификатором. Главный потребитель - параллельныйpg_dump: рабочие процессы выгружают разные таблицы из одного снимка, получая согласованную копию базы.Почему много открытых соединений замедляют работу даже в простое?
Показать ответ
Потому что построение снимка обходит структуру со всеми активными процессами, чтобы собрать список идущих транзакций. Чем больше соединений, тем дороже каждый снимок, а берётся он часто - минимум на каждую транзакцию. Поэтому тысячи простаивающих соединений утяжеляют снимки всем остальным. Решение - умеренный пул соединений вместо сотен прямых подключений из приложения.