В SQL стандарте определены четыре уровня изоляции транзакций. Наиболее строгим является уровень изоляции Serializable, который определяется в стандарте положением о том, что любое параллельное выполнение набора транзакций с уровнем изоляции Serializable гарантированно дает тот же результат, что и их выполнение по очереди в некотором порядке. Остальные три уровня определяются через нежелательные явления, возникающие в результате взаимодействия параллельных транзакций, которые не должны допускаться на конкретном уровне. В стандарте отмечается, что в силу определения уровня изоляции Serializable ни одно из этих явлений невозможно на данном уровне. (Это вполне закономерно: если результат выполнения транзакций должен соответствовать их последовательному выполнению, то возникновение каких-либо явлений, вызванных их взаимодействием, исключено.)
К явлениям, запрещенным на различных уровнях изоляции, относятся:
Транзакция считывает данные, записанные параллельной незавершенной транзакцией.
Транзакция повторно считывает ранее прочитанные данные и обнаруживает, что эти данные были изменены другой транзакцией (зафиксированной после первоначального чтения).
Транзакция повторно выполняет запрос, возвращающий набор строк, которые отвечают условию поиска, и обнаруживает, что этот набор строк изменился вследствие фиксации другой транзакции.
Результат успешной фиксации группы транзакций не соответствует ни одному из возможных вариантов последовательного выполнения этих транзакций.
Уровни изоляции транзакций, определенные в стандарте SQL и реализованные в PostgreSQL, описаны в Таблица 2.10.1.
Таблица 2.10.1. Уровни изоляции транзакций
| Уровень изоляции | Грязное чтение | Неповторяющееся чтение | Фантомное чтение | Аномалия сериализации |
|---|---|---|---|---|
| уровень изоляции Read Uncommitted | Допускается, но не в PostgreSQL | Возможно | Возможно | Возможно |
| уровень изоляции Read Committed | Невозможно | Возможно | Возможно | Возможно |
| уровень изоляции Repeatable Read | Невозможно | Невозможно | Допускается, но не в PostgreSQL | Возможно |
| уровень изоляции Serializable | Невозможно | Невозможно | Невозможно | Невозможно |
В Digital Q.DataBasePostgreSQL можно запросить любой из четырех стандартных уровней изоляции транзакций, однако внутренне реализованы только три различных уровня изоляции, то есть режим Read Uncommitted в PostgreSQL работает аналогично уровню изоляции Read Committed. Это обусловлено тем, что такой подход является единственным разумным способом сопоставления стандартных уровней изоляции с архитектурой управления одновременным доступом PostgreSQL на основе многоверсионности.
В таблице также указано, что реализация уровня изоляции Repeatable Read в PostgreSQL не допускает возникновения фантомного чтения. Это допустимо согласно стандарту SQL, поскольку стандарт определяет, какие аномалии не должны возникать на определенных уровнях изоляции; допускаются более строгие гарантии. Поведение доступных уровней изоляции подробно описано в следующих подразделах.
Для установки уровня изоляции транзакций используется команда SET TRANSACTION.
Некоторые Digital Q.DataBase типы данных и функции имеют
специальные правила транзакционного поведения. В частности, изменения,
внесенные в последовательность (и, следовательно, в счетчик
столбца, объявленного с использованием serial), становятся немедленно видимыми
всем остальным транзакциям и не откатываются в случае, если транзакция,
выполнившая эти изменения, отменяется. См. Раздел 2.6.17
и Раздел 2.5.1.4.
уровень изоляции Read Committed является уровнем изоляции
по умолчанию в Digital Q.DataBase. Когда транзакция
использует данный уровень изоляции, SELECT запрос
(без FOR UPDATE/SHARE ) видит только те данные, которые были зафиксированы до начала выполнения запроса; он не видит ни незафиксированных данных, ни изменений, внесенных параллельными транзакциями во время выполнения самого запроса. Фактически, SELECT запрос видит снимок базы данных на момент начала своего выполнения. Однако 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 Committed для каждой строки, предлагаемой для вставки, будет выполнена либо операция вставки, либо операция обновления. При отсутствии несвязанных ошибок один из этих двух результатов гарантирован. Если конфликт вызван другой транзакцией, результаты которой еще не видны 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 BY SOURCE и
NOT MATCHED [BY TARGET] следующие действия и выполнить первое из них каждого вида, которое завершится успешно. Если строка одновременно удаляется, то MERGE
будет вычислять параметры команды NOT MATCHED [BY TARGET]
действия и выполнит первое из них, завершившееся успешно.
Если MERGE пытается выполнить INSERT
а при наличии уникального индекса одновременно вставляется дублирующая строка, возникает ошибка нарушения уникальности;
MERGE не пытается избежать таких ошибок путем повторного вычисления MATCHED
условий.
В силу вышеуказанных правил команда обновления может видеть несогласованный снимок: она может видеть результаты параллельных команд обновления для тех же строк, которые она пытается обновить, но не видит влияния этих команд на другие строки в базе данных. Данное поведение делает режим Read Committed непригодным для команд, содержащих сложные условия поиска; однако он вполне подходит для более простых случаев. Например, рассмотрим обновление банковских балансов с помощью транзакций следующего вида:
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 = 10 строки до и после UPDATE. Это происходит потому, что значение строки до обновления 9 пропускается, и когда
UPDATE завершается и DELETE
получает блокировку, новое значение строки уже не равно 10 а составляет
11, что более не соответствует критериям.
Поскольку режим Read Committed начинает выполнение каждой команды с нового снимка, включающего все транзакции, зафиксированные к этому моменту, последующие команды в той же транзакции в любом случае будут видеть результаты параллельно зафиксированной транзакции. Рассматриваемый выше вопрос заключается в том, видит ли отдельная команда видит абсолютно согласованное представление базы данных.
Частичный уровень изоляции транзакций, обеспечиваемый режимом Read Committed, является достаточным для многих приложений; данный режим прост в использовании и обладает высоким быстродействием. Однако его возможностей не всегда достаточно. Приложениям, выполняющим сложные запросы и операции обновления, может потребоваться более строгое согласованное представление данных, чем то, которое обеспечивает режим Read Committed.
В уровень изоляции Repeatable Read видит только те данные, которые были зафиксированы до начала транзакции; он никогда не видит ни незафиксированных данных, ни изменений, внесенных параллельными транзакциями во время выполнения текущей транзакции. (Тем не менее, каждый запрос видит результаты предыдущих обновлений, выполненных в рамках его собственной транзакции, даже если они еще не зафиксированы.) Это более строгая гарантия, чем та, которую требует SQL стандарт для данного уровня изоляции; это предотвращает все феномены, описанные в Таблица 2.10.1 за исключением аномалий сериализации. Как упоминалось выше, это прямо допускается стандартом, который описывает только минимальные требования к защите, которые должен обеспечивать каждый уровень изоляции.
Данный уровень отличается от уровня изоляции Read Committed тем, что запрос в транзакции с уровнем изоляции Repeatable Read видит снимок данных на момент начала выполнения первой инструкции, не относящейся к управлению транзакциями, в
транзакция, а не на момент начала
текущей команды внутри транзакции. Таким образом, последовательные
SELECT команды в рамках отдельная
транзакции видят одни и те же данные, то есть они не видят изменений, внесенных другими транзакциями, которые были зафиксированы после начала текущей транзакции.
Приложения, использующие данный уровень, должны предусматривать повторный запуск транзакций при возникновении ошибки сериализации.
UPDATE, DELETE,
MERGE, SELECT FOR UPDATE,
и SELECT FOR SHARE команды
ведут себя так же, как SELECT
в части поиска целевых строк: будут найдены только те целевые строки, которые были зафиксированы на момент начала транзакции. Однако к моменту обнаружения такая целевая строка уже может быть изменена (удалена или заблокирована) другой параллельной транзакцией. В этом случае транзакция с уровнем изоляции Repeatable Read будет ожидать фиксации или отката первой обновляющей транзакции (если та еще выполняется). Если первая транзакция выполнит откат,
то ее изменения будут аннулированы, и транзакция с уровнем изоляции Repeatable Read сможет продолжить
обновление изначально найденной строки. Если же первая транзакция зафиксирует изменения
(и фактически обновит или удалит строку, а не просто заблокирует ее),
то транзакция с уровнем изоляции Repeatable Read будет откачена с выводом сообщения
ОШИБКА: не удалось сериализовать доступ из-за параллельного обновления
поскольку транзакция с уровнем изоляции Repeatable Read не может изменять или блокировать строки, которые были изменены другими транзакциями после начала данной транзакции с уровнем изоляции Repeatable Read.
При получении этого сообщения об ошибке приложению следует прервать текущую транзакцию и повторить выполнение всей транзакции с самого начала. При повторном проходе транзакция увидит ранее зафиксированное изменение как часть своего исходного снимка базы данных, поэтому логический конфликт при использовании новой версии строки в качестве начальной точки для обновления в новой транзакции отсутствует.
Следует отметить, что в повторном выполнении могут нуждаться только обновляющие транзакции; транзакции, работающие только на чтение, никогда не сталкиваются с конфликтами сериализации.
Уровень изоляции Repeatable Read предоставляет строгую гарантию того, что каждая транзакция видит полностью стабильный снимок базы данных. Тем не менее, данное представление не всегда будет соответствовать некоторому последовательному (поочередному) выполнению параллельных транзакций того же уровня. Например, даже транзакция в режиме «только чтение» на данном уровне может увидеть, что контрольная запись была обновлена для подтверждения завершения пакета, но не должны видит одну из детализированных записей, которая логически является частью пакета, поскольку была прочитана более ранняя редакция контрольной записи. Попытки обеспечить соблюдение бизнес-правил транзакциями, работающими на данном уровне изоляции, вряд ли будут успешными без тщательного использования явных блокировок для предотвращения конфликтов.
Уровень изоляции Repeatable Read реализован с использованием метода, известного в академической литературе по базам данных и в некоторых других СУБД как изоляция на основе снимков. Могут наблюдаться различия в поведении и производительности по сравнению с системами, использующими традиционный метод блокировок, ограничивающий уровень одновременного доступа. Некоторые другие системы могут даже предлагать уровень изоляции Repeatable Read и изоляцию на основе снимков как отдельные уровни изоляции с различным поведением. Допустимые феномены, различающие эти два метода, не были формализованы исследователями баз данных до разработки стандарта SQL и выходят за рамки данного руководства. Для получения полной информации, пожалуйста, обратитесь к [berenson95].
До Digital Q.DataBase До версии 9.1 запрос на уровень изоляции транзакций Serializable обеспечивал в точности то же поведение, которое описано здесь. Для сохранения прежнего поведения уровня Serializable теперь следует запрашивать уровень изоляции Repeatable Read.
В уровень изоляции Serializable данный уровень изоляции обеспечивает наиболее строгую изоляцию транзакций. Данный уровень эмулирует последовательное выполнение всех зафиксированных транзакций так, как если бы они выполнялись поочередно, одна за другой, а не одновременно. Однако, как и при использовании уровня изоляции Repeatable Read, приложения, использующие данный уровень, должны быть готовы к повторному выполнению транзакций из-за ошибки сериализации. Фактически этот уровень изоляции работает точно так же, как Repeatable Read, за исключением того, что он дополнительно отслеживает условия, при которых выполнение набора параллельных транзакций с уровнем изоляции Serializable может привести к результату, не соответствующему любому возможному последовательному (поочередному) выполнению этих транзакций. Данный мониторинг не вводит никаких дополнительных блокировок помимо тех, что присутствуют в уровне repeatable read, однако он требует определенных накладных расходов, а обнаружение условий, которые могут вызвать аномалия сериализации вызовет ошибку сериализации.
В качестве примера
рассмотрим таблицу mytab, изначально содержащую:
class | value
-------+-------
1 | 10
1 | 20
2 | 100
2 | 200
Предположим, что транзакция А с уровнем изоляции Serializable выполняет расчет:
SELECT SUM(value) FROM mytab WHERE class = 1;
и затем вставляет результат (30) как значение value в
новую строку с классом class = 2. Одновременно с этим транзакция B с уровнем изоляции Serializable выполняет расчет:
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; аналогичным образом иной порядок выполнения привел бы к вычислению другой суммы транзакцией A.
При использовании транзакций с уровнем изоляции Serializable для предотвращения аномалий важно, чтобы любые данные, считанные из постоянной пользовательской таблицы, не считались достоверными до тех пор, пока считавшая их транзакция не будет успешно зафиксирована. Это справедливо даже для транзакций только для чтения, за исключением того, что данные, считанные в рамках deferrable транзакции только для чтения, признаются достоверными сразу после считывания, так как подобная транзакция ожидает возможности получения снимка, гарантированно свободного от таких проблем, прежде чем начать чтение любых данных. Во всех остальных случаях приложения не должны полагаться на результаты, полученные в ходе транзакции, которая позже была прервана; вместо этого следует повторять транзакцию до тех пор, пока она не будет успешно завершена.
Для обеспечения истинной сериализуемости Digital Q.DataBase
использует предикатные блокировки, что означает удержание блокировок, позволяющих определить, повлияла бы операция записи на результат предшествующего чтения из параллельной транзакции, если бы эта запись была выполнена первой. В Digital Q.DataBase данные блокировки не вызывают блокирования и, следовательно, не могут не должны играть никакой роли в возникновении взаимных блокировок. Они используются для идентификации и пометки зависимостей между параллельными транзакциями с уровнем изоляции Serializable, которые в определенных сочетаниях могут приводить к аномалиям сериализации. Напротив, транзакции с уровнем изоляции Read Committed или транзакции с уровнем изоляции Repeatable Read, для которой необходимо обеспечить согласованность данных, может потребоваться установка блокировки на всю таблицу, что может заблокировать работу других пользователей с этой таблицей, либо она может использовать SELECT FOR
UPDATE или SELECT FOR SHARE , что может не только блокировать другие транзакции, но и вызывать обращения к диску.
Предикатные блокировки в Digital Q.DataBase, как и в большинстве других систем баз данных, основаны на данных, к которым фактически обращается транзакция. Они будут отображаться в
pg_locks
системном представлении с режимом SIReadLock. Конкретные блокировки, устанавливаемые в процессе выполнения запроса, зависят от используемого плана запроса; при этом несколько мелкогранулярных блокировок (например, блокировки кортежей) могут объединяться в меньшее число более крупных блокировок (например, блокировки страниц) в ходе выполнения транзакции для предотвращения исчерпания памяти, выделенной для отслеживания блокировок. Транзакция READ ONLY может освободить свои блокировки SIRead до своего завершения, если обнаружит, что возникновение конфликтов, способных привести к аномалии сериализации, более невозможно. Фактически, READ ONLY транзакции часто могут подтвердить этот факт при запуске и избежать установки каких-либо предикатных блокировок. Если явно запрашивается транзакция SERIALIZABLE READ ONLY DEFERRABLE
транзакция, она будет заблокирована до тех пор, пока не сможет подтвердить этот факт. (Это только случай, когда транзакции с уровнем изоляции Serializable блокируются, в то время как транзакции с уровнем изоляции Repeatable Read — нет.) С другой стороны, блокировки SIRead часто необходимо удерживать после фиксации транзакции до тех пор, пока не завершатся перекрывающиеся транзакции чтения-записи.
Последовательное использование транзакций с уровнем изоляции Serializable может упростить разработку. Гарантия того, что любой набор успешно зафиксированных параллельных транзакций с уровнем изоляции Serializable приведет к тому же результату, как если бы они выполнялись поочередно, означает следующее: если можно доказать, что отдельная транзакция в исходном виде работает корректно при изолированном выполнении, то можно быть уверенным, что она будет работать корректно в любом сочетании транзакций с уровнем изоляции Serializable (даже без информации о действиях других транзакций) или же она не будет успешно зафиксирована. Важно, чтобы среда, использующая данный метод, имела универсальный механизм обработки ошибок сериализации (которые всегда возвращаются с кодом SQLSTATE '40001'), так как крайне сложно точно предсказать, какие именно транзакции могут создать зависимости по чтению/записи и потребовать отката для предотвращения аномалий сериализации. Мониторинг зависимостей чтения/записи влечет за собой определенные затраты, равно как и перезапуск транзакций, прерванных из-за ошибки сериализации; однако при сопоставлении с издержками и блокировками, характерными для использования явных блокировок и SELECT FOR UPDATE или SELECT FOR
SHARE, транзакции с уровнем изоляции Serializable являются оптимальным выбором с точки зрения производительности для некоторых сред.
Несмотря на то что Digital Q.DataBaseуровень изоляции транзакций Serializable допускает фиксацию параллельных транзакций только при возможности доказать существование такого последовательного порядка выполнения, который приведет к тому же результату, данный метод не всегда предотвращает появление ошибок, которые не возникли бы при истинном последовательном выполнении. В частности, возможны случаи нарушения ограничений уникальности, вызванные конфликтами с перекрывающимися транзакциями с уровнем изоляции Serializable, даже после явной проверки отсутствия ключа перед попыткой его вставки. Этой ситуации можно избежать, обеспечив, чтобы все транзакции с уровнем изоляции Serializable, вставляющие потенциально конфликтующие ключи, предварительно выполняли явную проверку возможности такой вставки. Например, рассмотрим приложение, которое запрашивает у пользователя новый ключ и проверяет его отсутствие путем предварительной выборки либо генерирует новый ключ, выбирая максимальное значение существующего ключа и прибавляя единицу. Если некоторые транзакции с уровнем изоляции Serializable будут вставлять новые ключи напрямую, не соблюдая данный протокол, могут возникать ошибки нарушения ограничений уникальности даже в тех случаях, когда они были бы невозможны при последовательном выполнении параллельных транзакций.
Для обеспечения оптимальной производительности при использовании транзакций с уровнем изоляции Serializable для управления одновременным доступом следует учитывать следующие аспекты:
Объявляйте транзакции как READ ONLY когда это возможно.
Контролируйте количество активных соединений, используя пул соединений, если это необходимо. Данный аспект всегда важен для производительности, но он может иметь особое значение в высоконагруженных системах, использующих транзакции с уровнем изоляции Serializable.
Не включайте в одну транзакцию больше операций, чем требуется для обеспечения целостности.
Не оставляйте соединения в состоянии «idle in transaction» дольше, чем необходимо. Параметр конфигурации idle_in_transaction_session_timeout может быть использован для автоматического завершения длительных сессий.
Исключите явные блокировки, SELECT FOR UPDATE, и
SELECT FOR SHARE где они более не требуются ввиду того, что
защита автоматически обеспечивается транзакциями с уровнем изоляции Serializable.
Когда система вынуждена объединять несколько предикатных блокировок уровня страниц в одну предикатную блокировку уровня отношения, так как в таблице предикатных блокировок недостаточно памяти, частота возникновения ошибок сериализации может увеличиться. Этого можно избежать путем увеличения 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). Данный метод дополняет изоляцию на основе снимков проверками на аномалии сериализации. В сравнении с другими системами, использующими традиционные механизмы блокировки, могут наблюдаться некоторые различия в поведении и производительности. См. [ports12] для получения подробной информации.