Архитектура
Блокировки
12
Авторские права
© Postgres Professional, 2020 год.
Авторы: Егор Рогов, Павел Лузанов
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Общая информация о блокировках
Блокировки отношений и других объектов
Блокировки на уровне строк
3
Блокировки
Задача: упорядочение конкурентного доступа
к разделяемым ресурсам
Механизм
перед обращением к данным процесс захватывает блокировку
после обращения — освобождает (обычно в конце транзакции)
несовместимые блокировки приводят к очередям
Блокировки используются, чтобы упорядочить конкурентный доступ
к разделяемым ресурсам.
Под конкурентным доступом понимается одновременный доступ
нескольких процессов. Сами процессы могут выполняться как
параллельно (если позволяет аппаратура), так и последовательно
в режиме разделения времени.
Блокировки не нужны, если нет конкуренции (одновременно к данным
обращается только один процесс) или если нет разделяемого ресурса
(например, общий буферный кеш нуждается в блокировках,
а локальный — нет).
Перед тем, как обратиться к ресурсу, защищенному блокировкой,
процесс должен захватить эту блокировку. После того, как ресурс
больше не нужен процессу, он освобождает блокировку, чтобы
ресурсом могли воспользоваться другие процессы.
Захват блокировки возможен не всегда: ресурс может оказаться уже
занятым кем-то другим в несовместимом режиме. В простом случае
используют два режима: исключительный (несовместим ни с чем) и
разделяемый (совместим сам с собой). Но их может быть и больше,
в этом случае совместимость определяется матрицей.
Если ресурс занят, процесс должен встать в очередь ожидания. Это,
конечно, уменьшает производительность системы. Поэтому для
создания высокоэффективных приложений важно понимать механизм
блокировок.
4
Блокировки объектов
Блокировки номера транзакции
Блокировки отношений
Очередь ожидания
Из всего многообразия блокировок на уровне объектов мы рассмотрим
только блокировки номера транзакции и блокировки отношений.
Еще один вид блокировок того же типа – рекомендательные
блокировки – рассматривается в курсе DEV1.
5
Номер транзакции
Типы ресурсов в pg_locks
virtualxid — виртуальный номер транзакции
transactionid — номер транзакции
Режимы
исключительный
разделяемый
Транзакции удерживают исключительную блокировку
собственного номера
способ дождаться завершения транзакции
Рассмотрение блокировок на уровне объектов («обычные», «тяжелые»
блокировки) мы начнем с блокировок номера транзакции.
Блокировки объектов располагаются в общей памяти сервера, поэтому
их общее количество ограничено. Все такие блокировки можно
посмотреть в представлении pg_locks: они устроены одинаково и
отличаются только типом ресурса и режимами блокирования.
Блокировки номера транзакции отображаются в pg_locks с типами
ресурса Transactionid и Virtualxid.
Каждой транзакции при старте назначается сначала виртуальный
номер, который используется, пока транзакция только читает данные.
Это сделано для оптимизации. Виртуальный номер никак не
учитывается в правилах видимости. Кроме того, виртуальный номер не
обязан быть полностью уникальным (аналогично номеру процесса
в Unix) и его можно выдать быстро, без обращения к общей памяти.
Настоящий номер присваивается в тот момент, когда транзакция
изменяет какие-либо данные. Реальный уникальный номер нужен,
чтобы отслеживать статус транзакции (зафиксирована или оборвана);
такой номер можно записывать на диск в полях заголовка версий строк.
Каждая транзакция всегда сама удерживает исключительную
блокировку своего собственного номера (и виртуального, и — если
есть — реального). Это дает простой способ дождаться окончания
какой-либо транзакции: надо запросить блокировку ее номера.
Блокировку получить не удастся, и процесс будет разбужен только
когда блокировка освободится, а произойдет это при завершении
транзакции.
7
Блокировки отношений
Тип ресурса в pg_locks
relation — таблицы, индексы и т. п.
Режимы
Access Share SELECT
Row Share SELECT FOR UPDATE/SHARE
Row Exclusive UPDATE, DELETE, INSERT
Share Update Exclusive VACUUM, ALTER TABLE,
СREATE INDEX CONCURRENTLY
Share CREATE INDEX
Share Row Exclusive CREATE TRIGGER, ALTER TABLE
Exclusive REFRESH MAT. VIEW CONCURRENTLY
Access Exclusive DROP, TRUNCATE, VACUUM FULL,
LOCK TABLE, ALTER TABLE, REFRESH MAT. VIEW
допускают
параллельное
изменение
данных
в таблице
Второй важный случай блокировок объектов — блокировки отношений
(таблиц, индексов, последовательностей и т. п.). Такие блокировки
имеют тип Relation в pg_locks.
Для них определено целых 8 различных режимов, которые показаны на
слайде вместе с примерами команд SQL, которые используют эти
режимы. Матрица совместимости здесь не показана из-за ее большого
размера, но она приведена в документации:
Такое количество режимов существуют для того, чтобы позволить
выполнять одновременно как можно большее количество команд,
относящихся к одной таблице (индексу и т. п.).
Самый слабый режим — Access Share, он захватывается командой
SELECT и совместим с любым режимом, кроме самого сильного —
Access Exclusive. Это означает, что запрос не мешает ни другим
запросам, ни изменению данных в таблице, ни чему-либо другому, но не
дает, например, удалить таблицу в то время, как из нее читаются
данные.
Другой пример: режим Share (как и другие более сильные режимы) не
совместимы с изменением данных в таблице. Например, команда
CREATE INDEX заблокирует команды INSERT, UPDATE и DELETE
(и наоборот). Поэтому есть команда CREATE INDEX CONCURRENTLY,
использующая режим Share Update Exclusive, который совместим
с такими изменениями (за счет этого команда выполняется дольше).
8
Очередь ожидания
Честная очередь для несовместимых режимов
T
102
T
103
T
104
T
101
Row Exclusive
UPDATE
Access Exclusive
VACUUM FULL
Share
CREATE INDEX
Access Share
SELECT
Блокировки объектов предоставляют «честную» очередь ожидания. Это
означает, что операции с несовместимыми режимами выстраиваются
в очередь и вне очереди никто не проходит.
Например, на рисунке выполняется команда UPDATE. Операция
CREATE INDEX встает в очередь, поскольку использует несовместимый
режим. Если следом придет команда VACUUM FULL (использующая
самый сильный режим, не совместимый ни с чем), она встанет
в очередь за CREATE INDEX.
Обычный запрос SELECT, хоть и совместим с выполняющейся сейчас
командой UPDATE, тоже честно встанет в очередь за VACUUM FULL.
На рисунке транзакции показаны кружками, сплошные стрелки
обозначают захваченную блокировку, а пунктирные стрелки — попытки
захватить блокировку, уже занятую в несовместимом режиме.
9
Очередь ожидания
Честная очередь для несовместимых режимов
T
102
T
103
T
104
Access Exclusive
VACUUM FULL
Share
CREATE INDEX
Access Share
SELECT
После того, как первая транзакция завершается, блокировку
захватывает следующая транзакция, стоящая в очереди.
11
Блокировки строк
Разделяемые и исключительные блокировки
«Нечестная» очередь ожидания
12
Блокировки строк
Используются совместно с многоверсионностью,
для чтения данных не требуются
Информация только в страницах данных
в оперативной памяти ничего не хранится
Неограниченное количество
большое число не влияет на производительность
Инфраструктура
очередь ожидания организована с помощью блокировок объектов
Благодаря многоверсионности, блокировки уровня строк нужны только
при изменении данных (или для того, чтобы не допустить изменения),
но не нужны при обычном чтении.
Если в случае блокировок объектов каждый ресурс представлен
собственной блокировкой в оперативной памяти, то со строками так не
получается: отдельная блокировка для каждой табличной строки
оторых могут быть миллионы и миллиарды) потребует непомерных
накладных расходов и огромного объема оперативной памяти.
Один известный вариант решения состоит в повышении уровня: если
уже заблокировано N строк таблицы, то при блокировке еще одной
блокируется вся таблица целиком, а блокировки на уровне строк
снимаются. Но в этом случае страдает пропускная способность.
Поэтому в PostgreSQL сделано иначе. Информация о том, что строка
заблокирована, хранится только и исключительно в версии строки
внутри страницы данных. Там она представлена номером блокирующей
транзакции (xmax) и дополнительными информационными битами.
За счет этого может быть неограниченное количество блокировок
уровня строки. Это не приводит к потреблению каких-либо ресурсов
и не снижает производительность системы.
Обратная сторона такого подхода — сложность организации очереди
ожидания. Для этого все-таки приходится использовать блокировки
уровня объектов, но удается обойтись очень небольшим их
количеством (пропорциональным числу процессов, а не числу
заблокированных строк).
13
Блокировки строк
Режимы
Update — удаление строки или изменение всех полей
No Key Update — изменение любых полей, кроме ключевых
Share — запрет изменения любых полей строки
Key Share — запрет изменения ключевых полей строки
Key No Key
Share Share Update Update
Key Share
Share
No Key Update
Update
исключительные
разделяемые
Всего существует 4 режима, в которых можно заблокировать строку
(в версии строки режим проставляется с помощью дополнительных
информационных битов).
Два режима представляют исключительные (exclusive) блокировки,
которые одновременно может удерживать только одна транзакция.
Режим UPDATE предполагает полное изменение (или удаление) строки,
а режим NO KEY UPDATE — изменение только тех полей, которые не
входят в уникальные индексы (иными словами, при таком изменении
все внешние ключи остаются без изменений). Команда UPDATE сама
выбирает минимальный подходящий режим блокировки; обычно строки
блокируются в режиме NO KEY UPDATE.
Еще два режима представляют разделяемые (shared) блокировки,
которые могут удерживаться несколькими транзакциями.
Режим SHARE применяется, когда нужно прочитать строку, но при этом
нельзя допустить, чтобы она как-либо изменилась другой транзакцией.
Режим KEY SHARE допускает изменение строки, но только неключевых
полей. Этот режим, в частности, автоматически используется
PostgreSQL при проверке внешних ключей.
Напомним, что обычное чтение (SELECT) вообще не блокирует строки.
Общая матрица совместимости режимов приведена внизу слайда.
Из нее видно, что разделяемый режим KEY SHARE совместим
с исключительным режимом NO KEY UPDATE (то есть можно обновлять
неключевые поля и быть уверенным в том, что ключ не изменится).
15
Очередь ожидания
Порядок действий
1. захватить исключительную блокировку типа tuple
нужной версии строки
2. если версия строки занята, дождаться ее освобождения
3. записать номер своей транзакции в xmax
4. освободить блокировку типа tuple
xmaxxmin данные
100 . . .
T
101
101
Чтобы заблокировать строку, транзакция выполняет следующие
действия:
1. Захватывает исключительную блокировку типа tuple для
интересующей версии строки.
2. Если xmax и информационные биты говорят о том, что строка
заблокирована в конфликтующем режиме, то ждет освобождения
строки.
3. Прописывает свой xmax и необходимые информационные биты
в версию строки, таким образом блокируя ее.
4. Освобождает блокировку tuple версии строки.
Разберем этот процесс.
В примере, приведенном на слайде, транзакция 101 захватила
блокировку tuple, затем прописала свой номер в версии строки и тут же
освободила блокировку tuple.
16
Очередь ожидания
xmaxxmin данные
100 . . .101
T
101
T
102
Транзакция 102 захватила блокировку tuple для первой версии строки
(п. 1), но затем (п. 2) обнаружила, что версия строки уже заблокирована
транзакцией 101.
Теперь ей надо дождаться, пока строка освободится, но как это
сделать? Для этого используется своего рода трюк. Как мы уже
говорили, каждая транзакция удерживает исключительную блокировку
своего номера. Поскольку транзакция 102 фактически должна
дождаться завершения транзакции 101 (ведь блокировка строки
освобождается только при завершении транзакции), она запрашивает
блокировку номера 101.
Обратите внимание, что блокировка tuple при этом остается, так как
снимается только в п. 3, до которого дело еще не дошло.
17
xmaxxmin данные
Очередь ожидания
100 . . .101
T
101
T
103
T
104
T
102
Если появляются другие транзакции, конфликтующие с текущей
блокировкой строки, первым делом они пытаются захватить блокировку
типа tuple для этой строки — и выстраиваются в очередь.
В нашем примере, поскольку блокировка tuple уже удерживается
транзакцией 102, транзакции 103, 104 ждут освобождения этой
блокировки.
(Заметим, что транзакции, желающие получить строку в совместимом
режиме, проходит без очереди. Обычно это не представляет проблемы,
поскольку разделяемые блокировки строк используются по умолчанию
только при проверке внешних ключей.)
18
Очередь ожидания
101 . . .
xmaxxmin данные
100 . . .101
102
T
101
T
103
T
104
T
102
Получается двухуровневая очередь. Одна из транзакций (102
в примере) — «крайняя», и за ней — все остальные.
Когда транзакция 101 завершится, именно транзакция 102 получит
возможность первой записать свой номер в поле xmax, после чего она
освободит блокировку tuple.
Но дело в том, что при фиксации транзакции 101 блокировка tuple
первой версии не просто освобождается — она становится ненужной,
ведь теперь актуальной является уже вторая версия строки.
(При обрыве транзакции 101 это не так, но обрывы происходят
значительно реже.)
Поэтому все транзакции, стоявшие ранее в очереди, превращаются
в «толпу» и выстраиваются непосредственно за транзакцией 102.
Когда транзакция 102 освободит версию строки, среди ожидающих
транзакций возникает гонка за право захватить блокировку.
Если бы не двухуровневая схема блокирования, то такая гонка со
случайным победителем возникала бы всегда. Это могло бы приводить
к ситуации вечного ожидания «невезучей» транзакции. Наличие
блокировки типа tuple решает — хотя бы отчасти — эту проблему.
Стоит избегать проектных решений, которые предполагают массовые
изменения одной и той же строки. В этом случае возникает «горячая
точка», которая на высоких нагрузках может привести к снижению
производительности.
20
Итоги
Блокировки отношений и других объектов БД используются
для организации конкурентного доступа к общим ресурсам
хранятся в разделяемой памяти сервера
имеется механизм очередей
Блокировки строк реализованы иначе
хранятся в страницах данных из-за потенциально большого количества
используют блокировки уровня объектов для организации очереди
21
Практика
1. Какие блокировки на уровне изоляции Read Committed
удерживает транзакция, прочитавшая одну строку таблицы
по первичному ключу? Проверьте на практике.
2. Посмотрите, как в представлении pg_locks отображаются
рекомендательные блокировки.
3. Убедитесь на практике, что проверка внешнего ключа
и обновление строки могут выполняться одновременно.
Изучите возникающие при этом блокировки уровня строки.
4. Воспроизведите ситуацию взаимоблокировки двух
транзакций и проверьте, как она обрабатывается сервером.
2. Рекомендательные блокировки рассматриваются в курсе DEV1.
Посмотрите все столбцы представления pg_locks чтобы определить,
в каком из них отображается идентификатор ресурса.
3. Для этого потребуется создать две таблицы, связанные
ограничением внешнего ключа.
Для анализа блокировок используйте расширение pgrowlocks.
4. Взаимоблокировка двух транзакций возникает, когда
- первая транзакция удерживает блокировку объектов, необходимых
второй транзакции для продолжения работы,
- а вторая транзакция удерживает блокировку объектов, необходимых
первой транзакции для продолжения работы.
В общем случае может произойти взаимоблокировка более двух
транзакций.