Уровни изоляции транзакций с примерами на PostgreSQL

Вступление


В стандарте SQL описывается четыре уровня изоляции транзакций — Read uncommited (Чтение незафиксированных данных), Read committed (Чтение зафиксированных данных), Repeatable read (Повторяемое чтение) и Serializable (Сериализуемость). В данной статье будет рассмотрен жизненный цикл четырёх параллельно выполняющихся транзакций с уровнями изоляции Read committed и Serializable.


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


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


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


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


ACID или 4 свойства транзакций


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


Atomicity (атомарность) — выражается в том, что транзакция должна быть выполнена в целом или не выполнена вовсе.


Consistency (согласованность) — гарантирует, что по мере выполнения транзакций, данные переходят из одного согласованного состояния в другое, то есть транзакция не может разрушить взаимной согласованности данных.


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


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


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


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


Суть уровня изоляции Read Committed показана на диаграмме 1.


Примечание: В таблице уже находится запись с первой версией данных (v1). Прошу воспринимать команды SELECT v1; — как команду возвращающую данные версии v1, а UPDATE v1 to v2; — как команду обновления данных с первой версии до второй.


Создадим к базе-данных 4 подключения и откроем в каждом из подключений по транзакции с уровнем изоляции Read Committed

Read Committed


Read Committed


Шаг 1. В начальный момент времени до каких-либо изменений данных всем транзакциям доступна изначальная версия данных (v1);

Read Committed


Шаг 2. В ходе работы Первой транзакции данные без каких либо блокировок успешно обновляются до «второй версии» (v2);

Read Committed


Шаг 3. Изменения сделанные в Первой транзакции будут видны только ей самой (SELECT возвращает v2), и не будут доступны остальным транзакциям (SELECT запрос во Второй и Четвёртой транзакциях возвращает v1);

Read Committed


Шаг 4. Закрытие Первой транзакции. Все изменения сделанные в ходе её работы успешно фиксируются;

Read Committed


Шаг 5. После закрытия Первой транзакции (предыдущий шаг), изменения сделанные в ходе её выполнения над данными (обновление с v1 до v2) были распространены на остальные транзакции, SELECT запрос в оставшихся 3 открытых транзакциях возвращает v2 («Неповторяемое чтение», отличите уровня изоляции Read Committed от Serializable);

Read Committed


Шаг 6. Запрос на обновление данных во Второй транзакции до «третьей версии» успешно выполняется, но запросы на обновление данных блокируют изменяемые строки на дальнейшее их изменение, до завершения Второй транзакции;

Read Committed


Шаг 7. Из-за блокировки наложенной на данные, Третья транзакция переходит в режим ожидания с запросом на удаление данных. Ожидание Третьей транзакции будет происходить до закрытия Второй транзакции;

Read Committed


Шаг 8. Несмотря на то, что Третья транзакция ожидает закрытия Второй, как Вторая так и Четвёртая транзакции без каких либо проблем продолжают свою работу, возвращая данные согласно своим версиям. Вторая возвращает v3, Четвёртая возвращает v2;

Read Committed


Шаг 9. Закрытие Второй транзакции приводит к разблокированию данных для изменения. Уровень изоляции Read Committed позволяет продолжить работу Третьей транзакции без вызова ошибки. Получив доступ на изменение новой версии данных (v3) Третья транзакция УСПЕШНО тут же их «удаляет» (отличие Read Committed от Serializable);

Read Committed


Шаг 10. До закрытия Третьей транзакции, данные будут удалёнными только внутри Третьей транзакции. Четвёртой транзакции до закрытия Третьей данные доступны (SELECT запрос в Четвёртой транзакции возвращает v3);

Read Committed


Шаг 11. Закрытие Третьей транзакции. Все изменения сделанные в ходе её работы успешно фиксируются;

Read Committed


Шаг 12. Запрос на получение данных в Четвёртой транзакции ничего не возвращает («Фантомное чтение», SELECT запрос возвращает 0 записей).

Read Committed


Примечание. На диаграмме не показано действие запроса INSERT. В рамках данного уровня изоляции, строки добавленные, например в шаге 3, в Первой транзакции, были бы ВИДНЫ остальным транзакциям после завершения Первой транзакции.


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


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


Изоляция уровня Serializable обеспечивает беспрепятственный доступ к базе данных транзакциям с SELECT запросами. Но для транзакций с запросами UPDATE и DELETE, уровень изоляции Serializable не допускает модификации одной и той же строки в рамках разных транзакций. При изоляции такого уровня все транзакции обрабатываются так, как будто они все запущены последовательно (одна за другой). Если две одновременные транзакции попытаются обновить одну и туже строку, то это будет не возможно. В таком случае PostgreSQL принудит транзакцию, вторую, да и все последующие, что пытались изменить строку к отмене (откату — ROLLBACK).


Суть уровня изоляции Serializable показана на диаграмме 2.


Создадим к базе-данных 4 подключения и откроем в каждом из подключений по транзакции с уровнем изоляции Serializable

Serializable


Serializable


Шаг 1. Всем транзакциям доступна изначальная версия данных (v1);

Serializable


Шаг 2. В ходе работы Первой транзакции данные без каких либо блокировок успешно обновляются до «второй версии» (v2);

Serializable


Шаг 3. Изменения сделанные в Первой транзакции будут видны только ей самой (SELECT возвращает v2), и не будут доступны остальным транзакциям (SELECT запрос во Второй и Четвёртой транзакциях возвращает v1);

Serializable


Шаг 4. Запрос на обновление данных в первой транзакции (шаг 2), блокирует обновляемые строки, и переводит в режим ожидания Вторую транзакцию с запросом на удаление данных. Блокировка транзакций на обновляемые данных будет происходить до закрытия Первой транзакции;

Serializable


Шаг 5. Несмотря на то, что Вторая транзакция ожидает закрытия Первой, как Третья так и Четвёртая транзакции без каких либо проблем продолжают свою работу, возвращая данные согласно своим версиям;

Serializable


Шаг 6. Завершение Первой транзакции снимает блокировку с обновляемых данных, но в рамках уровня изоляции Serializable повторное обновление данных в параллельных транзакциях запрещено, и поэтому в ходе выполнения Второй транзакции возникает ошибка (отличие Serializable от Read Committed);

Serializable


Шаг 7. Запрос SELECT во Второй транзакции становится не возможным, так как ошибка возникшая на предыдущем шаге отменяет («блокирует») транзакцию. Запрос SELECT в Третьей и Четвертой транзакциях возвращают первоначальную версию данных (v1). Несмотря на то, что Первая транзакция была завершена успешно, изменения не стали видны остальным открытым транзакциям (отличие Serializable от Read Committed). Открытие Пятой транзакции в левом верхнем окне;

Serializable


Шаг 8. Закрытие Второй транзакции. Все изменения сделанные данной транзакцией будут отменены, из-за возникшей ошибки в ходе её работы;

Serializable


Шаг 9. Запрос SELECT в Пятой транзакции возвращает новую версию данных (v2). Запрос SELECT в Третьей и Четвёртой транзакциях возвращают первоначальную версию данных (v1);

Serializable


Шаг 10. Уровень изоляции Serializable всё также не даёт обновлять данные, запрос UPDATE в Третьей транзакции завершается не удачно, с вытекающими последствиями для хода всей транзакции (несмотря на то, что Первая транзакция уже удачно завершилась, и все внесённые ей изменения сохранены в базе данных). А вот запрос UPDATE в Пятой транзакции завершается успешно, так как она открыта после завершения Первой транзакции, и работает с новой версией данных;

Serializable


Шаг 11. Закрытие Третьей транзакции. Все изменения сделанные данной транзакцией будут отменены, из-за возникшей ошибки в ходе её работы;

Serializable


Шаг 12. Транзакция Четыре всё также показывает, что у транзакций с SELECT запросами никаких нет проблем, а Пятая транзакция получает уже обновлённые же собой данные (v5).

Serializable


Примечание. На диаграмме не показано действие запроса INSERT. В рамках данного уровня изоляции, строки добавленные, например в шаге 3, в Первой транзакции, были бы НЕ ДОСТУПНЫ Второй, Третьей и Четвёртой транзакциям после завершения Первой транзакции. Также на диаграмме не показан результат ROLLBACK (Шаги 8 и 11). В случае если бы Вторая и Третья транзакции делали какие либо изменения над не заблокированными данными, то все эти изменения не были бы зафиксированы, так как транзакции завершаются неудачно (суть свойства — Atomicity).


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


Для транзакций содержащих только SELECT запросы, использование уровня изоляции Serializable оправдывает себя тогда, когда вы не хотите видеть внесённые изменения параллельно завершёнными транзакциями в ходе работы текущей транзакции.


Аномалия сериализации (Потерянное обновление)


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


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


Документация на сайте PostgreSQL PRO пишет, что Read Committed допускает «Serialization Anomaly». Отечественная Wikipedia, не настаивая на то, что таблица относится именно к PostgreSQL, пишет, что Read Commited предотвращает аномалию сериализации. Английская Википедиа о таком феномене чтения данных умалчивает. Но немецкая Википедия приводит в своей версии таблицы феномен «Lost Updates», указывая на то, что Read Committed может быть не подвержен потере обновлений с дополнительной защитой через курсор (Cursor Stability). Украинская Википедия поддерживает русскоязычную версию статьи, испанская Википедия поддерживает английскую версию статьи. Англоязычная документация по PostgreSQL не отличается от документации с сайта PostgreSQL PRO.


Cursor Stability расширяет блокировочное поведение уровня READ COMMITED для SQL-курсоров, добавляя новую операцию чтения (Fetch) по курсору rc (означает read cursor, т.е. чтение по курсору) и требуя, чтобы блокировка устанавливалась на текущем элементе курсора. Блокировка удерживается до тех пор, пока курсор не будет перемещен (пока не измениться его текущий элемент) или закрыт, возможно, операцией фиксации. Естественно, транзакция, читающая по курсору, может изменить текущую строку (wc — запись по курсору), и в этом случае блокировка по записи этой строки будет сохраняться до тех пор, пока транзакция не зафиксируется, даже после передвижения курсора с последующей выборкой следующей строки.

Вот такой результат получился в PostgreSQL 9.6

Serialization Anomaly, Lost Updates


Итог: 149.


Заключение


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


Литература


» MVCC Unmasked
» 13.2. Изоляция транзакций
» Критика уровней изолированности
» Уровни изоляции транзакций в SQL. Шпаргалка
» Уровень изолированности транзакций
» … the lost update phenomena

Комментарии (1)

  • 22 декабря 2016 в 23:54

    0

    Вот это очень ценная шпора, спасибо!
    Не часто требуется что-то экзотическое использовать и приходится в доку лезть, а она немного дубовая. Тут все в одном месте, и наглядно.

© Habrahabr.ru