Транзакции и MVCC в PostgreSQL

MVCC (Multi-Version Concurrency Control) – это механизм управления параллельным доступом к данным в базе данных, который широко используется в PostgreSQL и других СУБД для поддержки одновременных транзакций (что такое транзакции – здесь). MVCC позволяет разным транзакциям видеть базу данных в разных “версиях” (или состояниях), что обеспечивает высокую степень изоляции и консистентности данных.

Как это работает:

  1. Создание версий данных: Когда транзакция вносит изменения в базу данных, PostgreSQL не изменяет существующие строки, а создает новые версии строк с обновленными данными. Это позволяет другим транзакциям видеть старые версии данных, пока изменения не будут окончательно зафиксированы.
  2. Версионирование по времени: Каждая версия строки имеет информацию о времени начала и окончания её действия. Это позволяет транзакциям видеть данные в соответствии с моментом времени начала транзакции, что обеспечивает изоляцию.
  3. Уровни изоляции: MVCC позволяет разным транзакциям работать на разных уровнях изоляции, таких как “читать некоммитированные данные”, “повторяемое чтение”, “снимок” и “сериализуемость” (подробнее об уровнях изоляции) . Это определяет, какие версии данных транзакция может видеть, и какие блокировки должны быть установлены.
  4. Удаление устаревших данных: PostgreSQL автоматически удаляет устаревшие версии данных, когда транзакция, которая их создала, успешно завершается (коммитится). Это помогает предотвратить накопление большого объема старых данных.

MVCC является мощным механизмом, который позволяет базе данных обеспечивать высокую конкурентность и параллельность, минимизируя блокировки и обеспечивая высокий уровень изоляции для транзакций. Это делает его ключевой частью архитектуры PostgreSQL и других современных СУБД.

Более подробно механизм MVCC мы затронем в других статьях.

Сейчас мы рассмотрим как связаны транзакции с этим механизмом на практике.

Номер текущей транзакции можно узнать командой:

SELECT txid_current();

Обратите внимание, что при смене БД у нас старая незаконченная транзакция будет завершаться отменой (rollback). Следующая транзакция будет иметь свой номер.

Создадим таблицу test и добавим три новых значения:

CREATE TABLE test(i int);

INSERT INTO test VALUES (10), (20), (30);

Посмотрим статистику по “живым” (актуальным) и “мёртвым” (старым версиям в результате update или delete) туплам (записям):

SELECT relname, n_live_tup, n_dead_tup, trunc(100*n_dead_tup/ (n_live_tup+1))::float “ratio%”, FROM pg_stat_user_tables WHERE relname = ‘test’;

Теперь, если обновить одну строчку из нашего набора и посмотрим предыдущий запрос:

UPDATE test SET i = 40 WHERE i = 30;

Видим, что теперь запрос выдаст другую информацию.

Итого, имеем три актуальных записи и одну мёртвую, содержащую старую информацию в строке со значением 30.

Напрямую можно посмотреть служебную информацию только по пяти полям и только у актуальных (“живых”) записей:

SELECT xmin,xmax,cmin,cmax,ctid FROM test;

Здесь видим из колонки ctid, что у нас не видно запись со страницы под номером 3.

Что значат колонки xmax, xmin можно посмотреть в моей статье.

Для более подробного изучения нужно установить расширение PostgreSQL pageinspect:

CREATE EXTENSION pageinspect;

Посмотрим, какие функции оно содержит (ограничимся первыми на скриншоте):

\dx+

Давайте для примера воспользуемся heap_page_items и get_raw_page:

SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page(‘test’,0));

Здесь получили доступ к сырой версии таблицы и видим третью строчку (“мертвую”). А в ней видим проставленный t_xmax и ссылку на новую версию строки t_ctid.

Также можно посмотреть всё содержимое служебных полей командой:

SELECT * FROM heap_page_items(get_raw_page(‘test’,0)) \gx

Более подробную информацию смотреть уже труднее. Воспользуемся запросом:

SELECT ‘(0,’||lp||’)’ AS ctid,

       CASE lp_flags

         WHEN 0 THEN ‘unused’

         WHEN 1 THEN ‘normal’

         WHEN 2 THEN ‘redirect to ‘||lp_off

         WHEN 3 THEN ‘dead’

       END AS state,

       t_xmin as xmin,

       t_xmax as xmax,

       (t_infomask & 256) > 0  AS xmin_commited,

       (t_infomask & 512) > 0  AS xmin_aborted,

       (t_infomask & 1024) > 0 AS xmax_commited,

       (t_infomask & 2048) > 0 AS xmax_aborted,

       t_ctid

FROM heap_page_items(get_raw_page(‘test’,0)) \gx

Видим, что возле удалённой третьей записи стоит флаг xmax_commited, а у второй актуальной записи, наоборот, стоит флаг xmax_aborted.

Более подробно про t_infomask и page_inspect можно прочитать в оф.документации.

Соответственно, чтобы удалять мёртвые записи нужен какой-то механизм. И он называется вакуум (VACUUM). О нём мы поговорим в следующих статьях.

Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

одиннадцать − 6 =