13.2. Изоляция транзакции#
13.2. Изоляция транзакции #
Стандарт SQL определяет четыре уровня изоляции транзакций. Самый строгий - Serializable, который определен стандартом в абзаце, который говорит, что любое одновременное выполнение набора Serializable транзакций гарантирует производить тот же эффект, что и выполнение их одну за другой в некотором порядке. Остальные три уровня определены в терминах феноменов, возникающих в результате взаимодействия между параллельными транзакциями, которые не должны происходить на каждом уровне. Стандарт отмечает, что из-за определения Serializable ни один из этих феноменов невозможен на этом уровне. (Это вполне ожидаемо - если эффект транзакций должен быть согласован с выполнением их одну за другой, как можно увидеть какие-либо феномены, вызванные взаимодействием?)
Явления, которые запрещены на различных уровнях, включают:
- dirty read
Транзакция читает данные, записанные параллельной неподтвержденной транзакцией.
- nonrepeatable read
Транзакция повторно считывает данные, которые она ранее считала, и обнаруживает, что данные были изменены другой транзакцией (которая подтвердила изменения с момента первоначального чтения).
- phantom read
Транзакция повторно выполняет запрос, возвращающий набор строк, удовлетворяющих условию поиска, и обнаруживает, что набор строк, удовлетворяющих условию, изменился из-за другой недавно завершенной транзакции.
- serialization anomaly
Результат успешного коммита группы транзакций не согласован с возможными порядками выполнения этих транзакций по одной за раз.
Стандарт SQL и уровни изоляции транзакций, реализованные в PostgreSQL, описаны в Таблица 13.1.
Таблица 13.1. Уровни изоляции транзакций
Уровень изоляции | Грязное чтение | Неповторяющееся чтение | Фантомное чтение | Аномалия сериализации |
---|---|---|---|---|
Чтение неподтверждённых данных | Допустимо, но не в PG | Возможно | Возможно | Возможно |
Чтение подтверждённых данных | Невозможно | Возможно | Возможно | Возможно |
Многократное чтение | Грязное чтение | Неповторяющееся чтение | Фантомное чтение | Аномалия сериализации |
Упорядоченное | Невозможно | Невозможно | Невозможно | Невозможно |
В Tantor SE-1C вы можете запросить любой из четырех стандартных уровней изоляции транзакций, но внутренне реализованы только три различных уровня изоляции, то есть режим Read Uncommitted в PostgreSQL ведет себя как Read Committed. Это происходит потому, что это единственный разумный способ сопоставить стандартные уровни изоляции с архитектурой многоверсионного контроля параллелизма PostgreSQL.
Таблица также показывает, что реализация повторяемого чтения в PostgreSQL не позволяет фантомные чтения. Это допустимо согласно стандарту SQL, поскольку стандарт указывает, какие аномалии не должны возникать при определенных уровнях изоляции; более высокие гарантии допустимы. Поведение доступных уровней изоляции подробно описано в следующих подразделах.
Для установки уровня изоляции транзакции используйте команду SET TRANSACTION.
Важно
Некоторые типы данных и функции Tantor SE-1C имеют особые правила в отношении транзакционного поведения. В частности, изменения, внесенные в последовательность (и, следовательно, счетчик столбца, объявленного с использованием serial
), немедленно видны всем другим транзакциям и не откатываются, если транзакция, внесшая изменения, прерывается. См. Раздел 9.17 и Раздел 8.1.4.
13.2.1. Уровень изоляции "Read Committed" #
Read Committed является уровнем изоляции по умолчанию в Tantor SE-1C. Когда транзакция использует этот уровень изоляции, запрос 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
условия следующими и выполнит первое, которое будет успешным.
Если 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.
13.2.2. Уровень изоляции "Повторяемое чтение" #
Уровень изоляции Повторяемое чтение видит только данные, зафиксированные до начала транзакции; он никогда не видит ни неподтвержденные данные, ни изменения, зафиксированные параллельными транзакциями во время выполнения транзакции. (Однако каждый запрос видит эффекты предыдущих обновлений, выполненных в рамках его собственной транзакции, даже если они еще не зафиксированы.) Это более сильная гарантия, чем требуется стандартом SQL для этого уровня изоляции, и предотвращает все явления, описанные в Таблица 13.1, за исключением аномалий сериализации. Как упомянуто выше, это специально разрешено стандартом, который описывает только минимальные защиты, которые должен обеспечивать каждый уровень изоляции.
Этот уровень отличается от Read Committed тем, что запрос в транзакции с повторяемым чтением видит снимок, сделанный в начале первого оператора, не относящегося к управлению транзакциями, в транзакции, а не в начале текущего оператора внутри транзакции. Таким образом, последовательные команды SELECT
внутри одной транзакции видят одни и те же данные, то есть они не видят изменений, сделанных другими транзакциями, которые подтвердились после начала их собственной транзакции.
Все приложения, использующие этот уровень, должны быть готовы повторно выполнять транзакции из-за сбоев сериализации.
Команды UPDATE
, DELETE
,
MERGE
, SELECT FOR UPDATE
и
SELECT FOR SHARE
ведут себя так же, как и команда SELECT
,
поиск целевых строк осуществляется только для строк, зафиксированных на момент начала транзакции.
Однако, такая целевая строка может уже быть обновлена (или удалена или заблокирована)
другой параллельной транзакцией к моменту ее обнаружения. В этом случае, транзакция с повторяемым чтением
будет ожидать завершения или отката первой обновляющей транзакции (если она все еще выполняется).
Если первая обновляющая транзакция откатывается, то ее эффекты аннулируются и транзакция с повторяемым чтением
может продолжить обновление исходно найденной строки. Но если первая обновляющая транзакция коммитится
(и фактически обновляет или удаляет строку, а не только блокирует ее),
тогда транзакция с повторяемым чтением будет откатываться с сообщением.
ERROR: could not serialize access due to concurrent update
потому что транзакция с повторяемым чтением не может изменять или блокировать строки, измененные другими транзакциями после начала транзакции с повторяемым чтением.
Когда приложение получает это сообщение об ошибке, оно должно прервать текущую транзакцию и повторить всю транзакцию с самого начала. Во второй раз транзакция увидит ранее подтвержденное изменение как часть своего начального представления базы данных, поэтому нет логического конфликта в использовании новой версии строки в качестве отправной точки для обновления новой транзакции.
Обратите внимание, что только обновляющие транзакции могут потребовать повторной попытки; транзакции только для чтения никогда не будут иметь конфликтов сериализации.
Режим Repeatable Read обеспечивает строгую гарантию того, что каждая транзакция видит полностью стабильное представление базы данных. Однако это представление не всегда будет согласовано с последовательным (по одной) выполнением параллельных транзакций того же уровня. Например, даже транзакция только для чтения на этом уровне может видеть запись управления, обновленную для отображения завершения пакета, но не видеть одну из детальных записей, которая логически является частью пакета, потому что она прочитала более раннюю версию записи управления. Попытки применить бизнес-правила с помощью транзакций, работающих на этом уровне изоляции, не будут работать правильно без тщательного использования явных блокировок для блокировки конфликтующих транзакций.
Уровень изоляции Repeatable Read реализуется с использованием техники, известной в академической литературе по базам данных и в некоторых других базах данных как Snapshot Isolation. Могут наблюдаться различия в поведении и производительности по сравнению с системами, использующими традиционную технику блокировки, которая снижает параллельность. Некоторые другие системы могут даже предлагать Repeatable Read и Snapshot Isolation как отдельные уровни изоляции с разным поведением. Разрешенные феномены, которые отличают две техники, не были формализованы исследователями баз данных до разработки стандарта SQL и выходят за рамки данного руководства. Для полного изложения см. [berenson95].
Примечание
До версии PostgreSQL 9.1 запрос на уровень изоляции транзакции Serializable обеспечивал точно такое же поведение, описанное здесь. Чтобы сохранить устаревшее поведение Serializable, теперь следует запросить уровень Repeatable Read.
13.2.3. Уровень изоляции сериализуемости #
Уровень изоляции Serializable обеспечивает наиболее строгую изоляцию транзакций. Этот уровень эмулирует последовательное выполнение транзакций для всех зафиксированных транзакций; как если бы транзакции выполнялись одна за другой, последовательно, а не одновременно. Однако, подобно уровню Repeatable Read, приложения, использующие этот уровень, должны быть готовы повторно выполнять транзакции из-за сбоев в сериализации. Фактически, этот уровень изоляции работает точно так же, как уровень 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, обе транзакции были бы зафиксировать; но поскольку нет последовательного порядка выполнения, согласованного с результатом, использование сериализуемых транзакций позволит одной транзакции подтвердиться, а другую откатить с этим сообщением:
ERROR: could not serialize access due to read/write dependencies among transactions
Это происходит потому, что если A выполнилась перед B, то B вычислил бы сумму 330, а не 300, и аналогично, другой порядок привел бы к другой сумме, вычисленной A.
Когда полагаются на сериализуемые транзакции для предотвращения аномалий, важно, чтобы любые данные, считанные из постоянной пользовательской таблицы, не считались действительными до тех пор, пока транзакция, которая их считала, успешно не завершится. Это верно даже для только для чтения транзакций, за исключением того, что данные, считанные внутри отложенной только для чтения транзакции, считаются действительными сразу после их считывания, потому что такая транзакция ожидает, пока она сможет получить снимок, гарантированно свободный от таких проблем, прежде чем начать считывать какие-либо данные. Во всех остальных случаях приложения не должны полагаться на результаты, считанные во время транзакции, которая позже была отменена; вместо этого они должны повторить транзакцию, пока она не выполнится успешно.
Чтобы гарантировать истинную сериализуемость Tantor SE-1C использует предикатную блокировку, что означает, что он сохраняет блокировки, которые позволяют ему определить, когда запись могла бы повлиять на результат предыдущего чтения из параллельной транзакции, если бы она выполнилась первой. В Tantor SE-1C эти блокировки не вызывают блокировки и, следовательно, не могут вызвать блокировку. Они используются для идентификации и обозначения зависимостей между параллельными сериализуемыми транзакциями, которые в определенных комбинациях могут привести к аномалиям сериализации. В отличие от того, Read Committed или Repeatable Read транзакция, которая хочет обеспечить согласованность данных, может потребоваться заблокировать всю таблицу, что может блокировать других пользователей, пытающихся использовать эту таблицу, или она может использовать SELECT FOR UPDATE
или SELECT FOR SHARE
, которые не только могут блокировать другие транзакции, но и вызывать доступ к диску.
Предикатные блокировки в Tantor SE-1C, как и в большинстве
других систем управления базами данных, основаны на данных, фактически доступных транзакцией. Они отображаются в системном представлении
pg_locks
с режимом mode
SIReadLock
. Особые блокировки,
получаемые во время выполнения запроса, зависят от плана, используемого запросом, и несколько более детализированные блокировки (например, блокировки кортежей) могут быть объединены в более крупные блокировки (например, блокировки страниц) во время
выполнения транзакции, чтобы избежать исчерпания памяти, используемой для
отслеживания блокировок. Транзакция с режимом READ ONLY
может
освободить свои блокировки SIRead до завершения, если она обнаруживает, что
конфликты, которые могут привести к сериализационной аномалии, больше не могут возникнуть.
Фактически, транзакции с режимом READ ONLY
часто могут
установить этот факт при запуске и избежать получения предикатных блокировок.
Если вы явно запросите транзакцию с режимом SERIALIZABLE READ ONLY DEFERRABLE
,
она будет заблокирована до тех пор, пока не установит этот факт. (Это
единственный случай, когда сериализуемые транзакции блокируются, но
повторяемые чтения - нет). С другой стороны, блокировки SIRead
часто должны быть сохранены после завершения транзакции до завершения перекрывающихся транзакций чтения и записи.
Сохранение последовательного использования сериализуемых транзакций может упростить разработку.
Гарантия того, что любой набор успешно завершенных параллельных сериализуемых транзакций будет иметь такой же эффект, как если бы они выполнялись одна за другой, означает, что если вы можете продемонстрировать, что отдельная транзакция, как написанная, будет делать правильные вещи при выполнении в отдельности, вы можете быть уверены, что она будет делать правильные вещи в любой комбинации сериализуемых транзакций, даже без какой-либо информации о том, что могут делать другие транзакции, или она не будет успешно завершена. Важно, чтобы среда, использующая эту технику, имела обобщенный способ обработки сбоев сериализации (которые всегда возвращаются с значением SQLSTATE '40001'), потому что будет очень сложно предсказать, какие именно транзакции могут способствовать зависимостям чтения/записи и должны быть отменены для предотвращения аномалий сериализации. Мониторинг зависимостей чтения/записи имеет свою стоимость, так же как и перезапуск транзакций, которые завершаются с ошибкой сериализации, но в сравнении с затратами и блокировкой, связанной с использованием явных блокировок и SELECT FOR UPDATE
или SELECT FOR SHARE
, сериализуемые транзакции являются наилучшим выбором производительности для некоторых сред.
Хотя уровень изоляции транзакций Serializable в Tantor SE-1C предполагает коммит только параллельных транзакций, если есть подтверждение, что существует последовательный порядок выполнения, который приведет к тому же результату, это не всегда предотвращает возникновение ошибок, которые не произошли бы при реальном последовательном выполнении. В частности, возможно возникновение нарушений ограничения уникальности, вызванных конфликтами с перекрывающимися Serializable транзакциями, даже после явной проверки отсутствия ключа перед попыткой его вставки. Это можно избежать, убедившись, что все Serializable транзакции, вставляющие потенциально конфликтующие ключи, явно проверяют, могут ли они это сделать. Например, представьте приложение, которое запрашивает у пользователя новый ключ и затем проверяет его отсутствие, пытаясь сначала его выбрать, или генерирует новый ключ, выбирая максимальный существующий ключ и добавляя единицу. Если некоторые Serializable транзакции вставляют новые ключи напрямую, не следуя этому протоколу, могут быть сообщены нарушения ограничений уникальности, даже в случаях, когда они не могут возникнуть в последовательном выполнении параллельных транзакций.
Для оптимальной производительности при использовании сериализуемых транзакций для контроля параллелизма следует учитывать следующие вопросы:
Объявляйте транзакции как
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, которая основывается на Snapshot Isolation, добавляя проверки на аномалии сериализации. Некоторые различия в поведении и производительности могут быть замечены при сравнении с другими системами, использующими традиционную технику блокировки. Подробную информацию см. в разделе [ports12].