Архитектура
Блокировки
16
Авторские права
© Postgres Professional, 2017–2024
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Общая информация о блокировках
Блокировки отношений и других объектов
Блокировки на уровне строк
3
Блокировки
Задача: упорядочение конкурентного доступа
к разделяемым ресурсам
Механизм
перед обращением к данным процесс захватывает блокировку
после обращения — освобождает (обычно в конце транзакции)
несовместимые блокировки приводят к очередям
Блокировки используются, чтобы упорядочить конкурентный доступ
к разделяемым ресурсам.
Под конкурентным доступом понимается одновременный доступ
нескольких процессов. Сами процессы могут выполняться как
параллельно (если позволяет аппаратура), так и последовательно
в режиме разделения времени.
Блокировки не нужны, если нет конкуренции (одновременно к данным
обращается только один процесс) или если нет разделяемого ресурса
(например, общий буферный кеш нуждается в блокировках,
а локальный — нет).
Перед тем, как обратиться к ресурсу, защищенному блокировкой,
процесс должен захватить эту блокировку. Когда процесс перестает
нуждаться в ресурсе, он освобождает блокировку, чтобы ресурсом
могли воспользоваться другие процессы.
Захват блокировки возможен не всегда: ресурс может оказаться уже
занятым кем-то другим в несовместимом режиме. В простом случае
используют два режима: исключительный (несовместим ни с чем)
и разделяемый (совместим сам с собой). Но их может быть и больше,
в этом случае совместимость определяется матрицей.
Если ресурс занят, процесс должен встать в очередь ожидания.
Это, конечно, уменьшает производительность системы. Поэтому для
создания высокоэффективных приложений важно понимать, как
работает механизм блокировок.
4
Блокировки объектов
Блокировки номера транзакции
Блокировки отношений
Очередь ожидания
Из всего многообразия блокировок на уровне объектов мы рассмотрим
только блокировки номера транзакции и блокировки отношений.
Еще один вид блокировок этого же типа — рекомендательные
блокировки — упоминался в теме «Обработка ошибок» курса DEV1.
5
Номер транзакции
Типы ресурсов в pg_locks
virtualxid — виртуальный номер транзакции
transactionid — настоящий номер транзакции
Режимы
исключительный
разделяемый
Транзакция удерживает исключительную блокировку
собственного номера
способ дождаться завершения транзакции
Рассмотрение блокировок на уровне объектов («обычные», «тяжелые»
блокировки) мы начнем с блокировок номера транзакции.
Блокировки объектов хранятся в общей памяти сервера, поэтому
их количество ограничено. Все такие блокировки можно посмотреть
в представлении pg_locks: они устроены одинаково и отличаются
только типами ресурсов и режимами блокирования. Номерам
транзакций соответствуют типы ресурсов Transactionid и Virtualxid.
Каждой транзакции при старте назначается сначала виртуальный
номер, который используется, пока транзакция только читает данные.
Это сделано для экономии настоящих номеров — виртуальный номер
никак не учитывается в правилах видимости. Его можно выдать быстро,
без обращения к общей памяти (требуется лишь уникальность номеров
всех активных транзакций).
Уникальный настоящий номер выдается счетчиком XID в тот момент,
когда транзакция изменяет какие-либо данные. Он нужен, чтобы
отслеживать статус пишущей транзакции (активна, зафиксирована
или оборвана); такой номер можно записывать в поля xmin и xmax
заголовка версий строк.
Каждая транзакция удерживает исключительную блокировку своих
номеров: и виртуального, и настоящего, если он есть. Это дает простой
способ дождаться окончания какой-либо транзакции: надо запросить
блокировку ее номера. Если блокировку сразу получить не удастся,
то запрашивающий процесс заснет и будет разбужен только по
завершении транзакции — когда блокировка освободится.
7
Блокировки отношений
Тип ресурса в pg_locks
relation — таблицы, индексы и т. п.
Режимы
Access Share SELECT
Row Share SELECT FOR UPDATE/SHARE
Row Exclusive UPDATE, DELETE, INSERT, MERGE
Share Update Exclusive VACUUM, ALTER TABLE,
CREATE INDEX CONCURRENTLY
Share CREATE INDEX
Share Row Exclusive CREATE TRIGGER, ALTER TABLE
Exclusive REFRESH MAT. VIEW CONCURRENTLY
Access Exclusive DROP, TRUNCATE, REINDEX, 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 и
MERGE (и наоборот). Поэтому существует команда CREATE INDEX
CONCURRENTLY, использующая режим Share Update Exclusive, который
совместим с такими изменениями (за счет этого команда выполняется
дольше).
8
Очередь ожидания
отношение
T 100
UPDATE
Row Exclusive
T 110
CREATE INDEX
Share
несовместимая
блокировка
T 120
VACUUM FULL
Access Exclusive
T 130
SELECT
Access Share
Блокировки объектов используют «честную» очередь ожидания.
Это означает, что запросы блокировок обслуживаются один за другим
в порядке их поступления.
Пусть, как на рисунке, транзакция T100 выполняет команду UPDATE,
при этом на отношение будет установлена блокировка с режимом
Row Exclusive.
Транзакция T110, выполняющая CREATE INDEX, встает в очередь
за T100, поскольку запрашивает режим Share, не совместимый с Row
Exclusive. Если следом придет транзакция T120 с командой VACUUM
FULL (ей нужен режим Access Exclusive, не совместимый ни с каким
другим), она встанет в очередь за CREATE INDEX.
А еще одна транзакция с обычным запросом SELECT (требует Access
Share, совместимый с выполняющейся сейчас командой UPDATE) тоже
честно займет очередь за VACUUM FULL.
9
Очередь ожидания
отношение
T 110T 120
T 130
CREATE INDEX
Share
SELECT
Access Share
VACUUM FULL
Access Exclusive
После того как первая транзакция завершается, блокировку
захватывает следующая транзакция, стоящая в очереди.
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
«Очередь» ожидания
xid 101
T 101
Share
удерживает
разделяемую
блокировку
xmin xmax
100 ...101
(0,1)
Для эффективной организации очереди ожидания используется
механизм блокировок объектов, при этом информация хранится
в оперативной памяти и видна в pg_locks.
Допустим, транзакции с номером 101 удалось заблокировать строку
в разделяемом режиме Share. Информация о блокировке записывается
в заголовок версии строки — в поле xmax появляется номер этой
транзакции.
16
«Очередь» ожидания
T 102
tuple 0,1
Update
xid 101
T 101
Share
ждет
завершения
Т 101
xmin xmax
100 ...101
(0,1)
Другая транзакция 102, желая получить блокировку, обращается
к строке и видит, что строка уже заблокирована. Если режимы
блокировок конфликтуют, транзакция 102 должна встать в очередь,
чтобы система «разбудила» ее, когда блокировка освободится.
Поскольку каждая транзакция удерживает исключительную блокировку
своего номера, транзакция 102 запрашивает блокировку номера
транзакции 101, чтобы дождаться ее завершения.
Кроме того, ожидающая транзакция удерживает блокировку условного
ресурса типа tuple (версия строки), показывая, что она первая
в «очереди».
17
«Очередь» ожидания
T 102
T 122
T 112
T 132
tuple 0,1
Update
Update
Update
Update
xid 101
T 101
Share
первый
в очереди стоит
в тамбуре
xmin xmax
100 ...101
(0,1)
Если появляются другие транзакции, конфликтующие с текущей
блокировкой версии строки (в нашем примере — 112, 122, 132), первым
делом они пытаются захватить блокировку типа tuple для этой версии.
Поскольку блокировка tuple уже удерживается транзакцией 102, другие
транзакции ждут освобождения этой блокировки. Получается
своеобразная «очередь», в которой есть первый и все остальные.
Если бы все прибывающие транзакции занимали очередь
непосредственно за транзакцией 101, при освобождении блокировки
возникала бы ситуация гонки. При неудачном стечении обстоятельств
транзакция 102 могла бы ждать блокировку вечно. Двухуровневая
схема блокирования немного упорядочивает подобную конкуренцию.
Стоит избегать проектных решений, которые предполагают массовые
изменения одной и той же строки. В этом случае возникает «горячая
точка», которая на высоких нагрузках может привести к снижению
производительности.
18
«Очередь» ожидания
T 102
xmin xmax
100 ...
(0,1)
T 122
T 101
T 112
T 132
tuple 0,1 xid 101
Share
Update
Update
Update
Update
T 133
Share
«мне только
спросить»
101 Share
133 Share
В нашем примере транзакция 101 заблокировала строку в разделяемом
(Share) режиме. Пока транзакция 102 ожидает завершения транзакции
101, может появиться еще транзакция 133, желающая получить
блокировку строки в разделяемом режиме, совместимом с текущей
блокировкой транзакции 101.
Такая транзакция не стоит в очереди, она сразу получает блокировку.
В этом случае список номеров блокирующих транзакций будет вынесен
в мультитранзакцию, а в поле xmax строки запишется номер этой
мультитранзакции.
19
«Очередь» ожидания
T 102
T 122
T 133
T 112
T 132
tuple 0,1
Share
Update
Update
Update
Update
xid 133
теперь ждет
завершения
Т 133
xmin xmax
100 ...133
(0,1)
Когда транзакция 101 завершится, транзакция 102 будет разбужена,
но не сможет захватить блокировку строки, поскольку строка все еще
заблокирована транзакцией 133. Транзакция 102 будет вынуждена
снова «заснуть».
PostgreSQL использует разделяемые блокировки для внутренних нужд,
в частности, при проверке внешних ключей. В прикладном коде можно
явно запросить разделяемую блокировку с помощью команды
SELECT … FOR SHARE или SELECT ... FOR KEY SHARE.
Однако следует помнить, что при постоянном потоке разделяемых
блокировок пишущая транзакция в худшем случае может ждать своей
очереди бесконечно. Эта ситуация называется по-английски locker
starvation.
Заметим, что такой проблемы в принципе не возникает при блокировках
объектов (таких, как отношения). В этом случае каждый ресурс
представлен собственной блокировкой в оперативной памяти и все
ждущие процессы выстраиваются в «честную» очередь.
20
«Очередь» ожидания
T 102
Update
xid 102
T 122
T 132
Update
Update
T 112
Update
tuple 0,1
xmin xmax
100 ...102
(0,1)
удерживает
исключительную
блокировку
Когда транзакция 133 завершится, транзакция 102 получит возможность
первой записать свой номер в поле xmax, после чего она освободит
блокировку tuple. Тогда одна случайная транзакция из всех остальных
успеет захватить блокировку tuple и станет первой в «очереди».
22
Итоги
Блокировки отношений и других объектов БД используются
для организации конкурентного доступа к общим ресурсам
хранятся в разделяемой памяти сервера
имеется механизм очередей
Блокировки строк реализованы иначе
хранятся в страницах данных из-за потенциально большого количества
используют блокировки уровня объектов для организации «очереди»
23
Практика
1. Какие блокировки на уровне изоляции Read Committed
удерживает транзакция, прочитавшая одну строку таблицы
по первичному ключу? Проверьте на практике.
2. Посмотрите, как в представлении pg_locks отображаются
рекомендательные блокировки.
3. Убедитесь на практике, что проверка внешнего ключа
и обновление строки могут выполняться одновременно.
Изучите возникающие при этом блокировки уровня строки.
4. Воспроизведите ситуацию взаимоблокировки двух
транзакций и проверьте, как она обрабатывается сервером.
2. Рекомендательные блокировки упоминались в теме «Обработка
ошибок» курса DEV1.
Посмотрите все столбцы представления pg_locks, чтобы определить,
в каком из них отображается идентификатор ресурса.
3. Для этого потребуется создать две таблицы, связанные
ограничением внешнего ключа.
Для анализа блокировок используйте расширение pgrowlocks.
4. Взаимоблокировка двух транзакций возникает, когда
первая транзакция удерживает блокировку объектов, необходимых
второй транзакции для продолжения работы,
вторая транзакция удерживает блокировку объектов, необходимых
первой транзакции для продолжения работы.
В общем случае может произойти взаимоблокировка более чем двух
транзакций.