Блокировки
Блокировки строк
16
Авторские права
© Postgres Professional, 20162025
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Исключительные и разделяемые блокировки строк
Мультитранзакции и заморозка
Реализация очереди ожидания
Взаимоблокировки
3
Блокировки строк
блокировки
на уровне
строк
предикатные
блокировки
блокировки
на уровне
объектов
блокировки
на уровне
отношений
блокировки
в оперативной
памяти
Несмотря на то, что информация о блокировках строк хранится
в версиях строк на диске, очередь ожидания для исключительных
блокировок строк организована с помощью блокировок объектов tuple.
4
Устройство
Информация только в страницах данных
поле xmax заголовка версии строки + информационные биты
в оперативной памяти ничего не хранится
Неограниченное количество
большое число не влияет на производительность
Инфраструктура
«очередь» ожидания организована с помощью блокировок объектов
Благодаря многоверсионности, блокировки уровня строк нужны только
при изменении данных (или для того, чтобы не допустить изменения)
и не нужны при обычном чтении.
Если в случае блокировок объектов каждый ресурс представлен
собственной блокировкой в оперативной памяти, то со строками так
не получается: отдельная блокировка для каждой табличной строки
(которых могут быть миллионы и миллиарды) потребует непомерных
накладных расходов и огромного объема оперативной памяти.
Один из вариантов решения — повышение уровня (эскалация): если
уже заблокировано N строк таблицы, то при блокировке еще одной
блокируется вся таблица целиком, а блокировки на уровне строк
снимаются. Но в этом случае страдает пропускная способность.
Поэтому в PostgreSQL сделано иначе. Информация о том, что строка
заблокирована, хранится исключительно в заголовке версии строки
внутри страницы данных. Там она представлена номером блокирующей
транзакции (xmax) и дополнительными информационными битами.
За счет этого можно установить неограниченное количество блокировок
уровня строки, и это не требует дополнительных ресурсов и не снижает
производительность системы.
Обратная сторона такого подхода — сложность организации очереди
ожидания. Для этого все-таки приходится использовать блокировки
уровня объектов, но удается обойтись очень небольшим их
количеством (пропорциональным числу процессов, а не числу
заблокированных строк).
5
Исключительные режимы
Update No Key Update
удаление строки изменение любых полей,
или изменение всех полей кроме ключевых
SELECT FOR UPDATE SELECT FOR NO KEY UPDATE
UPDATE UPDATE
(с изменением ключевых полей) (без изменения ключевых полей)
DELETE
Всего существует 4 режима, в которых можно заблокировать строку
(в версии строки режим проставляется с помощью дополнительных
информационных битов).
Два режима представляют исключительные (exclusive) блокировки,
которые одновременно может удерживать только одна транзакция.
Режим Update предполагает полное изменение (или удаление) строки,
а режим No Key Update — изменение только тех полей, которые не
входят в уникальные индексы (иными словами, при таком изменении
все внешние ключи остаются без изменений).
Команда UPDATE сама выбирает минимальный подходящий режим
блокировки; обычно строки блокируются в режиме No Key Update.
6
xmin committed
xmin aborted
xmax committed
xmax aborted
t
xmin xmax данные
100 42, FOO(0,1)
статус
normal 101
номер
блокирующей
транзакции
Исключительные режимы
xmax lock only
keys updated
изменились
значения ключевых
полей?
SELECT
FOR …
UPDATE
При изменении или удалении строки в поле xmax актуальной версии
записывается номер текущей транзакции. Установленное значение
xmax, соответствующее активной транзакции, выступает в качестве
блокировки.
То же самое происходит и при явном блокировании строки командой
SELECT FOR UPDATE, но еще проставляется дополнительный
информационный бит (xmax lock only), который говорит о том, что
версия строки по-прежнему актуальна, хоть и заблокирована.
Режим блокировки определяется еще одним информационным битом
(keys updated).
(На самом деле используется большее количество битов с более
сложными условиями и проверками, что связано с поддержкой
совместимости с предыдущими версиями. Но это не принципиально.)
Если другая транзакция намерена обновить или удалить
заблокированную строку в несовместимом режиме, она будет
вынуждена дождаться завершения транзакции с номером xmax.
7
Разделяемые режимы
Share Key Share
запрет изменения запрет изменения
любых полей строки ключевых полей строки
SELECT FOR SHARE SELECT FOR KEY SHARE
и проверка внешних ключей
Матрица совместимости режимов
Key No Key
Share Share Update Update
Key Share ×
Share × ×
No Key Update × × ×
Update × × × ×
Еще два режима представляют разделяемые (shared) блокировки,
которые могут удерживаться несколькими транзакциями.
Режим Share применяется, когда нужно прочитать строку, но при этом
нельзя допустить, чтобы она как-либо изменилась другой транзакцией.
Режим Key Share допускает изменение строки, но только неключевых
полей. Этот режим, в частности, автоматически используется
PostgreSQL при проверке внешних ключей.
Общая матрица совместимости режимов приведена внизу слайда.
Из нее видно, что:
исключительные режимы конфликтуют между собой;
разделяемые режимы совместимы между собой;
разделяемый режим Key Share совместим с исключительным
режимом No Key Update (то есть можно обновлять неключевые поля
и быть уверенным в том, что ключ не изменится).
8
xmin committed
xmin aborted
xmax committed
xmax aborted
t
Разделяемые режимы
xmin xmax данные
100 42,FOO(0,1)
статус
normal 1
номер
мультитранзакции
(multixact)
xmax is multi
101
Key Share
102
No Key Update
PGDATA/pg_multixact/
t
Мы говорили о том, что блокировка представляется номером
блокирующей транзакции в поле xmax. Разделяемые блокировки могут
удерживаться несколькими транзакциями, но в одно поле xmax нельзя
записать несколько номеров.
Поэтому для разделяемых блокировок применяются так называемые
мультитранзакции (MultiXact). Им выделяются отдельные номера,
которые соответствуют не одной транзакции, а целой группе. Чтобы
отличить мультитранзакцию от обычной, используется еще один
информационный бит (xmax is multi), а детальная информация об
участниках такой группы и режимах блокировки находится в каталоге
PGDATA/pg_multixact/. Естественно, последние использованные данные
хранятся в буферах в общей памяти сервера для ускорения доступа.
9
Настройка заморозки
Параметры для мультитранзакций
vacuum_multixact_freeze_min_age = 5 000 000
vacuum_multixact_freeze_table_age = 150 000 000
autovacuum_multixact_freeze_max_age = 400 000 000
vacuum_multixact_failsafe_age = 1 600 000 000
Параметры хранения таблиц
autovacuum_multixact_freeze_min_age
toast.autovacuum_multixact_freeze_min_age
autovacuum_multixact_freeze_table_age
toast.autovacuum_multixact_freeze_table_age
autovacuum_multixact_freeze_max_age
toast.autovacuum_multixact_freeze_max_age
Поскольку для мультитранзакций выделяются отдельные номера,
которые записываются в поле xmax версий строк, из-за ограничения
разрядности счетчика с ними возникают такие же сложности, как и
с обычным номером транзакции. Речь идет о переполнении счетчика
транзакций, которое рассматривалось в теме «Заморозка» модуля
«Многоверсионность».
Поэтому для номеров мультитранзакций тоже необходимо выполнять
аналог заморозки — старые номера multixact id заменяются на новые
(или на обычный номер, если в текущий момент блокировка уже
удерживается только одной транзакцией).
Заметим, что обычная заморозка версий строк выполняется для поля
xmin (если у версии строки непустое поле xmax, то либо это уже
неактуальная версия и она будет очищена, либо транзакция xmax
отменена и ее номер нас не интересует). А для мультитранзакций речь
идет о поле xmax актуальной версии строки, которая может оставаться
актуальной, но при этом постоянно блокироваться разными
транзакциями в разделяемом режиме.
За заморозку мультитранзакций отвечают параметры, аналогичные
параметрам обычной заморозки.
11
«Очередь» ожидания
xmin xmax
100 ...101(0,1)
xid 101
T 101
Share
удерживает
разделяемую
блокировку
Для организации очереди ожидания дополнительно используется
механизм блокировок объектов, при этом информация хранится
в оперативной памяти и видна в pg_locks.
Допустим, транзакции с номером 101 (пример на слайде) удалось
заблокировать строку в разделяемом режиме Share. Информация
о блокировке записывается в заголовок версии строки — в поле xmax
появляется номер этой транзакции.
12
«Очередь» ожидания
T 102
xmin xmax
100 ...101(0,1)
tuple 0,1
Update
xid 101
T 101
Share
ждет
завершения
Т 101
Другая транзакция 102, желая получить блокировку, обращается
к строке и видит, что строка заблокирована. Если режимы блокировок
конфликтуют, она должна каким-то образом встать в очередь, чтобы
система «разбудила» ее, когда блокировка освободится. Но блокировки
на уровне строк не предоставляют такой возможности — они никак не
представлены в оперативной памяти, это просто байты внутри
страницы данных.
Как мы видели в теме «Блокировки объектов», каждая транзакция
удерживает исключительную блокировку своего номера. Поскольку
транзакция 102 фактически должна дождаться завершения транзакции
101 (ведь блокировка строки освобождается только при завершении
транзакции), она запрашивает блокировку номера 101.
Когда транзакция 101 завершится, заблокированный ресурс
освободится (при фиксации — просто исчезнет), транзакция 102 будет
разбужена и сама сможет заблокировать строку (установить xmax = 102
в соответствующей версии строки).
Кроме того, ожидающая транзакция удерживает блокировку ресурса
типа tuple (версия строки), показывая, что она первая в «очереди».
13
«Очередь» ожидания
T 102
xmin xmax
100 ...101(0,1)
T 122
T 112
T 132
tuple 0,1
Update
Update
Update
Update
xid 101
T 101
Share
первый
в очереди стоит
в тамбуре
Если появляются другие транзакции, конфликтующие с текущей
блокировкой версии строки (в нашем примере — 112, 122, 132), первым
делом они пытаются захватить блокировку типа tuple для этой версии.
Поскольку блокировка tuple уже удерживается транзакцией 102, другие
транзакции ждут освобождения этой блокировки. Получается
своеобразная «очередь», в которой есть первый и все остальные.
Если бы все прибывающие транзакции занимали очередь
непосредственно за транзакцией 101, при освобождении блокировки
возникала бы ситуация гонки. При неудачном стечении обстоятельств
транзакция 102 могла бы ждать блокировку вечно. Двухуровневая
схема блокирования немного упорядочивает подобную конкуренцию.
Стоит избегать проектных решений, которые предполагают массовые
изменения одной и той же строки. В этом случае возникает «горячая
точка», которая на высоких нагрузках может привести к снижению
производительности.
15
«Очередь» ожидания
T 102
xmin xmax
100 ...2(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) режиме. Пока транзакция 102 ожидает завершения транзакции
101, может появиться еще транзакция 133, желающая получить
блокировку строки в разделяемом режиме, совместимом с текущей
блокировкой транзакции 101.
Такая транзакция не стоит в очереди, она сразу получает блокировку.
В этом случае из списка номеров блокирующих транзакций 101 и 133
формируется мультитранзакция 2, номер которой запишется в поле
xmax строки.
16
«Очередь» ожидания
T 102
xmin xmax
100 ...2(0,1)
T 122
T 133
T 112
T 132
tuple 0,1
Share
Update
Update
Update
Update
xid 133
теперь ждет
завершения
Т 133
Когда транзакция 101 завершится, транзакция 102 будет разбужена,
но не сможет захватить блокировку строки, поскольку строка все еще
заблокирована транзакцией 133. Транзакция 102 будет вынуждена
снова «заснуть».
При постоянном потоке разделяемых блокировок пишущая транзакция
может ждать своей очереди бесконечно. Это ситуация называется
по-английски locker starvation.
Заметим, что такой проблемы в принципе не возникает при блокировках
объектов (таких, как отношения). В этом случае каждый ресурс
представлен собственной блокировкой в оперативной памяти, и все
ждущие процессы выстраиваются в «честную» очередь.
17
«Очередь» ожидания
xmin xmax
100 ...102(0,1)
T 102
Update
xid 102
удерживает
исключительную
блокировку
T 122
T 132
Update
Update
T 112
Update
tuple 0,1
Когда транзакция 133 завершится, транзакция 102 получит возможность
первой записать свой номер в поле xmax, после чего она освободит
блокировку tuple. Тогда одна случайная транзакция из всех остальных
успеет захватить блокировку tuple и станет первой в «очереди».
19
Обнаруживаются поиском контуров в графе ожиданий
проверка выполняется после ожидания deadlock_timeout
Одна из транзакций обрывается, остальные продолжают
Взаимоблокировки
T 3 T 2
T 1
обычно —
неправильное
проектирование
приложения
T 2
T 1
Возможна ситуация взаимоблокировки, когда одна транзакция пытается
захватить ресурс, уже захваченный другой транзакцией, в то время как
другая транзакция пытается захватить ресурс, захваченный первой.
Взаимоблокировка возможна и при нескольких транзакциях: на слайде
показан пример такой ситуации для трех транзакций.
Визуально взаимоблокировку удобно представлять, построив граф
ожиданий. Для этого мы убираем конкретные ресурсы и оставляем
только транзакции, отмечая, какая транзакция какую ожидает. Если
в графе есть контур (из некоторой вершины можно по стрелкам
добраться до нее же самой) — это взаимоблокировка.
Отметим, что заблокированными ресурсами могут оказаться не только
строки; для разрешения взаимоблокировки важен не тип
заблокированных ресурсов, а взаимозависимость ожидающих их
освобождения транзакций.
Если взаимоблокировка возникла, участвующие транзакции уже не
могут ничего с этим сделать — они будут ждать бесконечно. Поэтому
PostgreSQL автоматически отслеживает взаимоблокировки. Проверка
выполняется, если какая-либо транзакция ожидает освобождения
ресурса дольше, чем указано в параметре deadlock_timeout. Если
выявлена взаимоблокировка, одна из транзакций принудительно
прерывается, чтобы остальные могли продолжить работу.
Взаимоблокировки обычно означают, что приложение спроектировано
неправильно. Сообщения в журнале сервера или увеличивающееся
значение pg_stat_database.deadlocks — повод задуматься о причинах.
21
Итоги
Блокировки строк хранятся в страницах данных
из-за потенциально большого количества
Очереди и обнаружение взаимоблокировок обеспечиваются
блокировками объектов
приходится прибегать к сложным схемам блокирования
22
Практика
1. Смоделируйте ситуацию обновления одной и той же строки
тремя командами UPDATE в разных сеансах.
Изучите возникшие блокировки в представлении pg_locks
и убедитесь, что все они понятны.
2. Воспроизведите взаимоблокировку трех транзакций.
Можно ли разобраться в ситуации постфактум, изучая
журнал сообщений?
3. Могут ли две транзакции, выполняющие единственную
команду UPDATE одной и той же таблицы, заблокировать
друг друга? Попробуйте воспроизвести такую ситуацию.