Управление параллельным доступом

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



Введение

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

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

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



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

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

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

  • «Грязное» чтение (dirty read)

    Транзакция считывает данные, записанные параллельной незафиксированной транзакцией.

  • Неповторяемое чтение (nonrepeatable read)

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

  • Фантомное чтение (phantom read)

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

  • Аномалии сериализации (serialization anomaly)

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

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

Таблица 1. Уровни изоляции транзакций

Уровень изоляцииГрязное чтениеНеповторяемое чтениеФантомное чтениеАномалия сериализации
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 видит и результаты предыдущих изменений, произошедших в его собственной транзакции, даже если они еще не зафиксированы. Также обратите внимание, что две последовательные команды SELECT могут видеть разные данные (даже если они выполняются в рамках одной транзакции), если другие транзакции фиксируют изменения данных после запуска первой SELECT и до запуска второй.

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

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

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

MERGE позволяет пользователю указывать различные комбинации подкоманд INSERT, UPDATE и DELETE. Команда MERGE с подкомандами INSERT и UPDATE похожа на INSERT с предложением ON CONFLICT DO UPDATE, но не гарантирует, что команда INSERT или UPDATE будет осуществлена. Если MERGE пытается выполнить UPDATE или DELETE, и строка параллельно изменяется, но условие соединения по-прежнему выполняется для текущего целевого и текущего исходного кортежа, то MERGE будет вести себя так же, как команды UPDATE или DELETE, и произведет свои действия с измененной версией строки. Однако поскольку в MERGE можно задать несколько действий, и они могут быть условными, условия для всех действий вычисляются заново для измененной версии строки, начиная с первого действия, даже если действие, которое подходило изначально, фигурирует в списке действий позже. С другой стороны, если строка параллельно изменяется или удаляется так, что условие соединения не выполняется, тогда MERGE будет вычислять условия действий NOT MATCHED и выполнит действие с первым подходящим условием. Если MERGE пытается выполнить INSERT при наличии уникального индекса, и параллельно добавляется строка, то возникает ошибка нарушения уникальности; MERGE не пытается избежать таких ошибок, заново вычисляя условия MATCHED.

В силу вышеприведенных правил изменяющая команда может увидеть снимок несогласованного состояния: она может видеть результаты выполнения параллельных изменяющих команд в той же строке, которую пытается изменить она, но при этом не видит их результаты в других строках в базе данных. Вследствие такого поведения режим 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;
-- выполняется параллельно в другом сеансе: DELETE FROM website WHERE hits = 10;
COMMIT;

Команда DELETE не сделает ничего, даже несмотря на то, что строка website.hits = 10 была в таблице как до, так и после выполнения UPDATE. Это связано с тем, что строка со значением 9 до изменения пропускается, а когда UPDATE завершается и DELETE получает блокировку, новым значением строки становится уже не 10, а 11, которое больше не соответствует критериям.

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

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


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

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

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

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

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

ERROR:  could not serialize access due to concurrent update
-- ОШИБКА: не удалось сериализовать доступ вследствие параллельного изменения

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

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

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

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

Для реализации уровня изоляции Repeatable Read применяется подход, который называется в академической литературе по базам данных и в других СУБД Изоляция снимков (Snapshot Isolation). По сравнению с системами, использующими традиционный метод блокировок, затрудняющий параллельное выполнение, при этом подходе наблюдается другое поведение и другая производительность. В некоторых СУБД могут существовать даже два отдельных уровня Repeatable Read и Snapshot Isolation с различным поведением. Допускаемые особые условия, представляющие отличия двух этих подходов, не были формализованы разработчиками теории БД до развития стандарта SQL, и их рассмотрение выходит за рамки данного руководства. Полностью эта тема рассматривается в техническом докладе «A Critique of ANSI SQL Isolation Levels».


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

Уровень изоляции Serializable обеспечивает самую строгую изоляцию транзакций. Этот уровень эмулирует последовательное выполнение всех зафиксированных транзакций, как если бы транзакции были выполнены одна за другой, последовательно, а не одновременно. Однако, как и на уровне Repeatable Read, на этом уровне приложения должны быть готовы повторить транзакции в случае сбоев сериализации. По сути этот уровень изоляции работает точно так же, как и 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, обеим будет разрешено зафиксироваться; но поскольку результат не соответствует последовательному порядку, использование транзакций в режиме Serializable позволит зафиксировать одну транзакцию и откатит другую с этим сообщением:

ERROR:  could not serialize access due to read/write dependencies among transactions
-- ОШИБКА: не удалось сериализовать доступ вследствие зависимостей чтения/записи между транзакциями

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

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

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

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

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

Хотя уровень изоляции Serializable в 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. Обязательно сопоставьте любое снижение числа откатов и перезапусков транзакций с любым общим изменением времени выполнения запроса, чтобы оценить выгоду.

Для реализации уровня изоляции Serializable применяется подход, который называется в академической литературе по базам данных Сериализуемая изоляция снимков (Serializable Snapshot Isolation), который построен на основе обычной изоляции снимков путем добавления дополнительных проверок на предмет аномалий сериализации. По сравнению с другими системами, использующими традиционный метод блокировок, при этом подходе наблюдается другое поведение и другая производительность. Подробнее это рассматривается в статье «Serializable Snapshot Isolation in PostgreSQL».



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

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

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


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

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

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

  • ACCESS SHARE (AccessShareLock)
    Конфликтует только с режимом блокировки ACCESS EXCLUSIVE.
    Команда SELECT запрашивает этот режим блокировки для ссылочных таблиц. В целом, любой запрос, который только читает таблицу и не изменяет ее, получит этот режим блокировки.

  • ROW SHARE (RowShareLock)
    Конфликтует с режимами блокировки EXCLUSIVE и ACCESS EXCLUSIVE.
    Команда SELECT получает блокировку в этом режиме для всех таблиц, для которых заданы параметры FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE или FOR KEY SHARE (в дополнение к блокировкам ACCESS SHARE для любых других ссылочных таблиц без явного параметра блокировки FOR ...).

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

  • SHARE UPDATE EXCLUSIVE (ShareUpdateExclusiveLock)
    Конфликтует с режимами блокировки SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от параллельного запуска изменений схемы и команды VACUUM.
    Запрашивается командами VACUUM (без FULL), ANALYZE, CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY, CREATE STATISTICS, COMMENT ON, REINDEX CONCURRENTLY, а также некоторыми вариантами ALTER INDEX и ALTER TABLE (подробную информацию см. на справочных страницах этих команд).

  • SHARE (ShareLock)
    Конфликтует с режимами блокировки ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от параллельных изменений данных.
    Запрашивается командой CREATE INDEX (без CONCURRENTLY).

  • SHARE ROW EXCLUSIVE (ShareRowExclusiveLock)
    Конфликтует с режимами блокировки ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим защищает таблицу от параллельных изменений данных и является самоисключающим, поэтому получить его может только один сеанс за раз.
    Запрашивается командой CREATE TRIGGER и некоторыми формами ALTER TABLE.

  • EXCLUSIVE (ExclusiveLock)
    Конфликтует с режимами блокировки ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE. Этот режим допускает только одновременные блокировки ACCESS SHARE, т. к. параллельно с транзакцией, владеющей этим режимом блокировки, может выполняться только чтение из таблицы.
    Запрашивается командой REFRESH MATERIALIZED VIEW CONCURRENTLY.

  • ACCESS EXCLUSIVE (AccessExclusiveLock)
    Конфликтует со всеми режимами блокировки (ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE и ACCESS EXCLUSIVE). Этот режим гарантирует, что только транзакция, владеющая этой блокировкой, имеет доступ к таблице (любым способом).
    Запрашивается командами 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, в настоящий момент желающая получить эксклюзивную блокировку таблицы 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 обнаруживает, что строка, которую она пытается изменить, уже заблокирована, поэтому она ожидает завершения транзакции, получившей блокировку. Теперь уже вторая транзакция ожидает завершения первой, прежде чем продолжить свою работу. Затем первая транзакция выполняет:

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

Первая транзакция пытается получить блокировку на уровне строки в указанной строке, но не может: такой блокировкой уже владеет вторая транзакция. Поэтому она ожидает завершения второй транзакции. Таким образом, первая транзакция блокируется второй, а вторая — первой: возникает взаимоблокировка. 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; -- ок
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- опасно!
SELECT pg_advisory_lock(q.id) FROM
(
  SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ок

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

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



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

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

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

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


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

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

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

Рекомендации по увеличению производительности см. в подразделе Уровень изоляции 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 не будет достоверным, поскольку второй запрос, скорее всего, будет включать результаты транзакций, не учитываемые первой. Подсчет двух сумм в одной транзакции Repeatable Read даст точную картину результатов действий только тех транзакций, которые были зафиксированы до начала данной — но можно с полным основанием задаться вопросом, будет ли ответ по-прежнему актуален на момент его выдачи. Если транзакция Repeatable Read сама внесла какие-то изменения, прежде чем попыталась проверить согласованность, полезность этой проверки становится еще более сомнительной, поскольку теперь она включает некоторые, но не все изменения, случившиеся после запуска транзакции. В таких случаях осмотрительный человек может заблокировать все задействованные в проверке таблицы, чтобы получить бесспорную картину реальной действительности. Блокировка в режиме SHARE (или более строгая) гарантирует отсутствие незафиксированных изменений в заблокированной таблице, за исключением тех, что внесла текущая транзакция.

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



Обработка сбоев сериализации

На уровнях изоляции Repeatable Read и Serializable могут выдаваться ошибки, разработанные для предотвращения аномалий сериализации. Как уже говорилось ранее, приложения, использующие эти уровни, должны быть готовы повторить транзакции, завершившиеся сбоем вследствие ошибок сериализации. Текст такого сообщения об ошибке будет варьироваться в зависимости от конкретных обстоятельств, но он всегда будет иметь код SQLSTATE 40001 (serialization_failure).

Также можно порекомендовать повторять запросы при ошибках взаимоблокировки. Их код SQLSTATE 40P01 (deadlock_detected).

В некоторых случаях также имеет смысл повторять запросы при ошибках нарушения уникальности ключа, которые имеют код SQLSTATE 23505 (unique_violation), и ошибках нарушения исключающего ограничения, которые имеют код SQLSTATE 23P01 (exclusion_violation). К примеру, если приложение выбирает новое значение для столбца первичного ключа после проверки сохраненных на данным момент ключей, оно может получить ошибку нарушения уникальности ключа, поскольку другой экземпляр этого же приложения параллельно выбрал такой же новый ключ. По сути, это сбой сериализации, но сервер не обнаруживает его как таковой, поскольку не может «видеть» связь между добавляемым значением и предыдущими чтениями. Есть также некоторые патологические случаи, когда сервер выдает ошибку нарушения уникальности ключа или исключающего ограничения, несмотря на то, что у него в принципе достаточно информации, чтобы определить, что первопричиной является проблема сериализации. Хотя запросы с ошибками serialization_failure рекомендуется просто безусловно повторять, при повторении запросов с другими кодами ошибок следует соблюдать осторожность, поскольку они могут указывать на присутствие постоянных состояний ошибки, а не временных сбоях.

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

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



Ограничения применимости

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

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

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



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

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

Индексы B-деревья, GiST и SP-GiST

Для доступа на чтение/запись используются краткосрочные совместные/эксклюзивные блокировки на уровне страницы. Блокировки освобождаются сразу после извлечения или добавления каждой строки индекса. Эти типы индексов обеспечивают максимальный параллелизм операций без возникновения условий взаимоблокировки.

Хеш-индексы

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

Индексы GIN

Для доступа на чтение/запись используются краткосрочные совместные/эксклюзивные блокировки на уровне страницы. Блокировки освобождаются сразу после извлечения или добавления каждой строки индекса. Но обратите внимание, что добавление значения в поле с индексом GIN обычно приводит к нескольким добавлениям ключа индекса в каждую строку, поэтому GIN может проделывать значительную работу для вставки всего одного значения.
.

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