Блокировки
ADPG/PostgreSQL предоставляет различные режимы блокировки для управления одновременным доступом к данным в таблицах. Большинство команд PostgreSQL используют блокировки соответствующих режимов, чтобы гарантировать, что обрабатываемые таблицы не будут удалены или изменены несовместимыми способами во время выполнения команды. Например, TRUNCATE
не может безопасно выполняться одновременно с другими операциями над той же таблицей, он получает блокировку ACCESS EXCLUSIVE
для таблицы. Вы также можете реализовать блокировки, управляемые приложением, в ситуациях, когда концепция MVCC не соответствует вашим требованиям.
В зависимости от типа блокируемого объекта можно выделить следующие блокировки:
-
Блокировки на уровне таблицы — блокируют таблицу.
-
Блокировки на уровне строки — блокируют определенную строку.
-
Блокировки на уровне страницы — исключительные и разделяемые блокировки на уровне страницы используются для управления доступом для чтения/записи к страницам таблиц в общем пуле буферов. Эти блокировки снимаются немедленно после выборки или обновления строки. Разработчикам приложений обычно не нужно беспокоиться о блокировках на уровне страниц.
Также блокировки могут быть:
-
исключительными (exclusive) — одновременно могут удерживаться только одной транзакцией;
-
разделяемыми (shared) — могут удерживаться несколькими транзакциями.
Блокировки на уровне таблицы
В таблице ниже описаны доступные режимы блокировки и случаи, в которых их использует ADPG/PostgreSQL. Вы также можете получить любую из этих блокировок явно с помощью команды LOCK, как описано ниже. Все эти режимы блокировки являются блокировками уровня таблицы, даже если имя содержит слово "row", поскольку названия режимов блокировок исторические.
Название | Использование блокировки |
---|---|
ACCESS SHARE (AccessShareLock) |
Команда |
ROW SHARE (RowShareLock) |
Команды |
ROW EXCLUSIVE (RowExclusiveLock) |
Команды |
SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock) |
Этот режим получают |
SHARE (ShareLock) |
Защищает таблицу от одновременных изменений данных. Его получает |
SHARE ROW EXCLUSIVE (ShareRowExclusiveLock) |
Защищает таблицу от одновременного изменения данных и является самоисключающим, поэтому одновременно его может получить только одна сессия. |
EXCLUSIVE (ExclusiveLock) |
Допускает только одновременные блокировки |
ACCESS EXCLUSIVE (AccessExclusiveLock) |
Гарантирует, что только транзакция, получившая эту блокировку, сможет получить доступ к таблице. Его получают команды |
Основное различие между одним режимом блокировки и другим заключается в наборе режимов блокировки, с которыми каждый из них конфликтует. См. таблицу Конфликтующие режимы блокировки уровня таблицы. Две транзакции не могут одновременно удерживать блокировки конфликтующих типов для одной и той же таблицы. При этом разные транзакции могут одновременно владеть блокировками неконфликтующих режимов. Обратите внимание, что некоторые режимы блокировки конфликтуют сами с собой. Например, блокировка ACCESS EXCLUSIVE
не может удерживаться более чем одной транзакцией одновременно. Другие режимы не являются самоконфликтующими. Например, блокировка ACCESS SHARE
может удерживаться несколькими транзакциями. В таблице ниже пересечение строки и столбца, содержащих конфликтующие транзакции, отмечено символом X
.
Запрашиваемый режим блокировки |
Существующий режим блокировки |
|||||||
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 |
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 |
После получения блокировка обычно удерживается до конца транзакции. Но если блокировка получена после установки точки сохранения, она немедленно снимается, если происходит откат до точки сохранения.
Блокировки на уровне строки
Помимо блокировок на уровне таблицы, ADPG/PostgreSQL использует блокировки на уровне строки. Типы блокировок и случаи, в которых их использует ADPG/PostgreSQL, перечислены в таблице ниже.
Название | Использование блокировки |
---|---|
FOR UPDATE |
Режим блокировки |
FOR NO KEY UPDATE |
Работает аналогично |
FOR SHARE |
Работает аналогично |
FOR KEY SHARE |
Действует аналогично |
Обратите внимание, что транзакция может удерживать конфликтующие блокировки одной и той же строки даже в разных подтранзакциях, но две транзакции никогда не получат конфликтующие блокировки одной и той же строки. Блокировки на уровне строк не влияют на выборку данных. Они предотвращают только запись и другие блокировки для определенной строки. Также, как и блокировки на уровне таблицы, блокировки на уровне строк снимаются при завершении транзакции или во время отката к точке сохранения. Таблица ниже отображает конфликты блокировок на уровне строк.
Запрашиваемый режим блокировки |
Существующий режим блокировки |
|||
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 |
PostgreSQL не хранит информацию об измененных строках в памяти, поэтому нет ограничений на количество строк, которые можно заблокировать. Однако блокировка строки может привести к записи на диск. Например, SELECT FOR UPDATE
изменяет выбранные строки, чтобы пометить их как заблокированные, и происходит запись на диск.
Команда LOCK
Команда LOCK устанавливает блокировку на уровне таблицы. При получении блокировок для команд, ссылающихся на таблицы, PostgreSQL всегда использует наименее ограничивающий режим. Можно использовать команду LOCK
в тех случаях, когда вам нужна более строгая блокировка. Если режим блокировки в команде не указан, используется ACCESS EXCLUSIVE
— наиболее ограничительный режим.
Например, следующий код получает блокировку SHARE
для таблицы первичного ключа при выполнении вставки в таблицу внешнего ключа:
BEGIN;
LOCK TABLE author IN SHARE MODE;
INSERT INTO book (author_id, title, public_year) VALUES
((SELECT id FROM author
WHERE name = 'Herbert Wells'),
'The Time Machine',
1895);
COMMIT;
LOCK
при необходимости ожидает снятия любых конфликтующих блокировок. Вы можете изменить это поведение, используя опцию NOWAIT
. Если указано NOWAIT
, LOCK
не ожидает получения желаемой блокировки. Если ее невозможно получить немедленно, PostgreSQL прерывает выполнение команды и выдает ошибку. Следующий код показывает, как использовать NOWAIT
:
BEGIN;
LOCK TABLE book IN SHARE ROW EXCLUSIVE MODE NOWAIT;
DELETE FROM book WHERE author_id =1;
COMMIT;
После получения блокировка удерживается до конца текущей транзакции. Команды UNLOCK
нет. Блокировки всегда снимаются в конце транзакции.
Когда команда LOCK
блокирует представление, все отношения, указанные в определении представления, блокируются рекурсивно с тем же режимом блокировки.
Обратите внимание, что в стандарте SQL нет LOCK TABLE
, вместо этого используется SET TRANSACTION для указания уровней параллелизма в транзакциях.
Просмотр блокировок
Чтобы просмотреть список текущих блокировок на сервере базы данных, используйте системное представление pg_locks.
Создайте таблицу accounts
для демонстрационных целей:
-- Первая сессия
CREATE TABLE accounts(
acc_number integer PRIMARY KEY,
balance numeric
);
INSERT INTO accounts VALUES (1234,1000.00), (5432,2000.00), (2346,3000.00);
Блокировки удобно просматривать для определенной сессии, для этого получите ID обслуживающего процесса сессии, используя функцию pg_backend_pid()
(см. System information functions and оperators).
-- Первая сессия
SELECT pg_backend_pid();
Результат:
pg_backend_pid ---------------- 6155
Откройте транзакцию и обновите строку в таблице:
-- Первая сессия
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE acc_number = 1234;
Выполните запрос к pg_locks
, используя ID обслуживающего процесса сессии (6155
):
-- Первая сессия
SELECT locktype, relation::regclass, transactionid AS xid, mode, granted
FROM pg_locks WHERE pid = 6155;
Результат:
locktype | relation | xid | mode | granted ---------------+---------------+---------+------------------+--------- relation | pg_locks | | AccessShareLock | t relation | accounts_pkey | | RowExclusiveLock | t relation | accounts | | RowExclusiveLock | t virtualxid | | | ExclusiveLock | t transactionid | | 2584964 | ExclusiveLock | t
Где:
-
locktype
— тип блокируемого объекта, см. Wait events of type lock; -
relation
— имя отношения, получаемое через приведение OID из поляrelation
к типуregclass
; -
mode
— режим блокировки, используются названия, приведенные в таблице Блокировки на уровне таблицы в круглых скобках; -
xid
— ID транзакции; -
granted
— имеет значениеt
, если блокировка удерживается, иf
, если блокировка ожидает.
Начните вторую сессию и новую транзакцию:
-- Second session
BEGIN;
SELECT pg_backend_pid();
Результат:
pg_backend_pid ---------------- 26741
Попытайтесь создать индекс:
-- Second session
CREATE INDEX ON accounts(acc_number);
Выполните запрос к pg_locks
, используя ID обслуживающего процесса второй сессии (26741
), в первой сессии:
-- Первая сессия
SELECT locktype, relation::regclass, transactionid AS xid, mode, granted
FROM pg_locks WHERE pid = 26741;
В результате можно увидеть, что транзакция второй сессии ожидает блокировку SHARE
:
locktype | relation | xid | mode | granted ------------+----------+-----+---------------+--------- virtualxid | | | ExclusiveLock | t relation | accounts | | ShareLock | f
Завершите транзакцию в первой сессии и проверьте результат:
-- Первая сессия
COMMIT;
SELECT locktype, relation::regclass, transactionid AS xid, mode, granted
FROM pg_locks WHERE pid = 26741;
Транзакция второй сессии получила блокировку SHARE
для создания индекса:
locktype | relation | xid | mode | granted ---------------+----------+---------+---------------------+--------- virtualxid | | | ExclusiveLock | t relation | 289625 | | AccessExclusiveLock | t relation | accounts | | ShareLock | t transactionid | | 2586134 | ExclusiveLock | t
Взаимоблокировки
Взаимоблокировки — это ситуация, когда две (или более) транзакции удерживают блокировки таким образом, что взаимно блокируют друг друга. Частое использование явных блокировок может увеличить вероятность взаимоблокировок.
Например, если транзакция 1
получает исключительную блокировку для таблицы A
, а затем пытается получить исключительную блокировку для таблицы B
, в то время как транзакция 2
уже имеет исключительную блокировку для таблицы B
и теперь пытается получить исключительную блокировка таблицы A
. Ни одна из транзакций не сможет продолжиться. ADPG/PostgreSQL автоматически обнаруживает ситуации взаимоблокировки и разрешает их, прерывая одну из транзакций. Это позволяет остальным транзакциям завершить работу. Какая именно транзакция будет прервана, обычно сложно предсказать, и не следует рассчитывать на определенное поведение.
Обратите внимание, что взаимоблокировки также могут возникать в результате блокировок на уровне строк. Таким образом, они могут возникнуть, даже если не используется явная блокировка. Рассмотрим случай, когда две параллельные транзакции изменяют таблицу. Выполняется первая транзакция:
UPDATE accounts SET balance = balance + 1000.00 WHERE acc_number = 1234;
Она получает блокировку на уровне строки для строки с acc_number
= 1234
. Затем выполняется вторая транзакция:
UPDATE accounts SET balance = balance + 500.00 WHERE acc_number = 5432;
UPDATE accounts SET balance = balance - 500.00 WHERE acc_number = 1234;
Первый оператор UPDATE
успешно получает блокировку на уровне строки для указанной строки (с acc_number
= 5432
) и обновляет эту строку. Однако второй оператор UPDATE
обнаруживает, что строка, которую он пытается обновить, уже была заблокирована, поэтому он ожидает завершения транзакции, получившей блокировку. Вторая транзакция теперь ожидает завершения первой транзакции, прежде чем продолжить выполнение.
Затем выполняется первая транзакция:
UPDATE accounts SET balance = balance - 1000.00 WHERE acc_number = 5432;
Первая транзакция пытается получить блокировку для указанной строки (с acc_number
= 5432
), но не может: вторая транзакция уже удерживает такую блокировку. Поэтому она ожидает завершения второй транзакции. Таким образом, первая транзакция блокирует вторую транзакцию, а вторая транзакция блокирует первую транзакцию. Возникает взаимоблокировка. ADPG/PostgreSQL обнаружит эту ситуацию и прервет одну из транзакций.
Как правило, лучший способ предотвратить взаимоблокировки — обеспечить, чтобы все приложения, обращающиеся к базе данных, блокировали несколько объектов в согласованном порядке. Если в приведенном выше примере обе транзакции обновляли бы строки в одном и том же порядке, взаимоблокировка не произошла бы.
Кроме того, блокировки следует упорядочить так, чтобы первая блокировка, установленная на объекте, имела наиболее ограничительный режим. Если такой порядок не может быть обеспечен заранее, взаимоблокировки можно обрабатывать "на лету", повторяя транзакции, которые прерываются из-за взаимоблокировок.
Рекомендательные блокировки
ADPG/PostgreSQL предоставляет средства для создания блокировок, значение которых определяется приложением. Они называются рекомендательными блокировками (advisory locks). Корректность применения таких блокировок зависит от приложения, их использующего. Рекомендательные блокировки могут быть полезны для стратегий блокировки, которые не соответствуют модели MVCC.
Существует два способа получения рекомендательной блокировки: на уровне сессии и на уровне транзакции. После получения на уровне сессии рекомендательная блокировка удерживается до тех пор, пока она не будет явно снята или сессия не завершится. В отличие от стандартных запросов блокировки, рекомендательные блокировки на уровне сессии нарушают логику транзакции: блокировка, полученная в транзакции, даже если произойдет откат этой транзакции, будет сохранена в сессии. Аналогичным образом снятие блокировки остается в силе, даже если транзакция, в которой она была снята, позже прервется. Вызывающий процесс может запросить блокировку несколько раз, и для каждого завершенного запроса на блокировку должен быть соответствующий запрос на освобождение, чтобы блокировка была снята.
Запросы на блокировку на уровне транзакции автоматически освобождаются в конце транзакции и не требуют явных операций освобождения. Такое поведение часто более удобно, чем поведение на уровне сессии для кратковременного использования рекомендательной блокировки. Запросы рекомендательной блокировки на уровне сессии и транзакции для одного и того же идентификатора будут блокировать друг друга ожидаемым образом. Если сессия уже удерживает данную рекомендательную блокировку, дополнительные запросы от нее всегда будут успешными, даже если другие сессии ожидают блокировки. Это утверждение справедливо вне зависимости от того, на каком уровне (сессии или транзакции) запрашиваются новые блокировки.
Например, можно получить исключительную рекомендательную блокировку на уровне сессии. Функции, управляющие рекомендательными блокировками, используют числовой идентификатор. В качестве возможного решения используйте хеш-код от имени:
BEGIN;
SELECT pg_advisory_lock(hashtext('custom_name'));
....
COMMIT;
SELECT pg_advisory_unlock(hashtext('custom_name'));
Описание функций, управляющих рекомендательными блокировками, можно найти в разделе Advisory lock functions.
В качестве примера можно рассмотреть использование рекомендательной блокировки для создания функции, которая не позволяет многопользовательский запуск.
CREATE FUNCTION single_call() RETURNS VOID AS $$
BEGIN
PERFORM pg_advisory_lock(1234);
EXECUTE 'UPDATE pgbench_accounts
SET abalance = abalance + 1'; -- выполните необходимые действия
PERFORM pg_advisory_unlock(1234);
END;
$$ LANGUAGE plpgsql
При использовании рекомендательных блокировок, особенно в запросах с явными указаниями ORDER BY
и LIMIT
, важно учитывать, что получаемые блокировки могут зависеть от порядка вычисления SQL-выражений. В следующем примере LIMIT
необязательно будет применен перед вызовом функции pg_advisory_lock
. Таким образом, приложение может получить блокировки, на которые оно не рассчитывало и не сможет освободить до завершения сессии:
SELECT pg_advisory_lock(id) FROM table1 WHERE id > 12345 LIMIT 100;
В следующем запросе такая проблема не возникнет:
SELECT pg_advisory_lock(q.id) FROM
(
SELECT id FROM table1 WHERE id > 12345 LIMIT 100
) q;
Как рекомендательные, так и обычные блокировки хранятся в общем пуле памяти. Его размер определяется параметрами конфигурации max_locks_per_transaction
и max_connections
. Эти параметры можно установить на вкладке Clusters → Кластер ADPG → Services → ADPG → Primary configuration в пользовательском интерфейсе ADCM. Используйте специальный раздел postgresql.conf custom section для установки значения max_locks_per_transaction
. Если этой памяти недостаточно, сервер не сможет выполнить блокировку.
Как и все блокировки в PostgreSQL, полный список рекомендательных блокировок, удерживаемых в данный момент любой сессией, можно найти в системном представлении pg_locks
. Поле locktype
для них будет содержать значение advisory
.