Уровень 2.0
Предусловие:
- Изучена модуль «Запись грязных страниц» данного курса
В этом задании вы разберете:
- Назначение и принцип работы блокировок
- Блокировки объектов
- Блокировки строк
Блокировки
Назначение
В запущенном экземпляре СУБД одновременно работает некоторое количество процессов. Среди них может быть множество обслуживающих процессов, выполняющих транзакции пользователей, а также несколько фоновых процессов, занимающихся обслуживанием самого экземпляра.
Каждый из процессов при этом может обращаться к некоторым общим ресурсам.
К таким ресурсам, в частности, относятся отношения (таблицы, индексы, представления и т. д.), страницы данных, версии строк, а также структуры в разделяемой памяти (буферы в различных кешах, хеш-таблицы и т. д.).
Чтобы решить проблему конкурентного доступа к разделяемым ресурсам, обращение к ним необходимо каким-то образом упорядочивать. Для этой цели служат блокировки.
Общий принцип работы
Перед обращением к ресурсу процесс захватывает его блокировку, после окончания работы с ресурсом ее освобождает.
В зависимости от цели обращения блокировка может захватываться в различных режимах. Некоторые режимы блокировок между собой совместимы, то есть блокировки могут быть одновременно наложены на один и тот же ресурс, другие между собой не совместимы.
Если процесс пытается наложить блокировку на ресурс, на который другим процессом уже наложена несовместимая блокировка, то ему будет отказано. В этом случае процессу придется либо встать в очередь и ждать освобождения блокировки, либо попробовать обратиться к ресурсу позже.
Однако не всегда при обращении к ресурсу процесс накладывает блокировку. Важным является случай обращения к строке в таблице для чтения.
В этом случае никаких блокировок на строку не накладывается и строка может беспрепятственно блокироваться другим процессом для выполнения изменений. В этом заслуга механизма многоверсионности. Каждый процесс работает со своей версией строки.
Правда, стоит отметить, что хотя на версию строки при чтении блокировка не накладывается, на таблицу все же накладывается определенная блокировка для того, чтобы во время чтения строк нельзя было, например, удалить таблицу.
Классификация блокировок
Блокировки классифицируются следующим образом:
- Блокировки объектов
- Блокировки строк
- Блокировки структур в оперативной памяти
Под объектами в данном случае понимаются довольно разнообразные типы ресурсов, к которым, в частности, относятся различные объекты системного каталога (отношения, схемы, роли и т. д.), файлы и страницы данных, а также номера транзакций.
Указанные объекты объединяет общий механизм работы с их блокировками. Блокировки объектов хранятся в общем пуле блокировок в оперативной памяти, имеют единый механизм ведения очереди и мониторинга состояния.
Блокировки объектов являются длительными и, как правило, захватываются до завершения транзакции.
Механизм работы с блокировками строк сильно отличается от механизма, используемого при работе с блокировками объектов.
Строки могут блокироваться в большом количестве, и оперативной памяти для хранения соответствующих блокировок может быть недостаточно. Поэтому отдельного объекта при блокировании строки не создается, а информация о блокировке хранится в самой версии строки в виде набора признаков ее заголовка.
Из-за этого механизм ведения очереди для блокировок строк сильно отличается от механизма для блокировок объектов и является более сложным по сравнению с ним.
Блокировки структур в оперативной памяти являются краткосрочными, их время, как правило, ограничивается долями секунд. Они используются, в частности, для упорядочивания доступа к буферам различных кешей в разделяемой памяти.
В качестве примера можно вспомнить механизм закрепления буфера в кеше буферов.
Блокировки в оперативной памяти устроены значительно проще блокировок объектов или строк и обычно имеют не более двух режимов захвата: исключительный и разделяемый.
Указанный вид блокировок не будет подробно рассматриваться в этой лекции, поскольку управление такими блокировками полностью обеспечивается экземпляром Pangolin.
Блокировки объектов
Блокировки объектов хранятся в разделяемой памяти.
Их количество ограничено значением max_connections x max_locks_per_transaction, то есть произведением максимального количества соединений и максимального количества блокировок на одну транзакцию.
Это количество является общим для всех транзакций, что означает, что у одной транзакции может быть больше блокировок, чем max_locks_per_transaction, если общее количество не израсходовано.
Информация об имеющихся на текущий момент блокировках объектов может быть получена с использованием представления pg_locks.
Для блокировок объектов реализована простая очередь ожидания. Если на объект наложена блокировка одним процессом, а другой процесс также пытается наложить блокировку в несовместимом режиме, то второй процесс становится в очередь, ожидая снятия блокировки первым.
Если при этом появляется третий процесс, желающий наложить блокировку на тот же объект, то он становится в очередь последним, даже если режим его блокировки совместим с режимом блокировки, наложенной на объект первым процессом.
Понять, какие процессы стоят в очереди перед процессом с заданным pid и запрашивают (или уже захватили) блокировку, несовместимую с блокировкой процесса с заданным pid, можно с использованием функции pg_blocking_pids.
А понять, какой именно операцией процесс заблокировал объект, можно с использованием представления pg_stat_activity.
Некоторые команды SQL (в том числе команда SELECT) позволяют не ждать освобождения блокировки в несовместимом режиме, а сразу выдавать ошибку. Для этого необходимо использовать в команде ключевое слово NOWAIT.
Также возможно возникновение ситуации взаимоблокировки объектов (deadlock), при которой две или более транзакций блокируют друг друга. Такие ситуации Pangolin распознает и прерывает одну из транзакций для исключения бесконечного ожидания.
Список типов блокировок объектов довольно большой и разнообразный (см. документацию). В том числе в него входят:
relation— блокировка отношенияobject— блокировка объекта, не являющегося отношением (схема, роль, база данных и т.д.)extend— блокировка файла отношения при добавлении новых страницfrozenid— блокировка при изменении границы заморозки номеров транзакций и мультитранзакций (в поляхdatfrozenxidиdatminmxidтаблицыpg_database)page— блокировка страницыtuple— блокировка кортежа (не путать с блокировкой строки)transactionid— блокировка номера транзакцииvirtualxid— блокировка виртуального номера транзакцииadvisory— рекомендательная блокировка
Захват и снятие большей части блокировок выполняется автоматически. Однако блокировку отношения также можно наложить явно с использованием команды LOCK, а наложение и освобождение рекомендательных блокировок возможны только пользователем.
Наиболее важным типом блокировки объектов с точки зрения администрирования и проектирования приложения является блокировка отношений.
Блокировки отношений и некоторые другие типы блокировок подробнее будут рассмотрены ниже.
Блокировки отношений
Блокировки отношений имеют достаточно большое количество режимов захвата, что позволяет гибко использовать разделяемый ресурс одновременно несколькими процессами.
Соответствие режимов блокировок отношений командам SQL приведено в таблице ниже.
| Режимы | Команды SQL |
|---|---|
| Access Share | SELECT |
| Row Share | SELECT FOR UPDATE / NO KEY UPDATE / SHARE / KEY SHARE |
| Row Exclusive | UPDATE, DELETE, INSERT и MERGE |
| Share Update Exclusive | VACUUM, ANALYZE, CREATE INDEX CONCURRENTLY, CREATE STATISTICS, COMMENT ON, REINDEX CONCURRENTLY, некоторые формы ALTER INDEX и ALTER TABLE |
| Share | CREATE INDEX |
| Share Row Exclusive | CREATE TRIGGER и некоторые формы ALTER TABLE |
| Exclusive | REFRESH MATERIALIZED VIEW CONCURRENTLY |
| Access Exclusive | DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL, REFRESH MATERIALIZED VIEW, многие формы ALTER INDEX и ALTER TABLE |
Режимы в таблице перечислены от самого слабого к самому сильному.
Например, режим Access Share является самым слабым. Блокировку таблицы в таком режиме захватывает команда SELECT.
Такая блокировка не препятствует никаким действиям в отношении таблицы, кроме действий, соответствующих самой сильной блокировке — Access Exclusive. При выполнении запроса одной транзакцией таблица, из которой запрашиваются данные, не может быть, например, удалена другой транзакцией.
Первые 4 режима блокировок в общем смысле не препятствуют изменению данных в таблице, а последние 4 препятствуют этому.
Названия режимов блокировок могут немного запутать (особенно когда в них содержится слово Row). Такие названия сложились исторически и логики в них мало. Однако важно понимать, какие режимы между собой совместимы, а какие нет.
Матрица совместимости режимов блокировок отношений представлена в таблице ниже.
| Access Share | Row Share | Row Exclusive | Share Update Exclusive | Share | Share Row Exclusive | Exclusive | Access Exclusive | |
|---|---|---|---|---|---|---|---|---|
| Access Share | x | |||||||
| Row Share | x | x | ||||||
| Row Exclusive | x | x | x | x | ||||
| Share Update Exclusive | x | x | x | x | x | |||
| Share | x | x | x | x | x | |||
| Share Row Exclusive | x | x | x | x | x | x | ||
| Exclusive | x | x | x | x | x | x | x | |
| Access Exclusive | x | x | x | x | x | x | x | x |
Одним из частых случаев блокирования таблицы для изменения данных является выполнение команды CREATE INDEX. При построении индекса захватывается блокировка в режиме Share, несовместимая с блокировкой в режиме Row Exclusive, которая необходима для выполнения таких команд, как UPDATE или DELETE.
В то же время построение индекса командой CREATE INDEX CONCURRENTLY требует блокировки в режиме Share Update Exclusive. Указанный режим совместим с режимом Row Exclusive, поэтому построение индекса таким способом не препятствует изменению данных.
Блокировка таблицы в любом из режимов может быть запрошена явно с использованием команды LOCK TABLE.
Другие блокировки объектов
Блокировки номеров транзакций
Номера транзакций также являются ресурсом, который может быть заблокирован.
Блокировка номера транзакции используется для определения момента завершения транзакции.
Каждая транзакция удерживает исключительную блокировку своего виртуального номера (блокировку типа virtualxid ), а также блокировку выданного настоящего номера (блокировку типа transactionid) после его выдачи при выполнении первой операции изменения данных.
Если процесс ждет завершения какой-либо транзакции, то он пытается захватить ее блокировку, получает отказ и встает в очередь ожидания. Когда транзакция завершается, блокировка ее номера освобождается, процесс, ожидающий в очереди блокировку, просыпается и понимает, что транзакция завершена.
Рекомендательные блокировки
Рекомендательные блокировки (блокировки типа advisory) используются только явно для реализации нестандартной логики блокирования.
Рекомендательными блокировками может быть заблокирован любой ресурс, который обычно не является объектом базы данных. Главное, чтобы этот ресурс можно было каким-либо образом идентифицировать. Например, для идентификации такого ресурса может использоваться какое-либо число или хеш-код от его имени.
Для управления рекомендательными блокировками имеется набор специальных функций, названия которых начинаются с pg_advisory.
Рекомендательные блокировки можно устанавливать либо до конца транзакции, либо до конца сеанса.
Предикатные блокировки
Предикатные блокировки используются для реализации уровня изоляции Serializable.
Термин предикатные является историческим. В ранних исследованиях для полной изоляции транзакций предлагалось блокировать не только строки, с которыми работают транзакции, но и условия их отбора, то есть предикаты.
В действительности в реализации предикатных блокировок в Pangolin никакие ресурсы (ни строки, ни предикаты) не блокируются, а лишь помечаются как используемые в транзакции с уровнем изоляции Serializable.
В случае обнаружения зависимости по данным одной транзакции от другой ее фиксация завершится ошибкой.
Такие блокировки, в которых изначально ничего не блокируется, а проверка соблюдения изоляции осуществляется при фиксации, называются оптимистическими.
Тем не менее для отслеживания зависимости транзакций по данным используется инфраструктура блокировок объектов.
Для этого используются блокировки типов tuple, page и relation со специальным режимом SIRead (Serializable Isolation Read).
При индексном доступе используются блокировки типа tuple для запрашиваемых строк и типа page для листовых индексных страниц. При последовательном доступе используется блокировка типа relation.
При увеличении количества блокировок применяется механизм повышения уровня гранулярности блокировок: множество мелкогранулярных блокировок заменяется на одну блокировку более высокого уровня гранулярности (tuple->page->relation).
Общее количество предикатных блокировок не может быть больше чем max_pred_locks_per_transact * max_connections (по умолчанию 64x100).
Блокировки строк
Общее устройство блокировок строк
Версионность, обеспечиваемая механизмом MVCC, позволяет изолировать параллельные транзакции, используя минимум блокировок строк.
Тем не менее при изменении (удалении) строк одной транзакцией требуется их блокировка для исключения возможности их изменения другими параллельными транзакциями.
Как было сказано выше, для упорядочивания конкурентного доступа к строкам плохо подходит стандартный механизм блокировок, используемый для объектов, поскольку для этого может потребоваться большой объем оперативной памяти.
В некоторых СУБД для решения этой проблемы используется механизм эскалации блокировок или, по-другому, механизм повышения уровня гранулярности блокировок, который уже упоминался при рассмотрении предикатных блокировок.
Например, если количество блокировок строк в таблице превышает некоторый порог, то блокируется вся таблица. Это позволяет экономить оперативную память, но при этом блокируются все строки таблицы, даже которые не требуются для выполнения запроса. Естественно, при таком подходе снижается пропускная способность СУБД.
В Pangolin блокировка строк осуществляется иначе. Отдельной структуры в памяти при блокировании строки не создается, а информация о блокировке хранится в самой версии строки в виде набора признаков ее заголовка.
Когда транзакция изменяет или удаляет строку в заголовке соответствующей версии строки, в поле xmax проставляется номер этой транзакции.
Ненулевое значение в поле xmax, соответствующее активной транзакции, выступает признаком блокировки.
Также в заголовке версии строки имеются информационные биты, с помощью которых определяется режим наложенной блокировки.
Такой подход позволяет блокировать строки без ограничения их количества.
В то же время для организации очереди транзакций в этом случае все равно приходится использовать обычные блокировки объектов. Например, чтобы понять момент завершения блокирующей транзакции, используется блокировка ее номера.
Однако число блокировок, необходимых для организации очереди в этом случае, пропорционально количеству транзакций, но не количеству строк.
Режимы блокировок строк
Для блокирования строк предусмотрено 4 режима:
| Режим | Команды SQL | Описание |
|---|---|---|
| FOR KEY SHARE | SELECT FOR KEY SHARE | Накладывается только явно. Запрещает изменения ключевых полей строки. Применяется при проверке внешних ключей. Разделяемый режим. |
| FOR SHARE | SELECT FOR SHARE | Накладывается только явно. Запрещает изменения любых полей строки. Разделяемый режим. |
| FOR NO KEY UPDATE | UPDATE (без ключевых полей), SELECT FOR NO KEY UPDATE | Накладывается при изменении любых полей строки, кроме ключевых. Исключительный режим. |
| FOR UPDATE | DELETE, UPDATE (с ключевыми полями), SELECT FOR UPDATE | Накладывается при удалении строки или изменение любых ее полей, включая ключевые. Исключительный режим. |
Матрица совместимости указанных режимов выглядит следующим образом:
| FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
|---|---|---|---|---|
| FOR KEY SHARE | x | |||
| FOR SHARE | x | x | ||
| FOR NO KEY UPDATE | x | x | x | |
| FOR UPDATE | x | x | x | x |
Если при изменении строки затрагиваются поля, по которым создан уникальный индекс, то строка блокируется в режиме FOR UPDATE. В таком же режиме строка блокируется при удалении и выполнении запроса командой SELECT FOR UPDATE.
Далее, без ограничения общности, с учетом названий режимов блокировки строк, любые поля, по которым создан уникальный индекс (участвуют в ограничениях целостности PRIMARY KEY или UNIQUE), будем называть ключевыми.
Чуть менее сильный режим блокировки FOR NO KEY UPDATE. Такой режим выбирается при изменении строки, не затрагивающем ключевые поля, а также при выполнении запроса командой SELECT FOR NO KEY UPDATE.
При выполнении команды UPDATE в зависимости от набора изменяемых полей выбирается один из двух перечисленных режимов. Чаще строки блокируются в режиме FOR NO KEY UPDATE, поскольку ключевые поля изменяются редко.
Режим FOR SHARE может применяться только явно при выполнении запроса командой SELECT FOR SHARE. Его применение гарантирует запрет изменения любых полей запрашиваемых строк до завершения транзакции.
Самый слабый режим FOR KEY SHARE гарантирует запрет изменения ключевых полей запрашиваемых строк до завершения транзакции. Он используется ядром Pangolin при проверке внешних ключей, а также может быть запрошен явно командой SELECT FOR KEY SHARE.
Режимы FOR SHARE и FOR KEY SHARE являются разделяемыми. Это означает, что на строку может быть наложено одновременно несколько блокировок в данных режимах.
Обратите внимание, что режим FOR KEY SHARE совместим с режимом FOR NO KEY UPDATE. Это означает, что при проверке внешних ключей изменение строк не блокируется, если оно не затрагивает ключевые поля.
Также еще раз подчеркнем, что при выполнении команды SELECT без явного указания режима блокировки никаких блокировок на отбираемые строки не накладывается.
Признаки блокировок в версиях строк
Ненулевое значение поля xmax в заголовке версии строки, соответствующее активной транзакции, свидетельствует о том, что строка заблокирована. А с помощью информационных битов определяется режим блокировки, например:
| ctid | xmax | is multi | lock only | keys updated | key share | share | Режим блокировки |
|---|---|---|---|---|---|---|---|
| (0,1) | 501 | FOR NO KEY UPDATE | |||||
| (0,2) | 502 | 1 | FOR UPDATE | ||||
| (0,3) | 503 | 1 | FOR NO KEY UPDATE | ||||
| (0,4) | 504 | 1 | 1 | FOR UPDATE | |||
| (0,5) | 505 | 1 | 1 | FOR KEY SHARE | |||
| (0,6) | 506 | 1 | 1 | 1 | FOR SHARE | ||
| (0,7) | 1 | 1 |
Режим блокировки определяют информационные биты keys updated, key share и share.
При этом, поскольку строка при блокировании не обязательно подлежит изменению, также имеется информационный бит lock only. Если он установлен, то строка только заблокирована, изменения в нее не вносились.
Помимо этого, обратите внимание на версию строки (0,7). В ней проставлен информационный бит is multi, свидетельствующий о том, что в поле xmax используется номер мультитранзакции.
Мультитранзакции используются при одновременном наложении нескольких совместимых блокировок на строку, поскольку несколько номеров транзакций в поле xmax записано быть не может.
Таким образом, мультитранзакция — это группа транзакций, которой присвоен номер.
Информация о наложенных блокировках транзакциями из состава мультитранзакции хранится в структуре pg_multixact.
Вспомним, что мультитранзакции ранее уже рассматривались в лекции «Заморозка версий строк».
Информация о наложенных на строки блокировках, в том числе при использовании мультитранзакций, может быть получена с использованием расширения pgrowlocks.
Очередь ожидания для блокировки строк
Очередь ожидания для блокировки версий срок устроена достаточно сложно. Это связано с тем, что, как уже было сказано, в оперативной памяти блокировки строк как таковые не хранятся.
Не будем приводить подробное описание организации очереди для блокировок строк. Однако отметим несколько моментов.
Для организации очереди в данном случае используются блокировки объектов, а именно блокировка номера транзакции и блокировка кортежа (не путать с блокировкой строки).
Блокировка номера транзакции используется для определения момента завершения захватившей блокировку транзакции, а блокировка кортежа — для частичного упорядочивания транзакций в очереди при их конкуренции за блокируемую строку.
В результате только одна транзакция в очереди гарантированно получит блокировку следующей, то есть является первой в очереди.
Для остальных транзакций, которые находятся позади нее, занятие этого первого места в очереди определяется случайным образом после его освобождения. При неудачном стечении обстоятельств некоторые транзакции могут бесконечно долго ждать своей очереди.
Поэтому стоит избегать ситуаций, в которых множество транзакций одновременно меняют одни и те же строки.
Помимо этого, в отличие от очереди блокировок объектов, очередь для блокировок строк не является «честной». Блокировки в совместимых режимах «проходят» без очереди.
Это теоретически может привести к ситуации, в которой в очереди будут находиться транзакции, запрашивающие блокировку на изменение, а тем временем строка будет постоянно блокироваться блокировками в разделяемом режиме, проходящими без очереди.
Разделяемые блокировки строк используются Pangolin при проверках внешних ключей, и это проблем не вызывает, поскольку режимы FOR KEY SHARE и NO KEY UPDATE совместимы, а строки, как правило, изменяются именно в режиме NO KEY UPDATE.
Однако явное использование разделяемых блокировок командой SELECT FOR SHARE может привести к вышеописанной ситуации. Поэтому использовать указанную команду следует с большой осторожностью.
Взаимоблокировки
Так же, как и для блокировок объектов, для блокировок строк возможно возникновение ситуации взаимоблокировки.
В простейшем случае взаимоблокировка возникает тогда, когда одна транзакция пытается заблокировать строку, ранее заблокированную другой транзакцией.
Взаимоблокировки могут возникать и с участием большего количества транзакций, если из их ожиданий можно построить замкнутый контур.
Если взаимоблокировка возникла, участвующие в ней транзакции оказываются в безвыходном положении, бесконечно ожидая освобождения блокировок.
Такие ситуации автоматически выявляются, и одна из транзакций (обычно та, на которой был замкнут контур) аварийно прерывается.
Процедура поиска взаимоблокировок запускается по прошествии времени ожидания снятия блокировки транзакцией, указанном в параметре deadlock_timeout (по умолчанию 1 секунда).
Информация о взаимоблокировках отражается в журнале сообщений, а также в счетчике deadlocks в системном каталоге pg_stat_database.
Увеличение количества взаимоблокировок, как правило, говорит о неправильно спроектированном приложении.
Итоги
Итак, задание подошло к концу, и вы узнали, что:
- Для упорядочивания конкурентного доступа к ресурсам используется механизм блокировок
- Каждая блокировка захватывается в определенном режиме. Совместимость режимов определяется матрицей совместимости
- Блокировки объектов хранятся в оперативной памяти
- Блокировки строк хранятся в страницах данных — в заголовках версий строк.