Конференция Arenadata
Новое время — новый Greenplum
Мы приглашаем вас принять участие в конференции, посвященной будущему Open-Source Greenplum 19 сентября в 18:00:00 UTC +3. Встреча будет проходить в гибридном формате — и офлайн, и онлайн. Онлайн-трансляция будет доступна для всех желающих.
Внезапное закрытие Greenplum его владельцем — компанией Broadcom - стало неприятным сюрпризом для всех, кто использует или планирует начать использовать решения на базе этой технологии. Многие ожидают выхода стабильной версии Greenplum 7 и надеются на её дальнейшее активное развитие.
Arenadata не могла допустить, чтобы разрабатываемый годами Open-Source проект Greenplum прекратил своё существование, поэтому 19 сентября мы представим наш ответ на данное решение Broadcom, а участники сообщества получат исчерпывающие разъяснения на все вопросы о дальнейшей судьбе этой технологии.

На конференции вас ждёт обсуждение следующих тем:

  • План возрождения Greenplum;
  • Дорожная карта;
  • Экспертное обсуждение и консультации.
Осталось до события

Блокировки

ADPG/PostgreSQL предоставляет различные режимы блокировки для управления одновременным доступом к данным в таблицах. Большинство команд PostgreSQL используют блокировки соответствующих режимов, чтобы гарантировать, что обрабатываемые таблицы не будут удалены или изменены несовместимыми способами во время выполнения команды. Например, TRUNCATE не может безопасно выполняться одновременно с другими операциями над той же таблицей, он получает блокировку ACCESS EXCLUSIVE для таблицы. Вы также можете реализовать блокировки, управляемые приложением, в ситуациях, когда концепция MVCC не соответствует вашим требованиям.

В зависимости от типа блокируемого объекта можно выделить следующие блокировки:

  • Блокировки на уровне таблицы — блокируют таблицу.

  • Блокировки на уровне строки — блокируют определенную строку.

  • Блокировки на уровне страницы — исключительные и разделяемые блокировки на уровне страницы используются для управления доступом для чтения/записи к страницам таблиц в общем пуле буферов. Эти блокировки снимаются немедленно после выборки или обновления строки. Разработчикам приложений обычно не нужно беспокоиться о блокировках на уровне страниц.

Также блокировки могут быть:

  • исключительными (exclusive) — одновременно могут удерживаться только одной транзакцией;

  • разделяемыми (shared) — могут удерживаться несколькими транзакциями.

Блокировки на уровне таблицы

В таблице ниже описаны доступные режимы блокировки и случаи, в которых их использует ADPG/PostgreSQL. Вы также можете получить любую из этих блокировок явно с помощью команды LOCK, как описано ниже. Все эти режимы блокировки являются блокировками уровня таблицы, даже если имя содержит слово "row", поскольку названия режимов блокировок исторические.

Блокировки на уровне таблицы
Название Использование блокировки

ACCESS SHARE (AccessShareLock)

Команда SELECT получает эту блокировку для обрабатываемых таблиц. В целом, любой запрос, который только читает таблицу и не изменяет ее, получает этот режим блокировки

ROW SHARE (RowShareLock)

Команды SELECT FOR UPDATE и SELECT FOR SHARE получают такую блокировку для своих целевых таблиц (в дополнение к блокировкам ACCESS SHARE для любых других таблиц, которые используется в этих запросах, но не в выражениях FOR UPDATE/FOR SHARE)

ROW EXCLUSIVE (RowExclusiveLock)

Команды UPDATE, DELETE и INSERT получают этот режим блокировки для обрабатываемой таблицы (в дополнение к блокировкам ACCESS SHARE для любых других таблиц, на которые ссылаются). В целом, этот режим блокировки будет получен любой командой, которая изменяет данные в таблице

SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)

Этот режим получают VACUUM (без FULL), ANALYZE, CREATE INDEX CONCURRENTLY, CREATE STATISTICS, COMMENT ON, REINDEX CONCURRENTLY, некоторые варианты ALTER INDEX и ALTER TABLE (более подробную информацию смотрите в документации этих команд)

SHARE (ShareLock)

Защищает таблицу от одновременных изменений данных. Его получает CREATE INDEX (без CONCURRENTLY)

SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)

Защищает таблицу от одновременного изменения данных и является самоисключающим, поэтому одновременно его может получить только одна сессия. SHARE ROW EXCLUSIVE получает CREATE TRIGGER и некоторые формы ALTER TABLE

EXCLUSIVE (ExclusiveLock)

Допускает только одновременные блокировки ACCESS SHARE — параллельно с транзакцией, получившей блокировку EXCLUSIVE, может выполняться только чтение из таблицы. Его получает REFRESH MATERIALIZED VIEW CONCURRENTLY

ACCESS EXCLUSIVE (AccessExclusiveLock)

Гарантирует, что только транзакция, получившая эту блокировку, сможет получить доступ к таблице. Его получают команды DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL и REFRESH MATERIALIZED VIEW (без CONCURRENTLY). Многие формы ALTER INDEX и ALTER TABLE также получают блокировку на этом уровне. ACCESS EXCLUSIVE является также режимом блокировки по умолчанию для оператора LOCK TABLE. Только ACCESS EXCLUSIVE блокирует оператор SELECT (без FOR UPDATE/SHARE)

Основное различие между одним режимом блокировки и другим заключается в наборе режимов блокировки, с которыми каждый из них конфликтует. См. таблицу Конфликтующие режимы блокировки уровня таблицы. Две транзакции не могут одновременно удерживать блокировки конфликтующих типов для одной и той же таблицы. При этом разные транзакции могут одновременно владеть блокировками неконфликтующих режимов. Обратите внимание, что некоторые режимы блокировки конфликтуют сами с собой. Например, блокировка 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 UPDATE блокирует строки, полученные оператором SELECT, как для изменения. Это предотвращает их блокировку, изменение или удаление другими транзакциями до завершения текущей транзакции. Другие транзакции, которые пытаются выполнить UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE или SELECT FOR KEY SHARE этих строк, будут заблокированы до завершения текущей транзакции. В рамках транзакции Repeatable read или Serializable ошибка будет выдана, если блокируемая строка изменилась с момента начала транзакции

Режим блокировки FOR UPDATE также получает любая команда DELETE для строки, а также команда UPDATE при определенном условии: UPDATE изменяет значения определенных столбцов, по которым создан уникальный индекс, применимый в качестве внешнего ключа (на частичные индексы и индексы выражений это не распространяется)

FOR NO KEY UPDATE

Работает аналогично FOR UPDATE, за исключением того, что эта блокировка более слабая: не блокирует команды SELECT FOR KEY SHARE, которые пытаются получить блокировку для тех же строк. Этот режим блокировки также получает любой UPDATE, который не получил блокировку FOR UPDATE

FOR SHARE

Работает аналогично FOR NO KEY UPDATE, за исключением того, что получает разделяемую, а не исключительную блокировку для каждой обрабатываемрой строки. Блокирует выполнение другими транзакциями команд UPDATE, DELETE, SELECT FOR UPDATE или SELECT FOR NO KEY UPDATE, но не мешает им выполнять SELECT FOR SHARE или SELECT FOR KEY SHARE

FOR KEY SHARE

Действует аналогично FOR SHARE, за исключением того, что блокировка слабее: блокируется SELECT FOR UPDATE, но не SELECT FOR NO KEY UPDATE. Эта блокировка не позволяет другим транзакциям выполнять команды DELETE или UPDATE, которые изменяют значения ключей, но разрешает другие команды UPDATE, а также SELECT FOR NO KEY UPDATE, SELECT FOR SHARE, SELECT 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.

Нашли ошибку? Выделите текст и нажмите Ctrl+Enter чтобы сообщить о ней