Рассмотрим две связанные между собой технологии PostgreSQL, а именно что такое изоляция и многоверсионность. В документации вы можете почитать про это здесь.

Многоверсионность

Представим следующие ситуации на сервере баз данных:

  • два процесса читают одну строчку одновременно;
  • два процесса одновременно хотят изменить одну и туже строчку;
  • один процесс читает строчку, а другой, в тоже время, её изменяет.

В первом случае проблем не возникает. Несколько процессов могут одновременно читать одну и туже строчку без всяких сложностей.

Для решения второй проблемы придумали блокировки. Процесс который начнет изменение строки ставит на ней блокировку. При этом второй процесс будет ждать пока первый не внесет свои изменения и не разблокирует эту строку.

А что делать, когда один процесс хочет изменить строку, а второй её прочитать? Можно было бы и тут применять блокировки, но был придуман вариант получше. Этот вариант называется — многоверсионность.

Многоверсионность — это наличие нескольких версий одной и той же строки. Например, мы создали строку с помощью команды INSERT, появилась первая версия строки. Затем мы обновили строку с помощью команды UPDATE, появилась вторая версия этой строки. Затем мы еще раз обновили строку, это уже третья версия. А потом мы удалили эту строку командой DELETE, больше версий этой строки нет.

INSERTUPDATEUPDATEDELETE
Первая версия
строки
Вторая версия
строки
Третья версия
строки
Этой строки
больше нет

Более подробно про многоверсионность в Oracle и PostgreSQL расписано в этой статье!

Снимки данных

Снимок данных — это согласованный срез базы данных на определённый момент времени. Это означает, что все версии строк согласованы между собой в конкретном снимке. Снимок основывается на номере транзакции и на списке активных транзакций в этот момент времени. Список транзакций нужен, чтобы рассматривать только зафиксированные транзакции в этот момент времени и отбрасывать не зафиксированные или начатые после этой транзакции.

Транзакция всегда работает с определённым снимком данных. Другими словами транзакция должна видеть согласованные данные, которые другие транзакции уже успели зафиксировать. И не должна видеть изменения, которые другие транзакции успели произвести, но не успели зафиксировать.

Многоверсионность позволяет экономить на блокировках — читающая и изменяющая транзакции могут работать одновременно. Блокировки нужны только при работе двух пишущих транзакций.

Блокировки

Как вы уже усвоили, блокировки накладываются на строки, когда 2 процесса хотят изменить одну и туже строку.

Еще блокировки могут накладываться на таблицы. Такие блокировки не приносят больших проблем. Они запрещают изменение или удаление таблицы, пока с ней идет работа. Например, обычный SELECT накладывает блокировку на таблицу.

Блокировки устанавливаются по мере необходимости и снимаются автоматически. Хотя можно наложить блокировку и вручную.

Статус транзакции и снимки данных

Чтобы работала многоверсионность и изоляция со снимками данных, PostgreSQL должен различать статусы транзакций. Транзакция может быть:

  • зафиксирована,
  • откачена,
  • активна.

За статус транзакции отвечают специальные двоичные файлы, в которых всего 2 бита:

  • 10 — зафиксирована;
  • 01 — откачена;
  • 00 — активна.

Та транзакция, которой нужно построить снимок, смотрит на текущие транзакции и не принимает во внимание откаченные или активные.

Таким образом многоверсионность позволяет не откатывать сами данные, просто меняя два бита в статусе транзакций.

Например была табличка:

2012Nissan Tiida I Рестайлинг500000
1988LADA (ВАЗ) 210945000

Первая транзакция меняет первую строку (UPDATE), но не успевает зафиксировать изменения:

2012Nissan Tiida I Рестайлинг550000

Вторая транзакция хочет прочитать первую строчку. Видит что сейчас эта строчка изменяется, но изменяющая транзакция ещё активна. И вторая транзакция читает первую версию этой строки:

2012Nissan Tiida I Рестайлинг500000

В это время первая транзакция по какой-то причине не выполнилась, и откатилась. При этом в файле таблицы успела записаться вторая версия строки (в момент UPDATE), но транзакция, которая эта сделала имеет статус — откачена.

Прилетает 3 транзакция, хочет прочитать эту строку и видит что у этой строчки 2 версии. Но вторая версия была сделана транзакцией, которая сейчас в состоянии «откачена» (в файле записано 01). И 3 транзакция читает предыдущую версию строки:

2012Nissan Tiida I Рестайлинг500000

В файле теперь хранится две версии одной строчки, лишняя версия позже удалится фоновым процессом VACUUM. А при работе транзакций сами данные не откатывались, что делает базу данных более отзывчивой и быстрой.

Многоверсионность и изоляция в PostgreSQL это хорошо! Но плохо то, что идет накопление старых версий строк. Чтобы разрешить эту проблему необходимо периодически производить очистку.

Очистка файлов данных от ненужных версий строк

Постепенно возникает такое состояние, когда некоторые снимки данных уже не нужны. Это значит что все версии строк относящиеся к этим снимкам тоже не нужны. Значит эти строчки нужно удалить и освободить место. Такую очистку делает процесс VACUUM. Этот процесс работает параллельно с другими процессами и ничего не блокирует. При этом в файлах данных появляются дыры, которые мы можем использовать для новых строчек. Но сами файлы не уменьшаются, просто в них появляется свободное пространство.

Есть процесс VACUUM FULL, который полностью перестраивает файлы данных, делая их компактными. Но этот процесс уже блокирует таблицу, и на запись и на чтение.

Обычно очисткой занимается процесс AUTOVACUUM. Это фоновый процесс, который периодически очищает таблицы. Таких процессов два вида:

  • AUTOVACUUM launcher — фоновый процесс, который реагирует на активность изменения данных и запускает рабочие процессы по очистке таблиц;
  • AUTOVACUUM worker — запускаются по необходимости и выполняют очистку.

Если таблица меняется часто, то и очищается она чаще. В документации про это можете почитать тут.

Таким образом многоверсионность и изоляция приносит некоторую проблему, но процесс автоматической очистки файлов в PostgreSQL решает её.

Уровни изоляции

Стандарт SQL содержит 4 уровня изоляции:

  • Read uncommitted — не поддерживается PostgreSQL. Позволяет читать не зафиксированные данные. Другими словами, одна транзакция не успела зафиксировать некоторые данные, а другая уже может их прочитать.
  • Read committed — используется по умолчанию. Система строит согласованные снимки состояний по любому запросу (SELECT, INSERT, UPDATE). Даже если в 1 транзакции несколько операторов, все они будут создавать свои снимки. Например, вы выполняете команду SELECT, которая выполняется 5 мнут. Но она будет работать со своим снимком данных. Если в этот момент другой запрос что-то поменяет в таблицах, то ваш SELECT это проигнорирует. Второй SELECT построит свой снимок данных и может вернуть другой результат.
  • Repeatable read — снимок строится на момент транзакции, а не отдельного оператора. Это нужно, если в транзакции много разных операторов и они должны видеть согласованные данные на один и тот же момент времени.
  • Serializable — полная изоляция. Используются блокировки так, чтобы пересекающиеся транзакции работали последовательно и не мешали друг другу. Если транзакции захотят обратиться к одним и тем же данным может получиться ситуация, когда одна транзакция завершиться успешно, а другая не завершиться. Если приложение работает в этом режиме, то оно должно уметь повторно выполнять транзакции, которые небыли выполнены.

Практика

Подключимся к СУБД и создадим табличку test, затем наполним её двумя строчками:

postgres@s-pg13:~$ psql
Timing is on.
psql (13.3)
Type "help" for help.

postgres@postgres=# CREATE TABLE test (date date, brand text, cost int);
CREATE TABLE
Time: 12,046 ms

postgres@postgres=# INSERT INTO test (date, brand, cost) VALUES ('2012-11-29', 'Nisan', 500000), ('1988-10-15', 'Lada', 45000);
INSERT 0 2
Time: 2,121 ms

Посмотрим на созданную табличку и начнём выполнять транзакцию, обновляющую цену для Nisan:

postgres@postgres=# SELECT * FROM test;
    date    | brand |  cost
------------+-------+--------
 2012-11-29 | Nisan | 500000
 1988-10-15 | Lada  |  45000
(2 rows)

Time: 0,242 ms

postgres@postgres=# BEGIN;
BEGIN
Time: 0,096 ms

postgres@postgres=# UPDATE test SET cost = 550000 WHERE brand = 'Nisan';
UPDATE 1
Time: 0,391 ms

После этого UDDATE в файле данных появилась новая версия строки. Но так как статус транзакции изменившей эту строку «Выполняется», то эта версия строки не будет видна для других транзакций. Проверим это, запустив новый терминал:

postgres@s-pg13:~$ psql
Timing is on.
psql (13.3)
Type "help" for help.

postgres@postgres=# SELECT * FROM test;
    date    | brand |  cost
------------+-------+--------
 2012-11-29 | Nisan | 500000
 1988-10-15 | Lada  |  45000
(2 rows)

Time: 0,541 ms

Вернёмся к предыдущему терминалу и зафиксируем изменения:

postgres@postgres=# END;
COMMIT
Time: 0,313 ms

Затем на втором терминале прочитаем табличку ещё раз:

postgres@postgres=# SELECT * FROM test;
    date    | brand |  cost
------------+-------+--------
 1988-10-15 | Lada  |  45000
 2012-11-29 | Nisan | 550000
(2 rows)

Time: 0,193 ms

Теперь изменённая строка видна другим транзакциям.

Кстати, если мы не начинаем транзакцию с помощью команды BEGIN, то одиночная команда выполняется как транзакция. Просто перед одиночной командой выполняется BEGIN, а после неё END или ROLLBACK.


Сводка
Изоляция и многоверсионность в Postgresql
Имя статьи
Изоляция и многоверсионность в Postgresql
Описание
В этой статье рассмотрим две связанные между собой технологии PostgreSQL. А именно что такое изоляция и многоверсионность

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

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