Хабрахабр

Уровни изолированности транзакций для самых маленьких

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

Немного теории

Сами транзакции особых объяснений не требуют, транзакция — это N (N≥1) запросов к БД, которые выполнятся успешно все вместе или не выполнятся вовсе. Изолированность же транзакции показывает то, насколько сильно влияют друг на друга параллельно выполняющиеся транзакции.
Выбирая уровень транзакции, мы пытаемся прийти к консенсусу в выборе между высокой согласованностью данных между транзакциями и скоростью выполнения этих самых транзакций.
Стоит отметить, что самую высокую скорость выполнения и самую низкую согласованность имеет уровень read uncommitted. Самую низкую скорость выполнения и самую высокую согласованность — serializable.

Подготовка окружения

Для примеров была выбрана СУБД MySQL. PostgreSQL мог бы тоже использоваться, но он не поддерживает уровень изоляции read uncommitted, и использует вместо него уровень read committed. Да и как оказалось, разные СУБД по-разному воспринимают уровни изолированности. Могут иметь разнообразные нюансы в обеспечении изоляции, иметь дополнительные уровни или не иметь общеизвестных.

И заполним базу данными. Создадим окружение с помощью готового образа MySQL с Docker Hub.

docker-compose.yaml

version: '3.4'
services: db: image: mysql:8 environment: - MYSQL_ROOT_PASSWORD=12345 command: --init-file /init.sql volumes: - data:/var/lib/mysql - ./init.sql:/init.sql expose: - "3306" ports: - "3309:3306" volumes: data:

Заполнение базы данных

create database if not exists bank; use bank; create table if not exists accounts
( id int unsigned auto_increment primary key, login varchar(255) not null, balance bigint default 0 not null, created_at timestamp default now()
) collate=utf8mb4_unicode_ci; insert into accounts (login, balance) values ('petya', 1000);
insert into accounts (login, balance) values ('vasya', 2000);
insert into accounts (login, balance) values ('mark', 500);

Рассмотрим как работают уровни и их особенности.
Примеры будем выполнять на 2 параллельно исполняющихся транзакциях. Условно транзакция в левом окне будем называть транзакция 1 (Т1), в правом окне — транзакция 2 (Т2).

Read uncommitted

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

Шаг 1. Начинаем 2 параллельные транзакции.

Шаг 2. Смотрим какая информация имеется у нас в начале.

Шаг 3. Теперь выполняем операции CREATE, DELETE, UPDATE в Т1, и посмотрим, что теперь видит другая транзакция.

Т2 видит данные другой транзакции, которые еще не были зафиксированы.

Шаг 4. И Т2 может получить какие-то данные.

Шаг 5. При откате изменений Т1, данные полученные Т2 окажутся ошибочными.

Результат COUNT(*) или MAX(*) можно использовать в каких-нибудь нестрогих отчетах.
Другой пример это режим отладки. На данном уровне нельзя использовать данные, на основе которых делаются важные для приложения выводы и критические решения т.к выводы эти могут быть далеки от реальности.
Данный уровень можно использовать, например, для примерных расчетов чего-либо. Когда во время транзакции, вы хотите видеть, что происходит с базой.

Read committed

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

Шаг 1 и Шаг 2 аналогичны предыдущему примеру.

Также выполним 3 простейшие операции с таблицей accounts (Т1) и сделаем полную выборку из этих таблиц в обеих транзакциях. Шаг 3.

И увидим, что феномен грязного чтения в Т2 отсутствует.

Зафиксируем изменения Т1 и проверим, что теперь видит Т2. Шаг 4.

Это так называемые феномен неповторяющегося чтения, когда мы видим обновленные и удаленные строки (UPDATE, DELETE), и феномен чтения фантомов, когда мы видим добавленные записи (INSERT). Теперь Т2 видит все, что сделала Т1.

Repeatable read

Уровень, позволяющий предотвратить феномен неповторяющегося чтения. Т.е. мы не видим в исполняющейся транзакции измененные и удаленные записи другой транзакцией. Но все еще видим вставленные записи из другой транзакции. Чтение фантомов никуда не уходит.

Снова повторяем Шаг 1 и Шаг 2.

В Т1 выполняем запросы CREATE, UPDATE и DELETE. Шаг 3. После, в Т2 пытаемся обновить ту же самую строку, которую обновили в Т1.

И получаем lock: T2 будет ждать, пока T1 не зафиксирует изменения или не откатится.

Зафиксируем изменения, которые сделала Т1. Шаг 4. И прочитаем снова данные из таблицы accounts в Т2.

Как же так, ведь по умолчанию, repeatable read позволяет нам предотвратить только феномен неповторяющегося чтения? Как видно, феноменов неповторяющегося чтения и чтения фантомов не наблюдается.

И в PostgreSQL от него тоже избавились для этого уровня. На самом деле в MySQL отсутствует эффект чтения фантомов для уровня repeatable read. Хотя в классическом представлении этого уровня, мы должны наблюдать этот эффект.

Например, злоумышленник сгенерировал себе код сертификата и пытается его активировать, пытаясь послать несколько запросов подряд на активацию купона. Небольшой абстрактный пример — сервис генерации подарочных сертификатов (кодов) и их использования. И в некоторых ситуациях может возникнуть двойная или даже тройная активация купона (пользователь получит 2x/3x бонусов). В таком случае у нас запустится несколько параллельно исполняемых транзакций, работающих с одним и тем же купоном. Подобную проблему можно также решить с помощью запроса SELECT FOR UPDATE, который также заблокирует обновляемую запись (купон). При repeatable read в данном случае возникнет lock и активация пройдет единожды, а в предыдущих 2 уровнях возможна многократная активация.

Serializable

Уровень, при котором транзакции ведут себя как будто ничего более не существует, никакого влияния друг на друга нет. В классическом представлении этот уровень избавляет от эффекта чтения фантомов.

Шаг 1. Начинаем транзакции.

Шаг 2. Т2 читаем таблицу accounts, затем Т1 пытаемся обновить данные прочитанные Т2.

Получаем lock: мы не можем изменить данные в одной транзакции, прочитанные в другой.

Шаг 3. И CREATE и DELETE ведет нас к lock'у в Т1.

Мы получаем максимальную согласованность данных, никакие лишние данные не зафиксируются. Пока Т2 не завершит свою работу, мы не сможем работать с данными, которые она прочитала. Цена за это медленная скорость транзакций из-за частых lock'ов поэтому при плохой архитектуре приложения это может сыграть с Вами злую шутку.

Выводы

В большинстве приложений уровень изолированности редко меняется и используется значение по умолчанию (например, в MySQL это repeatable read, в PostgreSQL — read committed).

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»