Параллельный контроль

В этой главе описывается поведение СУБД QHB, когда две или более сессий пытаются одновременно обратиться к одним и тем же данным. Задачи, стоящие перед СУБД в такой ситуации: обеспечить высокопроизводительный доступ к данным и сохранить их целостность. Материал данной статьи будет полезен всем разработчикам приложений баз данных.

Многоверсионная модель

QHB предоставляет разработчикам богатый набор инструментов для управления конкурентным доступом к данным. Внутренняя согласованность данных поддерживается с помощью многоверсионной модели (Multiversion Concurrency Control, MVCC). Она устроена таким образом, что в ходе исполнения каждой SQL-команды СУБД видит данные базы как бы "замороженными", определённой версии. Из этого следует, что те данные, которые были изменены или добавлены в ходе параллельной работы других транзакций, не нарушают целостности нашего представления о данных. MVCC, отказываясь от методологий блокировки традиционных систем баз данных, сводит к минимуму конфликты блокировок, чтобы обеспечить разумную производительность в многопользовательских средах.

Основное преимущество использования MVCC-модели управления параллелизмом, а не блокировок, состоит в том, что в MVCC блокировки, полученные для чтения данных, не конфликтуют с блокировками, взятыми для записи данных, а потому чтение никогда не блокирует запись, а запись чтение. QHB обеспечивает эту гарантию даже при использовании самого строгого уровня изоляции транзакций за счет использования Serializable Snapshot Isolation (SSI).

Средства блокировки на уровне таблиц и строк также доступны в QHB для приложений, которые обычно не нуждаются в полной изоляции транзакций и предпочитают явно управлять конкретными точками конфликта. Однако правильное использование MVCC обычно обеспечивает лучшую производительность, чем блокировки. Кроме того, определяемые приложением консультативные блокировки предоставляют механизм для получения блокировок, которые не привязаны к одной транзакции.

Изоляция транзакций

Стандарт SQL определяет четыре уровня изоляции транзакций. Наиболее строгим является Serializable, который определяется таким образом, что любое одновременное выполнение набора из нескольких Serializable транзакций гарантированно даст тот же эффект, что и запуск их по одному. Три других уровня определены в терминах явлений, возникающих в результате взаимодействия между параллельными транзакциями, которые не должны происходить на каждом уровне. Стандарт отмечает, что из-за определения Serializable, ни одно из этих явлений невозможно на этом уровне. (В этом нет ничего удивительного -- если эффект транзакций должен соответствовать тому, что они выполнялись по одному, как вы можете наблюдать какие-либо явления, вызванные их взаимодействием?)

Явления, которые запрещены на разных уровнях:

  • Грязное чтение (Dirty read). Транзакция считывает данные, которые были записаны в результате выполнения параллельной незафиксированной транзакции.
  • Неповторяемое чтение (Non-repeatable read). Транзакция считывает ранее прочитанные данные и замечает, что данные были изменены другой транзакцией (завершённой после первого чтения).
  • Фантомное чтение (Phantom read). Транзакция повторно выполняет запрос, возвращающий набор строк для некоторого условия и обнаруживает, что набор строк, удовлетворяющих условию, изменился из-за транзакции, завершившейся за это время.
  • Аномалии сериализации (Serialization anomaly). Результат успешной фиксации (commiting) группы транзакций оказывается несогласованным (inconsistent), отличающимся от результата полученного в ходе последовательного выполнения этих транзакций, независимо от порядка их выполнения. Результат успешной фиксации (commiting) группы транзакций конкурентно оказывается отличным от результата успешной фиксации группы транзаций выполнявшихся последовательно.

Уровни изолияции транзакций, описанные в стандарте SQL и реализованные в QHB:

Уровень изоляцииГрязное чтениеНеповторимое чтениеФантомное чтениеАномалия сериализации
Read uncommittedРазрешено, но не в QHBВозможноВозможноВозможно
Read committedНевозможноВозможноВозможноВозможно
Repeatable readНевозможноНевозможноРазрешено, но не в QHBВозможно
SerializableНевозможноНевозможноНевозможноНевозможно

В QHB вы можете запросить любой из четырех стандартных уровней изоляции транзакций, но внутренне реализованы только три различных уровня изоляции, то есть режим Read Uncommitted в QHB ведет себя как Read Committed. Это связано с тем, что это единственный разумный способ сопоставить стандартные уровни изоляции с архитектурой многоверсионного управления QHB.

Из таблицы также видно, что реализация Repeatable Read в QHB не позволяет выполнять фантомные чтения. Стандарт SQL допускает более строгое поведение: четыре уровня изоляции определяют только то, какие явления не должны происходить, а какие нет. Поведение доступных уровней изоляции подробно описано в следующих подразделах.

Чтобы установить уровень изоляции транзакции, используйте команду SET TRANSACTION.

Важно
Некоторые типы данных и функции QHB имеют специальные правила, касающиеся поведения транзакций. В частности, изменения, внесенные в последовательность (и, следовательно, счетчик столбца, объявленного с использованием serial), сразу видны всем другим транзакциям и не отменяются, если транзакция, которая внесла изменения, прерывается. См. главы Функции управления последовательностями и Серийные типы.

Уровень изоляции Read Committed

Read Committed - уровень изоляции по умолчанию в QHB. Когда транзакция использует этот уровень изоляции, запрос SELECT (без предложения FOR UPDATE/SHARE) видит только данные, зафиксированные до начала запроса; он никогда не видит ни незафиксированные данные, ни изменения, зафиксированные во время выполнения запроса параллельными транзакциями. По сути, запрос SELECT видит снимок базы в моменте, данные с начала выполнения запроса. Однако SELECT видит результаты предыдущих обновлений, выполненных в его собственной транзакции, даже если они еще не зафиксированы (commited). Также обратите внимание, что две последовательные команды SELECT могут видеть разные данные, даже если они находятся в пределах одной транзакции, если другие транзакции производят изменения данных после запуска первого SELECT и до запуска второго SELECT.

Команды UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и SELECT, в плане поиска целевых строк: они будут находить только те целевые строки, которые были зафиксированы на момент запуска команды. Однако такая целевая строка, возможно, уже была обновлена, удалена или заблокирована другой параллельной транзакцией к моменту ее обнаружения. В этом случае запланированное изменение будет дожидаться фиксации (commit) конкурентной трнзакции или отмены (rollback) если та ещё выполняется. Если конкурирующая транзакция откатывается (rollback), текущая транзакция может продолжить изменения полученной строки (конкурирующая её не изменила). Если конкурирующая транзакция зафиксировалась, но в результате её работы строка была удалена -- она будет проигнорирована; в противном случае она будет получена заново с повторной проверкой условия WHERE. Применительно к SELECT FOR UPDATE и SELECT FOR SHARE это означает, что обновлённая версия строки блокируется и возвращается клиенту.

INSERT с предложением ON CONFLICT DO UPDATE ведёт себя схожим образом: в режиме Read Commited каждая строка, предлагаемая для вставки, будет либо вставлена, либо изменена. Если не возникает несвязных ошибок, гарантируется один из двух исходов: если конфликт вызван конкурирующей транзакцией, результат который пока недоступен INSERT, UPDATE подействует на эту строку несмотря на то, что эта команда не должна видеть никакую версию этой строки.

INSERT с предложением ON CONFLICT DO NOTHING может привести к тому, что вставка не будет продолжена для строки из-за результата другой транзакции, эффекты которой не видны для снимка INSERT. Опять же, это характерно только для уровня Read Committed.

В силу вышеприведенных правил, команда обновления может увидеть несогласованное (inconsistent) состояние: она может видеть результаты выполнения конкурирующей команды. Вследствие этого, уровень Read Commited не подходит для команд со сложным сценарием поиска; однако, он вполне пригоден для простых случаев:

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

Если две такие транзакции одновременно пытаются изменить остаток на счете 12345, мы явно хотим, чтобы вторая транзакция началась с обновленной версии строки счета. Поскольку каждая команда влияет только на заранее определенную строку, ее отображение в обновленной версии строки не создает несогласованности.

Более сложное использование может привести к нежелательным результатам в режиме Read Committed. Например, рассмотрим команду DELETE работающую с данными, которые добавляются и удаляются согласно условиям другой командой, например, предположим, что website представляет собой таблицу из двух строк, где website.hits равны 9 и 10 :

BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session:  DELETE FROM website WHERE hits = 10;
COMMIT;

DELETE не сможет произвести удаление записей до момента фиксации транзакции. Запись с website.hits = 9 до выполнения UPDATE не будет подходить под условие DELETE, а вторая запись, с website.hits = 10 будет заблокирована до момента фиксации. После фиксации первой транзакции, первая запись получит website.hits = 10 и будет удалена во второй транзации.

Поскольку режим Read Committed запускает каждую команду с новым моментальным снимком, который включает в себя все транзакции, зафиксированные до этого момента, последующие команды в той же транзакции в любом случае будут видеть результаты совершенной параллельной транзакции.

Частичная изоляция транзакций, обеспечиваемая режимом Read Committed, подходит для многих приложений, и этот режим быстр и прост в использовании; однако, этого недостаточно для всех случаев. Приложениям, которые выполняют сложные запросы и обновления, может потребоваться более строго согласованное представление базы данных, чем обеспечивает режим Read Committed.

Уровень изоляции Repeatable Read

Уровень изоляции Repeatable Read видит только данные, зафиксированные до начала транзакции; он никогда не видит ни незафиксированные данные, ни изменения, зафиксированные во время выполнения транзакций параллельными транзакциями. (Тем не менее, запрос видит результаты предыдущих обновлений, выполненных в его собственной транзакции, даже если эти изменения еще не зафиксированы.) Это более надежная гарантия, чем требуется стандартом SQL для этого уровня изоляции, и предотвращает все явления описано в таблице 1 за исключением аномалий сериализации. Как упомянуто выше, это специально разрешено стандартом, который описывает только минимальную защиту, которую должен обеспечивать каждый уровень изоляции.

Этот уровень отличается от Read Committed тем, что запрос в повторяемой транзакции чтения видит моментальный снимок в начале первого оператора, не являющегося элементом управления транзакцией в транзакции, а не в начале текущего оператора в транзакции. Таким образом, последовательные команды SELECT в одной транзакции видят одни и те же данные, т.е. они не видят изменений, внесенных другими транзакциями, зафиксированными после запуска их собственной транзакции.

Приложения, использующие этот уровень, должны быть готовы повторить транзакции из-за ошибок сериализации.

Команды UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и команды SELECT с точки зрения поиска целевых строк: они найдут только целевые строки, которые были зафиксированы на момент начала транзакции. Однако такая целевая строка, возможно, уже была обновлена (или удалена, или заблокирована) другой параллельной транзакцией к моменту ее обнаружения. В этом случае повторяемая транзакция чтения будет ожидать, когда первая обновляющая транзакция будет зафиксирована или откатана (если она все еще выполняется). Если первый модуль обновления откатывается назад, то его эффекты сводятся на нет, и повторяемая транзакция чтения может продолжить обновление первоначально найденной строки. Но если первый обновитель фиксирует (и фактически обновил или удалил строку, а не просто заблокировал ее), то повторяемая транзакция чтения будет откатана с сообщением

ERROR:  could not serialize access due to concurrent update

потому что повторяемая транзакция чтения не может изменять или блокировать строки, измененные другими транзакциями после начала повторяемой транзакции чтения.

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

Обратите внимание, что только обновление транзакций может потребоваться повторить; Транзакции только для чтения никогда не будут иметь конфликтов сериализации.

Режим повторяемого чтения обеспечивает строгую гарантию того, что каждая транзакция видит полностью стабильное представление базы данных. Тем не менее, это представление не всегда будет соответствовать последовательному (по одному) выполнению параллельных транзакций одного и того же уровня. Например, даже транзакция только для чтения на этом уровне может видеть контрольную запись, обновленную, чтобы показать, что пакет был завершен, но не увидеть одну из подробных записей, которая логически является частью пакета, потому что она прочитала более раннюю версию контрольной записи., Попытки обеспечить выполнение бизнес-правил транзакциями, выполняющимися на этом уровне изоляции, вряд ли будут работать правильно без осторожного использования явных блокировок для блокировки конфликтующих транзакций.

Уровень изоляции Serializable

Уровень изоляции Serializable обеспечивает самую строгую изоляцию транзакций. Этот уровень эмулирует выполнение последовательных транзакций для всех зафиксированных транзакций; как если бы транзакции были выполнены одна за другой, поочередно, а не одновременно. Однако, как и уровень повторяемого чтения, приложения, использующие этот уровень, должны быть готовы повторить транзакции из-за сбоев сериализации. Фактически, этот уровень изоляции работает точно так же, как и Repeatable Read, за исключением того, что он отслеживает условия, которые могут привести к тому, что выполнение параллельного набора сериализуемых транзакций будет вести себя не так, как все возможные последовательные (по одному) выполнения этих транзакций. Этот мониторинг не вводит каких-либо блокировок, кроме присутствующих в повторяющемся чтении, но есть некоторые накладные расходы на мониторинг, и обнаружение условий, которые могут вызвать аномалию сериализации, вызовет сбой сериализации .

В качестве примера рассмотрим таблицу mytab, изначально содержащую:

class | value
------+-------
    1 |    10
    1 |    20
    2 |   100
    2 |   200

Предположим, что сериализуемая транзакция A вычисляет:

SELECT SUM(value) FROM mytab WHERE class = 1;

а затем вставляет результат 30 в качестве value в новую строку с class = 2. Одновременно сериализуемая транзакция B вычисляет:

SELECT SUM(value) FROM mytab WHERE class = 2;

и получает результат 300, который он вставляет в новую строку с class = 1. Затем обе транзакции пытаются зафиксировать. Если какая-либо транзакция выполняется на уровне изоляции Repeatable Read, обеим будет разрешено зафиксировать; но поскольку последовательный порядок выполнения не согласуется с результатом, использование сериализуемых транзакций позволит зафиксировать одну транзакцию и откатит другую с этим сообщением:

ERROR:  could not serialize access due to read/write dependencies among transactions

Это потому, что если бы A выполнилась до B, B вычислила бы сумму 330, а не 300, и аналогично другой порядок привел бы к другой сумме, вычисленной A.

Если для предотвращения аномалий Вы используете уровень изоляции Serializable, то важно помнить, что данные постоянной(persistant) пользовательской таблицы не явлются валидными до того момента, пока не произойдёт фиксация транзакцией. Это верно в том числе и для транзакций, которые выполняют только чтение, за исключением ситуаций, когда данные читаются отложенной(deffer)

Если вы используете уровень изоляции Serializable для предотвращения аномалий, важно, чтобы любые данные, считанные из постоянной пользовательской таблицы, не считались действительными до тех пор, пока производящая их транзакция не была успешно зафиксирована. Это верно даже для read-only транзакций, за исключением того, что данные, считанные в отложенной транзакции только для чтения, как известно, действительны, как только они прочитаны, потому что такая транзакция ожидает, пока она не сможет получить моментальный снимок, гарантированный свободный от такого проблемы, прежде чем начать читать какие-либо данные. Во всех других случаях приложения не должны зависеть от результатов, прочитанных во время транзакции, которая впоследствии была прервана; вместо этого они должны повторить транзакцию, пока она не завершится успешно.

Чтобы гарантировать истинную сериализуемость, QHB использует блокировку предикатов, что означает, что он сохраняет блокировки, которые позволяют ему определять, когда запись повлияла бы на результат предыдущего чтения из параллельной транзакции, если бы она выполнялась первой. В QHB эти блокировки не вызывают блокировку и, следовательно, не могут играть какую-либо роль в возникновении тупика. Они используются для идентификации и пометки зависимостей между параллельными сериализуемыми транзакциями, которые в определенных комбинациях могут привести к аномалиям сериализации. В отличие от транзакции Read Committed или Repeatable Read, которая хочет обеспечить согласованность данных, может потребоваться снять блокировку всей таблицы, что может заблокировать других пользователей, пытающихся использовать эту таблицу, или использовать SELECT FOR UPDATE или SELECT FOR SHARE который не только может заблокировать другие транзакции, но и вызвать доступ к диску.

Блокировки предикатов в QHB, как и в большинстве других систем баз данных, основаны на данных, фактически доступных транзакции. Они будут отображаться в системном представлении pg_locks в mode SIReadLock . Конкретные блокировки, полученные во время выполнения запроса, будут зависеть от плана, используемого запросом, и несколько более мелких блокировок (например, блокировок кортежей) могут быть объединены в меньшее количество более грубых блокировок (например, блокировок страниц) в течение транзакция для предотвращения исчерпания памяти, используемой для отслеживания блокировок. Транзакция READ ONLY может освободить свои блокировки SIRead до завершения, если обнаружит, что по-прежнему не может возникнуть конфликтов, которые могут привести к аномалии сериализации. Фактически транзакции READ ONLY часто могут установить этот факт при запуске и избежать каких-либо предикатных блокировок. Если вы явно запросите SERIALIZABLE READ ONLY DEFERRABLE, она будет блокирована, пока не сможет установить этот факт. (Это единственный случай, когда сериализуемые транзакции блокируют, а повторяющиеся транзакции чтения не делают.) С другой стороны, блокировки SIRead часто необходимо сохранять после принятия транзакции до тех пор, пока не завершатся перекрывающиеся транзакции чтения-записи.

Последовательное использование сериализуемых транзакций может упростить разработку. Гарантия того, что любой набор успешно совершенных параллельных сериализуемых транзакций будет иметь такой же эффект, как если бы они выполнялись по одной за раз, означает, что если вы сможете продемонстрировать, что отдельная транзакция, как написано, будет работать правильно при запуске сама по себе, вы может быть уверен, что он будет действовать правильно в любой комбинации сериализуемых транзакций, даже без какой-либо информации о том, что эти другие транзакции могут сделать, или он не будет успешно зафиксирован. Важно, чтобы среда, в которой используется этот метод, имела обобщенный способ обработки ошибок сериализации (которые всегда возвращаются со значением SQLSTATE ’40001’), поскольку будет очень сложно предсказать, какие именно транзакции могут способствовать чтению / записи. зависимости и необходимо откатить, чтобы предотвратить сериализацию аномалий. Мониторинг зависимостей чтения / записи имеет свою стоимость, также как и перезапуск транзакций, которые завершаются с ошибкой сериализации, но сбалансированы с затратами и блокировками, связанными с использованием явных блокировок и SELECT FOR UPDATE или SELECT FOR SHARE Сериализуемые транзакции лучший выбор производительности для некоторых сред.

Хотя уровень изоляции Sergizable в QHB позволяет фиксировать параллельные транзакции только в том случае, если он может доказать, что существует последовательный порядок выполнения, который даст тот же эффект, он не всегда предотвращает возникновение ошибок, которые не возникнут при истинном последовательном выполнении., В частности, можно увидеть уникальные нарушения ограничений, вызванные конфликтами с перекрывающимися сериализуемыми транзакциями, даже после явной проверки отсутствия ключа перед попыткой его вставки. Этого можно избежать, убедившись, что все сериализуемые транзакции, которые вставляют потенциально конфликтующие ключи, явно проверяют, могут ли они сделать это в первую очередь. Например, представьте приложение, которое запрашивает у пользователя новый ключ, а затем проверяет, что его еще не существует, сначала пытаясь выбрать его, или генерирует новый ключ, выбирая максимально существующий ключ и добавляя его. Если некоторые сериализуемые транзакции вставляют новые ключи напрямую, не следуя этому протоколу, нарушения уникальных ограничений могут быть зарегистрированы даже в тех случаях, когда они не могут произойти при последовательном выполнении параллельных транзакций.

Для обеспечения оптимальной производительности при использовании параллельных транзакций для управления параллелизмом следует учитывать следующие проблемы:

  • Объявите транзакции READ ONLY когда это возможно.

  • Управляйте количеством активных соединений, используя пул соединений, если это необходимо. Это всегда важный фактор производительности, но он может быть особенно важен в загруженной системе с использованием сериализуемых транзакций.

  • Не вкладывайте в одну транзакцию больше, чем необходимо для обеспечения целостности.

  • Не оставляйте соединения висящими « бездействующими в транзакции » дольше, чем это необходимо. Параметр конфигурации idle_in_transaction_session_timeout может использоваться для автоматического отключения длительных сеансов.

  • Устраните явные блокировки, SELECT FOR UPDATE и SELECT FOR SHARE где они больше не нужны, благодаря защитам, автоматически предоставляемым сериализуемыми транзакциями.

  • Когда система вынуждена объединять несколько блокировок предикатов на уровне страниц в одну блокировку предикатов на уровне отношений, поскольку в таблице блокировок предикатов не хватает памяти, может произойти увеличение частоты сбоев сериализации. Вы можете избежать этого, увеличив max_pred_locks_per_transaction, max_pred_locks_per_relation и / или max_pred_locks_per_page.

  • Последовательное сканирование всегда требует блокировки предиката на уровне отношений. Это может привести к увеличению частоты сбоев сериализации. Может быть полезно поощрять использование сканирования индекса путем уменьшения random_page_cost и / или увеличения cpu_tuple_cost. Обязательно сопоставьте любое уменьшение откатов транзакций и перезапусков с любым общим изменением времени выполнения запроса.

Явная блокировка

QHB предоставляет различные режимы блокировки для управления одновременным доступом к данным в таблицах. Эти режимы могут использоваться для управляемой приложением блокировки в ситуациях, когда MVCC не дает желаемого поведения. Кроме того, большинство команд QHB автоматически получают блокировки соответствующих режимов, чтобы гарантировать, что ссылочные таблицы не будут удалены или изменены несовместимыми способами во время выполнения команды. (Например, TRUNCATE не может безопасно выполняться одновременно с другими операциями в той же таблице, поэтому он получает эксклюзивную блокировку для таблицы, чтобы обеспечить это.)

Чтобы просмотреть список текущих незавершенных блокировок на сервере базы данных, используйте системное представление pg_locks. Для получения дополнительной информации о мониторинге состояния подсистемы диспетчера блокировки см. Главу 27 .

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

В приведенном ниже списке показаны доступные режимы блокировки и контексты, в которых они автоматически используются QHB. Вы также можете получить любую из этих блокировок явно с помощью команды LOCK. Помните, что все эти режимы блокировки являются блокировками на уровне таблицы, даже если имя содержит слово « строка » ; Названия режимов блокировки являются историческими. В некоторой степени имена отражают типичное использование каждого режима блокировки - но семантика все та же. Единственное реальное различие между одним режимом блокировки и другим - это набор режимов блокировки, с которыми конфликтует каждый (см. Таблицу 2). Две транзакции не могут одновременно удерживать блокировки конфликтующих режимов на одной и той же таблице. (Однако транзакция никогда не конфликтует сама с собой. Например, она может получить блокировку ACCESS EXCLUSIVE а затем получить блокировку ACCESS SHARE для той же таблицы.) Бесконфликтные режимы блокировки могут одновременно поддерживаться многими транзакциями. Обратите внимание, в частности, на то, что некоторые режимы блокировки являются конфликтующими друг с другом (например, блокировка ACCESS EXCLUSIVE не может удерживаться более чем одной транзакцией одновременно), в то время как другие не являются конфликтующими друг с другом (например, блокировка ACCESS SHARE может удерживаться несколько транзакций).

Режимы блокировки на уровне таблицы

ACCESS SHARE

Конфликтует только с режимом блокировки ACCESS EXCLUSIVE .

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

ROW SHARE

Конфликты с режимами блокировки EXCLUSIVE и ACCESS EXCLUSIVE .

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

ROW EXCLUSIVE

Конфликты с режимами блокировки SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE .

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

SHARE UPDATE EXCLUSIVE

Конфликты с режимами блокировки SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от одновременных изменений схемы и VACUUM .

Приобретен VACUUM (без FULL ), CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY CREATE STATISTICS, а также некоторые варианты ALTER INDEX и ALTER TABLE (для полной информации см. ALTER INDEX и ALTER TABLE ).

SHARE

Конфликты с режимами блокировки ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от одновременных изменений данных.

Приобретен по CREATE INDEX (без одновременного CONCURRENTLY ).

SHARE ROW EXCLUSIVE

Конфликты с режимами блокировки ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от одновременных изменений данных и является самоисключающим, так что только один сеанс может удерживать его одновременно.

Приобретено CREATE TRIGGER и некоторыми формами ALTER TABLE (см. ALTER TABLE ).

EXCLUSIVE

Конфликты с режимами блокировки ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим допускает только одновременные блокировки ACCESS SHARE, т. ACCESS SHARE Только чтение из таблицы может выполняться параллельно с транзакцией, удерживающей этот режим блокировки.

Приобретен REFRESH MATERIALIZED VIEW CONCURRENTLY .

ACCESS EXCLUSIVE

Конфликтует с блокировками всех режимов ( ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE ). Этот режим гарантирует, что держатель является единственной транзакцией, которая имеет доступ к таблице любым способом.

Получено REINDEX DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL и REFRESH MATERIALIZED VIEW (без CONCURRENTLY ). Многие формы ALTER INDEX и ALTER TABLE также получают блокировку на этом уровне. Это также режим блокировки по умолчанию для операторов LOCK TABLE которые не указывают режим явно.

Заметка
Только блокировка ACCESS EXCLUSIVE блокирует SELECT (без FOR UPDATE/SHARE).

После получения блокировка обычно удерживается до конца транзакции. Но если блокировка получена после установления точки сохранения, блокировка снимается немедленно, если точка отката возвращается к. Это согласуется с принципом, что ROLLBACK отменяет все эффекты команд, начиная с точки сохранения. То же самое относится и к блокировкам, полученным в блоке исключений PL/pgSQL: сбой ошибки из блока освобождает блокировки, полученные внутри него.

Таблица 2. Конфликтующие режимы блокировки

Запрошенный
режим
блокировки

Текущий режим блокировки

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

Блокировки на уровне строк

В дополнение к блокировкам на уровне таблиц существуют блокировки на уровне строк, которые перечислены ниже с контекстами, в которых они автоматически используются QHB. См. Таблицу 3 для полной таблицы конфликтов блокировки на уровне строк. Обратите внимание, что транзакция может содержать конфликтующие блокировки в одной и той же строке, даже в разных субтранзакциях; но кроме этого, две транзакции не могут содержать конфликтующие блокировки в одной строке. Блокировки на уровне строк не влияют на запросы данных; они блокируют только писателей и шкафчиков в одном ряду.

Режимы блокировки на уровне строк

FOR UPDATE

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

QHB не запоминает какую-либо информацию об измененных строках в памяти, поэтому не существует ограничений на количество строк, заблокированных за один раз. Однако блокировка строки может привести к записи на диск, например, SELECT FOR UPDATE изменяет выбранные строки, чтобы пометить их как заблокированные, и, следовательно, приведет к записи на диск.

Таблица 3. Конфликтующие блокировки на уровне строк

Запрошенный режим блокировки Текущий режим блокировки
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

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

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

Взаимные блокировки

Использование явной блокировки может увеличить вероятность взаимоблокировок, при этом каждая из двух (или более) транзакций удерживает блокировки, которые нужны другой. Например, если транзакция 1 получает эксклюзивную блокировку для таблицы A, а затем пытается получить эксклюзивную блокировку для таблицы B, в то время как транзакция 2 уже имеет эксклюзивную блокировку таблицы B и теперь хочет получить эксклюзивную блокировку для таблицы A, ни одна из них не может продолжить, QHB автоматически обнаруживает тупиковые ситуации и разрешает их, прерывая одну из задействованных транзакций, позволяя другим завершить. (Точно, какая транзакция будет прервана, трудно предсказать, и на нее не следует полагаться.)

Обратите внимание, что взаимные блокировки также могут возникать в результате блокировок на уровне строк (и, следовательно, они могут возникать, даже если явная блокировка не используется). Рассмотрим случай, когда две параллельные транзакции изменяют таблицу. Первая транзакция выполняет:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;

Это получает блокировку на уровне строк в строке с указанным номером учетной записи. Затем вторая транзакция выполняет:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;

Первый оператор UPDATE успешно получает блокировку на уровне строк в указанной строке, поэтому он успешно обновляет эту строку. Однако второй оператор UPDATE обнаруживает, что строка, которую он пытается обновить, уже заблокирована, поэтому он ожидает завершения транзакции, получившей блокировку. Вторая транзакция теперь ожидает завершения первой транзакции, прежде чем продолжить выполнение. Теперь транзакция 1 выполняет:

UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;

Транзакция 1 пытается получить блокировку на уровне строки в указанной строке, но не может: транзакция 2 уже удерживает такую блокировку. Поэтому он ожидает завершения транзакции 2. Таким образом, транзакция 1 блокируется в транзакции 2, а транзакция 2 блокируется в транзакции 1: условие взаимоблокировки. QHB обнаружит эту ситуацию и прервет одну из транзакций.

Лучшая защита от взаимных блокировок, как правило, состоит в том, чтобы избежать их, будучи уверенным, что все приложения, использующие базу данных, получают блокировки для нескольких объектов в согласованном порядке. В приведенном выше примере, если обе транзакции обновили строки в одном и том же порядке, тупиковая ситуация не возникла бы. Следует также убедиться, что первая блокировка, полученная для объекта в транзакции, является наиболее ограничительным режимом, который будет необходим для этого объекта. Если это невозможно проверить заранее, то взаимные блокировки могут быть обработаны на лету, повторяя транзакции, которые прерываются из-за взаимных блокировок.

До тех пор, пока не будет обнаружена ситуация взаимоблокировки, транзакция, ищущая блокировку на уровне таблицы или на уровне строки, будет бесконечно долго ожидать освобождения конфликтующих блокировок. Это означает, что для приложений плохая идея держать транзакции открытыми в течение длительных периодов времени (например, во время ожидания ввода данных пользователем).

Консультативные блокировки

QHB предоставляет средства для создания блокировок, которые имеют значения, определенные приложением. Они называются консультативными блокировками, потому что система не навязывает их использование - это зависит от приложения, чтобы использовать их правильно. Консультативные блокировки могут быть полезны для стратегий блокировки, которые неудобно подходят для модели MVCC. Например, обычное использование консультативных блокировок состоит в том, чтобы эмулировать стратегии пессимистической блокировки, типичные для так называемых систем управления данными « плоских файлов ». Хотя флаг, хранящийся в таблице, можно использовать для той же цели, консультативные блокировки быстрее, избегают раздувания таблиц и автоматически очищаются сервером в конце сеанса.

Есть два способа получить консультативную блокировку в QHB : на уровне сеанса или на уровне транзакции. После получения на уровне сеанса консультативная блокировка удерживается до тех пор, пока явно не будет снята или сеанс не завершится. В отличие от стандартных запросов на блокировку, запросы на консультативную блокировку на уровне сеанса не учитывают семантику транзакции: блокировка, полученная во время обратной транзакции, будет по-прежнему удерживаться после отката, и аналогично разблокировка эффективна, даже если вызывающая транзакция завершится неудачей позже. Блокировка может быть получена несколько раз в процессе ее владения; для каждого выполненного запроса на блокировку должен быть соответствующий запрос на разблокировку до фактического снятия блокировки. С другой стороны, запросы на блокировку на уровне транзакции ведут себя больше как обычные запросы на блокировку: они автоматически освобождаются в конце транзакции и явной операции разблокировки не выполняется. Такое поведение часто более удобно, чем поведение на уровне сеанса, для кратковременного использования консультативной блокировки. Запросы на уровне сеанса и на уровне транзакции для одного и того же идентификатора консультативной блокировки будут блокировать друг друга ожидаемым образом. Если сеанс уже удерживает данную консультативную блокировку, дополнительные запросы от него всегда будут успешными, даже если другие сеансы ожидают блокировки; это утверждение верно независимо от того, находится ли существующая блокировка блокировки и новый запрос на уровне сеанса или уровне транзакции.

Как и все блокировки в QHB, полный список консультативных блокировок, удерживаемых в настоящее время любым сеансом, можно найти в системном представлении pg_locks .

Как консультативные, так и обычные блокировки хранятся в пуле общей памяти, размер которой определяется переменными конфигурации max_locks_per_transaction и max_connections. Необходимо соблюдать осторожность, чтобы не исчерпать эту память, иначе сервер вообще не сможет предоставить никаких блокировок. Это накладывает верхний предел на количество консультативных блокировок, предоставляемых сервером, обычно в пределах от десятков до сотен тысяч в зависимости от того, как настроен сервер.

В некоторых случаях, используя рекомендательные методы блокировки, особенно в запросах, включающих явное упорядочение и предложения LIMIT, необходимо соблюдать осторожность, чтобы контролировать блокировки, полученные из-за порядка, в котором оцениваются выражения SQL. Например:

SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger!
SELECT pg_advisory_lock(q.id) FROM
(
  SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok

В приведенных выше запросах вторая форма опасна, поскольку не гарантируется применение LIMIT до выполнения функции блокировки. Это может привести к получению некоторых блокировок, которые приложение не ожидает, и, следовательно, не сможет освободить (пока не завершится сеанс). С точки зрения приложения, такие блокировки были бы висячими, хотя все еще видимыми в pg_locks.

Функции, предоставляемые для управления консультативными блокировками, описаны в разделе Функции консультативных блокировок.

Проверка согласованности данных на уровне приложения

Очень сложно обеспечить соблюдение бизнес-правил в отношении целостности данных с использованием транзакций Read Committed, поскольку представление данных смещается с каждым оператором, и даже один оператор не может ограничиваться снимком оператора, если возникает конфликт записи.

Хотя транзакция Repeatable Read имеет стабильное представление данных на протяжении всего своего выполнения, существует небольшая проблема с использованием моментальных снимков MVCC для проверок согласованности данных, связанных с так называемыми конфликтами чтения / записи. Если одна транзакция записывает данные, а параллельная транзакция пытается прочитать те же данные (до или после записи), она не может увидеть работу другой транзакции. Читатель тогда, кажется, выполнил сначала независимо от того, который начался первым или который совершил первым. Если это так далеко, это не проблема, но если считыватель также записывает данные, которые считываются параллельной транзакцией, то теперь есть транзакция, которая, кажется, выполнялась перед любой из ранее упомянутых транзакций. Если транзакция, которая, по-видимому, выполнила последнюю, на самом деле фиксируется первой, цикл очень легко отобразить на графике порядка выполнения транзакций. Когда появляется такой цикл, проверки целостности не будут работать правильно без посторонней помощи.

Как показано в разделе Подзапросы, сериализуемые транзакции - это просто повторяющиеся транзакции чтения, которые добавляют неблокирующий мониторинг опасных шаблонов конфликтов чтения / записи. Когда обнаружен шаблон, который может вызвать цикл в видимом порядке выполнения, одна из задействованных транзакций откатывается, чтобы прервать цикл.

Обеспечение согласованности с сериализуемыми транзакциями

Если уровень изоляции Сериализуемой транзакции используется для всех записей и для всех операций чтения, которые требуют согласованного просмотра данных, никаких других усилий не требуется для обеспечения согласованности. Программное обеспечение из других сред, написанное для использования сериализуемых транзакций для обеспечения согласованности, должно « просто работать » в этом отношении в QHB .

При использовании этого метода, это позволит избежать ненужной нагрузки для прикладных программистов, если прикладное программное обеспечение проходит через среду, которая автоматически повторяет транзакции, которые откатываются с ошибкой сериализации. Это может быть хорошей идеей, чтобы установить default_transaction_isolation в serializable. Также было бы целесообразно предпринять некоторые действия, чтобы гарантировать, что никакой другой уровень изоляции транзакции не используется, либо непреднамеренно, либо для подрыва проверок целостности, посредством проверок уровня изоляции транзакции в триггерах.

Смотрите главу Подзапросы для предложений по производительности.

!!! Предупреждение

Этот уровень защиты целостности с использованием сериализуемых
транзакций еще не распространяется на режим горячего резервирования.
Из-за этого те, кто использует горячее резервирование,
могут хотеть использовать *Repeatable Read* и явную блокировку на главном.

Обеспечение согласованности с помощью явных блокирующих блокировок

Когда возможны несериализуемые записи, чтобы обеспечить текущую достоверность строки и защитить ее от одновременных обновлений, необходимо использовать SELECT FOR UPDATE, SELECT FOR SHARE или соответствующую инструкцию LOCK TABLE. ( SELECT FOR UPDATE и SELECT FOR SHARE блокируют только возвращенные строки от одновременных обновлений, в то время как LOCK TABLE блокирует всю таблицу.) Это следует учитывать при переносе приложений на QHB из других сред.

Для тех, кто конвертирует из других сред, также следует отметить тот факт, что SELECT FOR UPDATE не гарантирует, что параллельная транзакция не будет обновлять или удалять выбранную строку. Для этого в QHB вы должны обновить строку, даже если значения не нужно менять. SELECT FOR UPDATE временно блокирует другие транзакции от получения той же блокировки или выполнения UPDATE или DELETE которые могут повлиять на заблокированную строку, но как только транзакция, удерживающая эту блокировку, фиксирует или откатывает назад, заблокированная транзакция продолжит конфликтующую операцию, если только фактическое UPDATE ряда был выполнен, пока замок удерживался.

Глобальные проверки достоверности требуют дополнительного внимания при несериализуемых MVCC. Например, банковское приложение может захотеть проверить, что сумма всех кредитов в одной таблице равна сумме дебетов в другой таблице, когда обе таблицы активно обновляются. Сравнение результатов двух последовательных команд SELECT sum(...) не будет надежно работать в режиме Read Committed, поскольку второй запрос, скорее всего, будет включать результаты транзакций, не учитываемые первой. Выполнение двух сумм в одной повторяемой транзакции чтения даст точную картину только последствий транзакций, совершенных до начала повторяемой транзакции чтения, но можно с полным основанием задаться вопросом, актуален ли ответ к моменту его доставки. Если повторяемая транзакция чтения сама применила некоторые изменения перед попыткой проверки согласованности, полезность проверки становится еще более дискуссионной, поскольку теперь она включает некоторые, но не все изменения после запуска транзакции. В таких случаях осторожный человек может захотеть заблокировать все таблицы, необходимые для проверки, чтобы получить бесспорную картину текущей реальности. Блокировка в режиме SHARE (или выше) гарантирует отсутствие незафиксированных изменений в заблокированной таблице, кроме изменений в текущей транзакции.

Также обратите внимание, что если кто-то полагается на явную блокировку для предотвращения одновременных изменений, следует либо использовать режим Read Committed, либо в режиме Repeatable Read, будьте осторожны, чтобы получить блокировки перед выполнением запросов. Блокировка, полученная повторяемой транзакцией чтения, гарантирует, что никакие другие транзакции, модифицирующие таблицу, все еще не запущены, но если моментальный снимок, видимый транзакцией, предшествует получению блокировки, он может предшествовать некоторым уже зафиксированным изменениям в таблице. Снимок повторяющейся транзакции чтения фактически замораживается в начале первого запроса или команды изменения данных ( SELECT, INSERT, UPDATE или DELETE), поэтому можно явно получить блокировки до того, как моментальный снимок будет заморожен.

Ограничения

Некоторые команды DDL, в настоящее время только TRUNCATE и формы переписывания таблиц ALTER TABLE, не являются безопасными для MVCC. Это означает, что после фиксации усечения или перезаписи таблица будет отображаться пустой для одновременных транзакций, если они используют снимок, сделанный до принятия команды DDL. Это будет проблемой только для транзакции, которая не обращалась к рассматриваемой таблице до запуска команды DDL - любая сделавшая транзакция содержала бы по меньшей мере блокировку таблицы ACCESS SHARE, которая блокировала бы команду DDL до завершения этой транзакции. Таким образом, эти команды не вызовут каких-либо явных несоответствий в содержании таблицы для последовательных запросов к целевой таблице, но они могут вызвать видимое несоответствие между содержимым целевой таблицы и другими таблицами в базе данных.

Поддержка уровня изоляции Serializable транзакций еще не была добавлена к целям репликации с горячим резервированием. Самым строгим уровнем изоляции, поддерживаемым в настоящее время в режиме горячего резервирования, является Repeatable Read. Хотя выполнение всех постоянных операций записи в базу данных в рамках сериализуемых транзакций на главном сервере гарантирует, что все резервные серверы в конечном итоге достигнут согласованного состояния, транзакция повторного чтения, выполняемая в режиме ожидания, может иногда видеть переходное состояние, несовместимое с любым последовательным выполнением транзакций на мастере.

Блокировка и индексы

Хотя QHB обеспечивает блокирующий доступ на чтение / запись к данным таблицы, блокирующему доступ на чтение / записи в настоящее время не предлагаются для каждого метода доступа индекса осуществляется в QHB. Различные типы индексов обрабатываются следующим образом:

ИндексыОписание
ИндексыКраткосрочные блокировки / эксклюзивные блокировки на уровне страниц используются для доступа на чтение / запись. Блокировки снимаются сразу после извлечения или вставки каждой строки индекса. Эти типы индексов обеспечивают максимальный параллелизм без условий взаимоблокировки.
Хеш-индексыСовместно используемые / эксклюзивные блокировки на уровне хеш-сегмента используются для доступа на чтение / запись. Замки снимаются после обработки всего ведра. Блокировки уровня сегмента обеспечивают лучший параллелизм, чем блокировки уровня индекса, но возможна взаимоблокировка, так как блокировки удерживаются дольше, чем одна операция индекса.
GIN индексыКраткосрочные блокировки / эксклюзивные блокировки на уровне страниц используются для доступа на чтение / запись. Блокировки снимаются сразу после извлечения или вставки каждой строки индекса. Но обратите внимание, что вставка GIN-индексированного значения обычно приводит к нескольким вставкам индексного ключа на строку, поэтому GIN может выполнять существенную работу для вставки одного значения.

В настоящее время B-деревья предлагают лучшую производительность для параллельных приложений; поскольку они также имеют больше возможностей, чем хеш-индексы, они являются рекомендуемым типом индекса для параллельных приложений, которым необходимо индексировать скалярные данные. При работе с нескалярными данными B-деревья бесполезны, и вместо них следует использовать индексы GiST, SP-GiST или GIN.