Уровни изоляции транзакций
Стандарт SQL определяет четыре уровня изоляции транзакций. Самый строгий — Serializable. Согласно стандарту SQL, любое одновременное выполнение набора сериализуемых транзакций должно давать тот же эффект, что и их поочередное выполнение в некотором порядке. Остальные три уровня определяются через описание допустимых аномалий, которые являются результатом взаимодействия между параллельными транзакциями. Определенные аномалии не должны возникать на определенном уровне изоляции транзакций. Ни одна из этих аномалий невозможна на уровне Serializable.
Могут возникнуть следующие аномалии:
-
Грязное чтение (dirty read) — транзакция считывает данные, записанные параллельной незафиксированной транзакцией.
-
Неповторяемое чтение (non-repeatable read) — транзакция перечитывает ранее прочитанные данные и обнаруживает, что данные были изменены другой транзакцией (которая была зафиксирована позже момента первоначального чтения).
-
Фантомное чтение (phantom read) — транзакция повторно выполняет запрос, который возвращает набор строк для некоторого условия, и обнаруживает, что набор строк, удовлетворяющих условию, изменился из-за другой недавно зафиксированной транзакции.
-
Аномалия сериализации (serialization anomaly) — результат успешной фиксации группы транзакций несовместим со всеми возможными порядками выполнения этих транзакций по одной.
Уровни изоляции транзакций описаны в таблице ниже. Значение Не разрешено в ADPG/PostgreSQL
используется для аномалий, которые разрешены стандартом SQL, но запрещены в ADPG/PostgreSQL.
Уровень изоляции | Грязное чтение | Неповторяемое чтение | Фантомное чтение | Аномалия сериализации |
---|---|---|---|---|
Read uncommitted |
Не разрешено в ADPG/PostgreSQL |
Допускается |
Допускается |
Допускается |
Read committed |
Не допускается |
Допускается |
Допускается |
Допускается |
Repeatable read |
Не допускается |
Не допускается |
Не разрешено в ADPG/PostgreSQL |
Допускается |
Serializable |
Не допускается |
Не допускается |
Не допускается |
Не допускается |
В ADPG/PostgreSQL вы можете запросить любой из четырех стандартных уровней изоляции транзакций, но внутри реализованы только три различных уровня изоляции. Режим PostgreSQL Read uncommitted ведет себя как Read committed. Такое решение позволяет сопоставить стандартные уровни изоляции с архитектурой управления многоверсионным параллелизмом PostgreSQL.
В таблице также показано, что реализация PostgreSQL Repeatable read не допускает фантомного чтения. Это приемлемо в соответствии со стандартом SQL, поскольку стандарт определяет, какие аномалии не должны возникать на определенных уровнях изоляции. Более строгие ограничения допустимы.
Важно
Некоторые типы данных и функции PostgreSQL имеют специальные правила относительно поведения транзакций. В частности, изменения, внесенные в последовательность (и, следовательно, счетчик столбца, объявленный как serial ), немедленно видны всем другим транзакциям и не откатываются, если транзакция, внесшая изменения, прерывается.
|
Уровень изоляции по умолчанию в ADPG/PostgreSQL — Read committed. Чтобы убедиться в этом, выполните следующую команду psql:
SHOW TRANSACTION ISOLATION LEVEL;
Результат:
transaction_isolation ----------------------- read committed (1 row)
Чтобы выбрать желаемый уровень изоляции транзакции, используйте команду SET TRANSACTION.
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
...
COMMIT;
Также возможно установить уровень изоляции транзакций в командах BEGIN и START TRANSACTION.
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
...
COMMIT;
START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
...
COMMIT;
Уровень изоляции Read committed
Как упоминалось выше, Read committed — это уровень изоляции по умолчанию в ADPG/PostgreSQL. Когда транзакция использует этот уровень изоляции, запрос 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
подействует на эту строку, даже несмотря на то, что эта команда может не видеть никакую версию этой строки.
В режиме Read committed, если вы выполняете команду INSERT
с выражением ON CONFLICT DO NOTHING
, строка может не быть добавлена в результате другой транзакции, эффект которой не виден на снепшоте команды INSERT
.
Из-за вышеуказанных правил команда обновления может увидеть несогласованное состояние: она может видеть результаты параллельных команд, изменяющих те же строки, что пытается изменить она, но при этом не видит результаты этих команд в других строках таблиц. Такое поведение делает режим Read committed непригодным для команд, включающих сложные условия поиска. Однако вы можете использовать его в более простых случаях. Например, рассмотрим обновление банковских балансов — перевод денег с одного счета на другой с помощью следующих транзакций:
BEGIN;
UPDATE accounts SET balance = balance - 1000.00 WHERE acc_number = 5432;
UPDATE accounts SET balance = balance + 1000.00 WHERE acc_number = 1234;
COMMIT;
BEGIN;
UPDATE accounts SET balance = balance - 1000.00 WHERE acc_number = 6543;
UPDATE accounts SET balance = balance + 1000.00 WHERE acc_number = 1234;
COMMIT;
Если эти две транзакции одновременно попытаются изменить баланс счета 1234
, вторая транзакция должна работать с измененной версией строки.
BEGIN;
UPDATE accounts SET balance = balance - 1000.00 WHERE acc_number = 5432;
/* Выполнено из другой сессии:
BEGIN;
UPDATE accounts SET balance = balance - 1000.00 WHERE acc_number = 6543;
UPDATE accounts SET balance = balance + 1000.00 WHERE acc_number = 1234;
COMMIT;
*/
UPDATE accounts SET balance = balance + 1000.00 WHERE acc_number = 1234;
COMMIT;
Поскольку каждая команда влияет только на определенную строку, если команда увидит измененную версию строки, это не приведет к проблемам несогласованности.
Более сложные сценарии могут привести к нежелательным результатам в режиме Read committed. Например, рассмотрим команду DELETE
, работающую с данными, которые одновременно добавляются и удаляются из критериев ограничения другой командой. Например, website
— это таблица из двух строк, где website.hits
равны 9
и 10
. Выполним следующие транзакции:
BEGIN;
UPDATE website SET hits = hits + 1;
-- Выполнено из другой сессии: DELETE FROM website WHERE hits = 10;
COMMIT;
Команда DELETE
не будет иметь никакого эффекта, даже если до и после UPDATE
есть строка с полем hits
, равным 10
. Это происходит потому, что значение строки 9
перед обновлением пропускается, а когда UPDATE
завершается и DELETE
получает блокировку, новое значение строки больше не 10
, а 11
, что не соответствует условию.
Частичная изоляция транзакций, обеспечиваемая режимом Read committed, подходит для многих приложений, и этот режим достаточно быстр и прост в использовании. Однако этого недостаточно для всех случаев. Приложениям, выполняющим сложные запросы и обновления, может потребоваться более строго согласованное представление базы данных, чем обеспечивает режим Read committed.
Уровень изоляции Repeatable read
В режиме Repeatable read видны только те данные, которые были зафиксированы до начала транзакции, а незафиксированные данные и изменения, внесенные другими транзакциями во время выполнения транзакции, не видны. Однако запрос видит результаты предыдущих обновлений, выполненных в рамках его транзакции, даже если они еще не зафиксированы. Это предотвращает все явления, описанные в таблице аномалий, за исключением аномалий сериализации.
Этот уровень отличается от Read committed тем, что запрос в транзакции Repeatable read видит снепшот данных в начале первого оператора транзакции (команды управления транзакцией не учитываются). Последующие команды SELECT
в одной и той же транзакции видят одни и те же данные — они не видят изменений, внесенных и зафиксированных другими транзакциями после начала текущей транзакции.
Приложения, использующие этот уровень, должны быть готовы к повторным попыткам транзакций из-за сбоев сериализации.
Команды UPDATE
, DELETE
, SELECT FOR UPDATE
и SELECT FOR SHARE
ведут себя как SELECT
при поиске целевых строк. Они находят только те целевые строки, которые были зафиксированы на момент начала транзакции. Однако к моменту обнаружения эти целевые строки могут уже быть изменены, удалены или заблокированы другой параллельной транзакцией. В этом случае транзакция в режиме Repeatable read ожидает завершения первой транзакции, изменяющей данные (если она все еще выполняется). Если первая транзакция выполняет откат, транзакция Repeatable read может продолжить обновление первоначально найденной строки. Если первая транзакция фиксируется и обновляет или удаляет строку, а не просто блокирует ее, транзакция Repeatable read будет отменена с сообщением: ERROR: could not serialize access due to concurrent update
. Транзакция Repeatable read не может изменять или блокировать строки, измененные другими транзакциями после её начала. Когда приложение получает сообщение об ошибке, оно должно прервать текущую транзакцию и попытаться повторить ее с самого начала. Обратите внимание, что повторная попытка может потребоваться только для транзакций, изменяющих данные. Транзакции на чтение не будут иметь конфликтов сериализации.
Режим Repeatable read обеспечивает строгую гарантию того, что каждая транзакция видит полностью стабильное представление базы данных. Однако это представление не обязательно всегда будет соответствовать некоторому последовательному выполнению параллельных транзакций одного и того же уровня.
Уровень изоляции Repeatable read реализуется с использованием метода, известного как Изоляция снепшотов (snapshot isolation).
Уровень изоляции Serializable
Уровень изоляции Serializable обеспечивает самую строгую изоляцию транзакций. Этот уровень эмулирует последовательное выполнение транзакций для всех зафиксированных транзакций, как если бы транзакции выполнялись одна за другой, последовательно, а не одновременно. Однако, как и в случае с уровнем Repeatable read, приложения должны быть готовы к повторным попыткам транзакций из-за сбоев сериализации.
Этот режим изоляции работает так же, как и Repeatable read, только он дополнительно отслеживает условия, при которых результат сериализуемых транзакций, выполняемых параллельно, может не соответствовать результату тех же транзакций, выполняемых поочередно. Такой мониторинг не вносит никаких блокировок, кроме тех, которые присутствуют в режиме Repeatable read, но создает некоторые дополнительные издержки. Если обнаружена аномалия сериализации, сериализация завершается сбоем.
В качестве примера рассмотрим таблицу table1
, которая изначально содержит следующие данные:
id | category | value ----+----------+------- 1 | a | 10 2 | a | 20 3 | b | 300 4 | b | 400
Предположим, что сериализуемая транзакция 1 вычисляет сумму значений категории a
и вставляет ее в таблицу как значение категории b
:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO table1(id, category, value) VALUES
(5,'b',
(SELECT SUM(value) FROM table1 WHERE category = 'a'));
Сериализуемая транзакция 2 вычисляет сумму значений категории b
и вставляет ее в таблицу как значение категории а
:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
INSERT INTO table1(id, category, value) VALUES
(6,'a',
(SELECT SUM(value) FROM table1 WHERE category = 'b'));
Если выполнить COMMIT
транзакции 1, она будет зафиксирована без ошибок. Если затем выполнить COMMIT
транзакции 2, произойдет следующая ошибка:
ERROR: could not serialize access due to read/write dependencies among transactions
Это связано с тем, что если бы транзакция 1 была выполнена до транзакции 2, транзакция 2 вычислила бы сумму 730
, а не 700
, и аналогичным образом другой порядок привел бы к другой сумме, вычисленной транзакцией 1.
При использовании сериализуемых транзакций для предотвращения аномалий важно, чтобы любые данные, считанные из постоянной таблицы, не считались действительными до тех пор, пока транзакция, которая их читала, не будет успешно зафиксирована. Это верно даже для транзакций только для чтения, за исключением того, что данные, прочитанные в рамках отложенной транзакции только для чтения (deferrable read-only transaction), считаются действительными, как только они прочитаны, поскольку такая транзакция ждет, пока она сможет получить снепшот, гарантированно свободный от таких проблем, прежде чем начать читать какие-либо данные. Во всех остальных случаях приложения не должны полагаться на результаты чтения данных в транзакции, которая не была зафиксирована. В случае ошибки и отката приложения должны повторять транзакцию до ее успешного завершения.
Чтобы полностью гарантировать сериализуемость, ADPG/PostgreSQL использует предикатные блокировки, которые позволяют определить, когда запись повлияет на результат предыдущего чтения из параллельной транзакции, если эта запись была выполнена первой. В PostgreSQL эти блокировки не приводят к фактической блокировке данных и, следовательно, никоим образом не могут вызывать взаимоблокировки транзакций. Они помогают идентифицировать и отмечать зависимости между параллельными транзакциями уровня Serializable, которые в определенных комбинациях могут привести к аномалиям сериализации. Транзакции Read commit или Repeatable read, наоборот, для обеспечения целостности данных должны либо блокировать целые таблицы, что предотвратит доступ пользователей к этим таблицам, либо использовать SELECT FOR UPDATE
или SELECT FOR SHARE
, которые могут не только блокировать другие транзакции, но и создавать дополнительную нагрузку на диск.
Предикатные блокировки основаны на данных, к которым фактически обращается транзакция. Они отображаются в системном представлении pg_locks
со значением режима SIReadLock
. Конкретные блокировки, полученные во время выполнения запроса, зависят от плана запроса. Несколько более детализированных блокировок (например, блокировок кортежей) могут быть объеденины в меньшее количество более общих блокировок (например, блокировок страниц), чтобы сэкономить память, затрачиваемую на отслеживание блокировок.
При правильном использовании транзакции Serializable могут упростить разработку приложений. Однако важно, чтобы среда, использующая этот метод, имела обобщенный способ обработки ошибок сериализации (которые всегда возвращаются со значением SQLSTATE
, равным 40001
), потому что сложно предсказать, какие именно транзакции будет необходимо откатить, чтобы предотвратить аномалии сериализации.
При использовании сериализуемых транзакций для управления параллелизмом учитывайте следующие рекомендации:
-
По возможности объявляйте транзакции как
READ ONLY
. -
Контролируйте количество активных подключений, используя при необходимости пул подключений. Это всегда важный фактор производительности, но он может быть особенно важен в загруженной системе, использующей сериализуемые транзакции.
-
В одной транзакции не должно содержаться больше команд, чем необходимо для обеспечения целостности.
-
Не оставляйте соединения "простаивающими в транзакции" ("idle in transaction") дольше, чем необходимо. Для автоматического отключения затянувшихся транзакций можно применить параметр конфигурации 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. Однако тут важно сопоставить выигрыш от уменьшения числа откатов и перезапусков транзакций с проигрышем от возможного менее эффективного выполнения запросов.
Указать все параметры, упомянутые выше, можно в поле postgresql.conf custom section в настройках сервиса ADPG. См. Настройка сервисов.
Для реализации уровня изоляции Serializable применяется подход, который называется Изоляция сериализуемых снепшотов (serializable snapshot isolation). Он основан на изоляции снепшотов с дополнительными проверками на предмет аномалий сериализации.