Перейти к основному содержимому

Уровень 2.0

Предусловие: изучен модуль «Конфигурация сервера»

В этой главе мы рассмотрим механизм транзакций в СУБД Pangolin. Вы узнаете, как обеспечиваются требования ACID, как работает многоверсионность и изоляция, какие бывают блокировки и как отслеживается состояние транзакций.

Транзакции​

Поддержка транзакций — важнейшее свойство СУБД промышленного уровня.

Транзакция — логически неделимый набор операций (ACID):

  • Атомарность (atomicity) — транзакция либо выполняется полностью, либо полностью отменяется.
  • Согласованность (consistency) — переводят базу данных из одного корректного состояния в другое корректное состояние.
  • Изоляция (isolation) — транзакция не должна подвергаться влиянию и сама влиять на другие транзакции, параллельно работающие в системе.
  • Долговечность (durability) — данные не должны теряться даже в случае сбоя системы.

Управление транзакциями обычно находится на стороне клиента. Поддержка транзакций — ответственность сервера.

Транзакция собирает несколько команд в неделимую операцию, которая либо завершается как единое целое при ее фиксации (COMMIT), либо ни одно из действий не оставляет следов при ее отмене (ROLLBACK). Транзакции — средство гарантировать целостность (согласованность) данных в базе. Последовательность действий, выполняемая транзакцией, не должна быть видна для других транзакций как минимум до ее фиксации. Если при выполнении транзакции происходит ошибка, то никаких следов этих действий остаться не должно.

К транзакциям предъявляются требования ACID. В PostgreSQL транзакция представляет собой последовательность команд SQL, заключенную в блок между командами BEGIN и COMMIT. Если во время выполнения транзакции решено отказаться от фиксации, вместо COMMIT используют ROLLBACK для отката. При откате гарантируется возврат к исходному состоянию данных так, как будто транзакция и не начиналась.

Управление транзакциями чаще всего отнесено в приложение, но в PostgreSQL на стороне сервера также можно управлять транзакциями в процедурах.

Пример транзакции​

Рассмотрим простой пример – создадим таблицу внутри транзакции, а затем откатим изменения.

Начнем транзакцию:

student=> BEGIN;
BEGIN

Создадим таблицу с помощью CTAS (CREATE TABLE AS SELECT), которая будет содержать текущую дату и время:

student=*> CREATE TABLE table1 AS SELECT now() mtime;
SELECT 1

Проверим, что таблица создана и содержит данные:

student=*> SELECT * FROM table1;
mtime
-------------------------------
2024-09-06 08:29:11.251527+03
(1 row)

Отменим транзакцию:

student=*> ROLLBACK;
ROLLBACK

Попытаемся обратиться к таблице после отката:

student=> SELECT * FROM table1; -- Таблицы нет
ERROR: relation "table1" does not exist
LINE 1: SELECT * FROM table1;
^

В PostgreSQL команда CREATE TABLE транзакционна в отличие от некоторых других СУБД. Таблица была создана внутри транзакции, а после отката она полностью исчезла.

Изоляция​

Как работает изоляция​

В PostgreSQL используется механизм многоверсионности (Multi-version concurrency control MVCC), который позволяет разным транзакциям одновременно читать и записывать строки без приостановки работы друг друга. Точнее, читать одну и ту же строку могут одновременно множество транзакций, даже тогда, когда эту строку изменяет какая-либо транзакция. Изменять строку в моменте может лишь одна транзакция, другие транзакции, претендующие на изменение этой же строки, должны ожидать освобождения блокировки на изменяемую строку. Команды и транзакции работают с индивидуальными подмножествами версий строк, называемых снимками данных.

Механизм MVCC использует разные версии строк, команде или транзакции видимы версии строк из соответствующего снимка. Используя снимки, в PostgreSQL добиваются независимой параллельной работы множества транзакций — изоляции.

Правила изоляции:

  • Одну и ту же версию строки могут читать множество транзакций, не блокируя друг друга;
  • Изменять версию строки может единственная транзакция, блокируя другие транзакции, пытающиеся изменить эту строку;
  • При блокировке строки для ее изменения читающие транзакции не блокируются.

При вставке INSERT создает строку, а точнее, ее первую версию. При удалении строки DELETE специально помечает первую версию строки так, чтобы ее не было видно. Но эта версии строки физически не удаляется, так как она может быть нужна другим транзакциям. Изменение строки командой UPDATE помечает первую версию строки так же, как и DELETE, и эту версию далее не видно, но UPDATE тут же создает новую версию строки, и она видна. Если удаленные версии строк не нужны ни для одной транзакции, их следует очистить. Подробнее об этом в главе «Обслуживание СУБД».

Примеры изоляции​

Пример 1​

Первый сеанс в ОС. Начнем транзакцию и создадим таблицу с миллионом строк:

[student@ServerName ~]$ psql
student@student=> BEGIN;
student@student=*> CREATE TABLE bill AS SELECT g.i FROM generate_series(1,1000000) g(i);
SELECT 1000000

Проверим количество строк в таблице (внутри транзакции данные видны):

student@student=*> SELECT count(*) FROM bill;
count
---------
1000000
(1 row)

Второй сеанс в ОС. Пытаемся выполнить тот же запрос из другого сеанса:

[student@ServerName ~]$ psql -c 'SELECT count(*) FROM bill'
ERROR: relation "bill" does not exist
LINE 1: SELECT count(*) FROM bill

Изменения, вносимые еще не зафиксированной транзакцией, не видны другим транзакциям.

Стандарт SQL описывает ситуацию, когда изменения в данных, производимые еще не зафиксированной транзакцией, становятся видны другим транзакциям. Эта ситуация называется «аномалия грязного чтения». В PostgreSQL такой аномалии не наблюдается ни на каком уровне изоляции транзакций.

Пример 2​

Первый сеанс в ОС. Завершаем начатую ранее транзакцию фиксацией:

student@student=*> COMMIT;
COMMIT

Второй сеанс в ОС. Теперь данные видны, так как транзакция в первом сеансе зафиксирована:

[student@ServerName ~]$ psql -c 'SELECT count(*) FROM bill'
count
---------
1000000
(1 row)

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

На самом деле эти изменения будут видны и тем транзакциям, которые начались до фиксации и работают на уровне изоляции READ COMMITTED. Подробнее об этом далее в этой главе.

Долговечность​

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

Проведем эксперимент – принудительно остановим экземпляр без выполнения контрольной точки, имитируя «выключение питания».

Имитируем сбой, посылая процессам postgres сигнал SIGQUIT (сигнал 3, смотрите kill -l):

[student@ServerName ~]$ sudo killall -QUIT postgres

Где -QUIT — флаг завершения работы без контрольной точки.

Проверим, что процессы остановлены:

[student@ServerName ~]$ ps f -C postgres
PID TTY STAT TIME COMMAND

Запустим сервер заново:

[student@ServerName ~]$ sudo systemctl start postgresql

Проверим целостность данных:

[student@ServerName ~]$ psql -c 'SELECT count(*) FROM bill'
count
---------
1000000
(1 row)

Данные сохранены, несмотря на аварийное завершение. Это и есть долговечность, обеспеченная журналом WAL.

Внимание!

Не используйте сигнал SIGKILL (9 сигнал) для завершения процессов PostgreSQL – это может привести к повреждению данных.

Номера транзакций​

Каждая транзакция уникально идентифицируется номером XID (transaction ID). XID выделяются последовательно и определяют «время» базы данных – меньшие номера соответствуют более ранним транзакциям, большие – более поздним.

Получим номер текущей транзакции:

student@student=> BEGIN;

BEGIN
student@student=*> SELECT pg_current_xact_id();

pg_current_xact_id
--------------------
800
(1 row)
student@student=*> COMMIT;

COMMIT

В обычном PostgreSQL XID – 32-разрядные, что приводит к проблеме зацикливания счетчика. В СУБД Pangolin счетчики транзакций сделаны 64-разрядными, при использовании которых зацикливание счетчика транзакций маловероятно. Такая разрядность счетчика сделана искусственно: на каждой странице данных записывается 32-битное смещение, которое, будучи добавлено к обычным номерам транзакций, и дает 64 разряда. В примере получен номер текущей транзакции. Для этого была использована функция pg_current_xact_id().

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

Версии строк​

Разные версии одной строки отличаются друг от друга с помощью двух специальных полей, которые находятся в заголовке строки:

  • xmin — номер транзакции, создавшую эту версию строки (новые версии строк создают INSERT и UPDATE);
  • xmax — номер транзакции, удалившей эту версию строки (удаляют версии UPDATE и DELETE).

Оба этих поля 32-разрядные. Иметь поля xmin и xmax 64-разрядными будет слишком накладно, поэтому в Pangolin и используется 32-битное смещение, записанное в специальное место на каждой странице отношения. Таким образом, на одной странице могут находиться версии строк, созданные или удаленные транзакциями с номерами, отличающимися не более чем на 232 транзакции.

Как работают операции:

  • INSERT создает строку, заполняет xmin (младшая часть XID);
  • UPDATE помечает старую версиюб: в xmax записывает номер транзакции и создает новую версию строки, в поле xmin которой стоит тот же номер;
  • DELETE записывает в xmax номер транзакции;

Удаленные версии строк физически не стираются до специальной очистки.

Удаленные версии строк отличаются наличием номера транзакции, удалившей их, в поле xmin. Эти удаленные версии строк могут достаточно долго находиться на страницах, так как они могут быть нужны каким-либо транзакциям. Если же эти версии строк не входят ни в один снимок данных, их следует очистить специальной очисткой, освободив место на странице для вставки новых версий строк. Об очистке будет рассказано далее.

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

Снимок данных – это не физическая копия данных, а набор чисел, определяющих, какие версии строк видимы в данный момент. Снимок содержит:

  • XID самой последней зафиксированной до создания снимка транзакции;
  • список XID всех уже активных на момент создания снимка транзакций.

Правила видимости:

  • изменения строк, произведенные в транзакции, видны в этой транзакции;
  • версии строк, созданные и не удаленные зафиксированными транзакциями с xmin < XID, видны в снимке;
  • версии строк, удаленные зафиксированными транзакциями с xmax < XID, не видны в снимке;
  • версии строк, созданные транзакциями, не зафиксированными на момент создания снимка, не видны;
  • версии строк, созданные транзакциями с xmin > XID (стартовавшими после создания снимка), относятся к будущему и из настоящей транзакции не видны.

Уровни изоляции транзакций​

Уровни изоляции транзакций определены стандартом SQL, но не все уровни реализованы в PostgreSQL.

Read Uncommitted, определенный стандартом SQL, не реализован в PostgreSQL, так как допускает «аномалию грязного чтения» — разрешает в других транзакциях видеть изменения, еще не зафиксированные текущей транзакцией.

Read Committed — уровень изоляции, принятый в PostgreSQL по умолчанию. На этом уровне перед каждой командой транзакции снимок строится заново. Соответственно изменения, произведенные зафиксированными другими транзакциями, будут видны в текущей транзакции.

Repeatable Read — на этом уровне изоляции снимок данных строится ровно один раз — перед первой командой транзакции, и вся транзакция работает с этим снимком. При работе на таком уровне изоляции вполне вероятны ошибки сериализации из-за параллельной работы других транзакций.

Serializable — полная изоляция транзакций друг от друга, один снимок на транзакцию. Если действие транзакций невозможно строго упорядочить, возникает ошибка сериализации.

Необходимость очистки​

Удаленные версии строк («мертвые» строки — dead tuples), не входящие ни в один снимок, должны очищаться. Иначе они просто расходуют место на диске и приводят к раздуванию размеров файлов хранения данных. Очистку осуществляют в PostgreSQL следующими способами:

  • Командой VACUUM стирает мертвые версии строк, освобождая место на страницах для вставки новых версий строк;
  • Демоном autovacuum — это набор фоновых процессов, выполняющих очистку автоматически по мере накопления изменений в базах данных.

По умолчанию autovacuum в PostgreSQL включен, и выключать его не следует даже в системах, ориентированных на архивную работу и не подразумевающих изменений данных.

Очистка не только очищает мертвые версии строк, но и строит слои fsm — карту свободного пространства, vm — карту видимости. Более того, очистка еще и отвечает за заморозку, предотвращая зацикливание счетчика транзакций.

Имеется также команда VACUUM FULL, она выполняет другую процедуру: полностью реорганизует страницы файла с данными, перенося их в новый файл данных уже без мертвых строк, уплотняя таким образом файл и уменьшая его размер.

Удаленные версии строк, видимые в каких-либо снимках, очищать нельзя.

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

Блокировки​

Помимо механизма MVCC, PostgreSQL также использует и блокировки.

Тип блокировки

Назначение

Блокировки строк

Чтение не блокирует строки, изменение блокирует строку для других изменений, но разрешает чтение

Блокировки страниц

Для служебных операций (например, для работы с индексами)

Блокировки таблиц

Запрещают изменение или удаление таблицы, могут запрещать чтение таблицы, например при ее перестроении командой VACUUM FULL

Блокировки объектов в памяти

Устанавливаются и снимаются автоматически, но можно управлять и вручную

PostgreSQL автоматически управляет блокировками, но есть инструменты для ручного управления ими.

Состояние транзакций​

Для отслеживания состояния транзакций в PostgreSQL существует массив XACT в памяти, где для каждой транзакции хранятся два бита:

  • признак фиксации (COMMIT);
  • признак отмены (ABORT/ROLLBACK).

Эта информация важна для определения видимости версий строк в снимках. Поэтому она должна быть защищена от сбоя, что делается с помощью журнала предзаписи WAL. Более того, после сбоя информация о статусе транзакций влияет на процедуры восстановления. Поэтому статус записывается на диск в каталог pg_xact. Эти данные называются журналом фиксации (commit log).

Если в XACT транзакция не помечена как зафиксированная, изменяемые ею строки не видны другим транзакциям.

Итоги​

  • Транзакции обеспечивают атомарность, согласованность, изоляцию и долговременность хранения;
  • Команды и транзакции работают со снимками данных, предоставляющими различные версии строк — MVCC;
  • Уровень изоляции транзакции определяет порядок построения снимков данных;
  • Реализованы три уровня изоляции транзакций;
  • Для определения видимости строк необходимо отслеживать состояние транзакций;
  • Кроме MVCC параллельную работу с данными обеспечивают блокировки.

Самопроверка​

Вопрос 1

Какой уровень изоляции транзакций используется по умолчанию в Pangolin?

Вопрос 2

Что такое снимок данных (snapshot)?

Вопрос 3

Что обозначает поле xmin заголовка строки?

Вопрос 4

Что такое точка сохранения (savepoint) в транзакции?