Блокировки

Для избегания гонок при получении ресурсов применяется механизм блокировок записи, существуют блокировки на таблицы, на страницы и на строки.

Захват блокировки возможен не всегда: ресурс может оказаться уже занятым кем-то другим. Тогда процесс либо встает в очередь ожидания (если механизм блокировки дает такую возможность), либо повторяет попытку захвата блокировки через определенное время. Так или иначе это приводит к тому, что процесс вынужден простаивать в ожидании освобождения ресурса.

По времени использования блокировки можно разделить на длительные и короткие

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

  • Краткосрочные блокировки захватываются на небольшое время (от нескольких инструкций процессора до долей секунд) и обычно относятся к структурам данных в общей памяти. Такими блокировками PostgreSQL управляет полностью автоматически — об их существовании надо просто знать.

Блокировки объектов (отношений)

Блокировки объектов располагаются в общей памяти сервера. Их количество ограничено произведением значений двух параметров: max_locks_per_transaction × max_connections.
Каждый из параметров задается при запуске приложения
Также, стоит заметить что это число только определяет размер пулла блокировок. То есть, подключение может превысить свой максимум, но проблемы начнутся, если число блокировок будет суммарно на всех больше чем пулл.

Все блокировки можно посмотреть в представлении pg_locks.

Если ресурс уже заблокирован в несовместимом режиме, транзакция, пытающаяся захватить этот ресурс, ставится в очередь и ожидает освобождения блокировки. Ожидающие транзакции не потребляют ресурсы процессора: соответствующие обслуживающие процессы «засыпают» и пробуждаются операционной системой при освобождении ресурса.

Виды блокировок:

  • relation - Блокировки отношений.
  • ransactionid и virtualxid - блокировка номера транзакции, удобно когда нужно дождаться выполнения какой либо транзакции
    • tuple - Блокировка версии строки. Используется в некоторых случаях для установки приоритета среди нескольких транзакций, ожидающих блокировку одной и той же строки.
    • extend Используется при добавлении страниц к файлу какого-либо отношения.
    • object Блокировка объектов, которые не являются отношениями (баз данных, схем, подписок и т. п.).
    • page Блокировка страницы, используется нечасто и только некоторыми типами индексов.
    • advisory Рекомендательная блокировка, устанавливается пользователем вручную.

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

режим блокировки AS RS RE SUE S SRE E AE пример SQL-команд
Access Share X SELECT
Row Share X X SELECT FOR UPDATE/SHARE
Row Exclusive X X X X INSERT, UPDATE, DELETE
Share Update Exclusive X X X X X VACUUM, ALTER TABLE*, СREATE INDEX CONCURRENTLY
Share X X X X X CREATE INDEX
Share Row Exclusive X X X X X X CREATE TRIGGER, ALTER TABLE*
Exclusive X X X X X X X REFRESH MAT. VIEW CONCURRENTLY
Access Exclusive X X X X X X X X DROP, TRUNCATE, VACUUM FULL, LOCK TABLE, ALTER TABLE*, REFRESH MAT. VIEW
  • Команда ALTER TABLE имеет много вариантов, разные из которых требуют разных уровней блокировки. Поэтому в матрице эта команда появляется в разных строках и отмечена звездочкой.

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

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

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

Для блокировок строк существуют 2 исключительных режима

  • Режим FOR UPDATE предполагает полное изменение (или удаление) строки.
  • Режим FOR NO KEY UPDATE — изменение только тех полей, которые не входят в уникальные индексы (иными словами, при таком изменении все внешние ключи остаются без изменений).

Еще два режима представляют разделяемые (shared) блокировки, которые могут удерживаться несколькими транзакциями.

  • Режим FOR SHARE применяется, когда нужно прочитать строку, но при этом нельзя допустить, чтобы она как-либо изменилась другой транзакцией.
  • Режим FOR KEY SHARE допускает изменение строки, но только неключевых полей. Этот режим, в частности, автоматически используется PostgreSQL при проверке внешних ключей.

Команда UPDATE сама выбирает минимальный подходящий режим блокировки; обычно строки блокируются в режиме FOR NO KEY UPDATE.

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

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

Вот так выглядит матрица режимов

режим FOR KEY SHARE FOR SHARE FOR NO KEY UPDATE FOR UPDATE
FOR KEY SHARE Х
FOR SHARE Х Х
FOR NO KEY UPDATE Х Х Х
FOR UPDATE Х Х Х Х

У нас может быть, что несколько транзакций блокируют, но поле xmax одно, для этого применяется термин мультитранзакций, это номер который может объединять несколько транзакций, в xmax вписывается номер мультитранзакции в случае если блокираторов несколько.

Откат в случае блокировки

Обычно команды SQL ожидают освобождения необходимых им ресурсов. Но иногда хочется отказаться от выполнения команды, если блокировку не удалось получить сразу же. Для этого такие команды, как SELECT, LOCK, ALTER, позволяют использовать фразу NOWAIT.

Например:

Блокируем:

=> BEGIN;=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;

Просим прочитать

|  => SELECT * FROM accounts FOR UPDATE NOWAIT;

Получаем ошибку

|  ERROR:  could not obtain lock on row in relation "accounts"

Команда немедленно завершается с ошибкой, если ресурс оказался занят. В прикладном коде такую ошибку можно перехватить и обработать.

У команд UPDATE и DELETE фразу NOWAIT указать нельзя, но можно сначала выполнить SELECT FOR UPDATE NOWAIT, а затем — если получилось — обновить или удалить строку.

Есть еще один вариант не ждать — использовать команду SELECT FOR с фразой SKIP LOCKED. Такая команда будет пропускать заблокированные строки, но обрабатывать свободные.l

Взаимоблокировки

Как было описано выше, блокировка отпускается только при завершении транзакции, в таком случае возможны ситуации взаимоблокировок.
Например, первая транзакция изменила запись Б и хочет изменить А, вторая транзакция изменила запись А и хочет изменить Б и они друг друга ждут.

Такие ситуации отслеживаются самой PostgreSQL, алгоритм такой
Когда процесс пытается захватить блокировку и не может, он встает в очередь и засыпает, но взводит таймер на значение, указанное в параметре deadlock_timeout (по умолчанию — 1 секунда). Если по истечении deadlock_timeout ожидание продолжается, тогда ожидающий процесс будет разбужен и инициирует проверку на deadlock.

Если проверка (которая состоит в построении графа ожиданий и поиска в нем контуров) не выявила взаимоблокировок, то процесс продолжает спать — теперь уже до победного конца.

параметр lock_timeout, который действует на любой оператор и позволяет избежать неопределенно долгого ожидания: если блокировку не удалось получить за указанное время, оператор завершается с ошибкой lock_not_available

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

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