Уровни изоляции транзакций в БД
В этой статье я собрал сводку базовых знаний об уровнях изолированности транзакций. Понятно, что для полного погружения нужно копать глубже, да и использование уровней отличается в зависимости от СУБД, но кажется, что для первого знакомства с изоляцией транзакций в БД этой статьи должно быть вполне достаточно. Если ваш опыт отличается от того, что описано в статье (а это вполне возможно), то приглашаю в комментарии :)
Уровень изоляции (изоляция или изолированность — это, кстати, буква »I» в аббревиатуре ACID — »Isolation») определяет, как транзакции могут взаимодействовать между собой, и насколько сильно могут пересекаться и мешать друг другу при параллельной работе. Иначе говоря, разные уровни изоляции допускают или не допускают разные аномалии при параллельной работе транзакций (про аномалии расскажем дальше).
Всего есть 4 основных уровня изоляции:
READ UNCOMMITED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
Давайте каждый из уровней разберём подробно:
READ UNCOMMITED
Это самый слабый уровень изоляции, когда транзакция может видеть результаты других транзакций, даже если они ещё не закоммичены.
Например, моя транзакция делает SELECT баланса из аккаунта 1, а вторая транзакция параллельно меняет этот баланс, но не коммитит изменения. Даже без коммита второй транзакции мой селект вернёт новый изменённый баланс. А если вторая транзакция откатит изменения и я опять сделаю SELECT к балансу, то я получу уже старый баланс. Это как раз аномалия, которая называется «грязное чтение» (Dirty Read) — когда данные, которые я прочитал, кто-то может откатить ещё до того, как я завершу свою транзакцию.
Грязное чтение в READ UNCOMMITED
Причём в этой ситуации моя первая транзакция прочитает баланс, и она явно как-то будет его использовать дальше в своей логике, а параллельно вторая транзакция, несмотря на это, сможет изменить баланс — получается, что я в своей транзакции использовал уже неактуальные данные — это аномалия »неповторяющееся чтение» (Non-repeatable Read или Fuzzy Read) — когда данные, которые я прочитал, кто-то может изменить ещё до того, как я завершу свою транзакцию.
Неповторяющееся чтение в READ UNCOMMITED
Ну и в-третьих, может быть ситуация, когда я своей первой транзакции прочитал какие-то строки из БД (например, выбрал все аккаунты с балансом = 0), а вторая транзакция параллельно обнулила баланс на ещё одном аккаунте — получается, моя первая транзакция думает, что аккаунтов с нулевым балансом у нас, например, 10, а по факту их будет уже 11. Это аномалия »фантомное чтение» (Phantom Read) — когда ряд данных, которые я прочитал, кто-то может изменить до того, как я завершу свою транзакцию.
Фантомное чтение в READ UNCOMMITED
В общем, вот такой у нас уровень изоляции READ UNCOMMITED — по факту, тут изоляция отсутствует. Где-нибудь в аналитике больших данных такой уровень может ещё использоваться, где нам не так важна точность данных, но это достаточно редко.
READ COMMITTED
На этом уровне транзакция может читать только те изменения в других параллельных транзакциях, которые уже были закоммичены. Это нас спасает от грязного чтения, но не спасает от неповторяющегося чтения и от фантомного чтения. Т. е., во-первых, я могу в своей транзакции прочитать баланс аккаунта, только который уже закоммичен, но моя транзакция ещё не закончена, я делаю что-то ещё, а потом ещё раз в этой же транзакции хочу ещё раз прочитать баланс этого аккаунта, но за это время уже какая-то другая транзакция изменила баланс — получается, в начале своей транзакции я получу один баланс, а в конце при запросе баланса того же аккаунта я получу уже другую сумму.
Отсутствие грязного чтения в READ COMMITED
Неповторяющееся чтение в READ COMMITED
От фантомного чтения этот уровень тоже не защищает ровно по тем же причинам — мы два раза в транзакции делаем один и тот же SELECT, но получаем в них разные результаты, потому что кто-то между моими селектами изменил набор нужных мне строк.
Фантомное чтение в READ COMMITED
Такой уровень по умолчанию используется, например, в PostgreSQL, MS SQL и Oracle (если ничего не поменялось с момента написания статьи).
REPEATABLE READ
Этот уровень означает, что пока транзакция не завершится, никто параллельно не может изменять или удалять строки, которые транзакция уже прочитала. Т. е. пока транзакция открыта, то прочитанные ею строки точно будут оставаться в том виде, когда она их прочитала.
Это нас спасает и от грязного чтения, и от неповторяющегося чтения, но всё ещё мы не решаем проблему фантомного чтения. Почему? Конечно, строки, которые я уже прочитал в транзакции, никто не изменит, но никто не мешает добавлять новые строки параллельно — их мы ещё не читали, создавать их ничего не мешает.
Отсутствие неповторяющегося чтения в REPEATABLE READ
Фантомное чтение в REPEATABLE READ
Уровень REPEATABLE READ используется по умолчанию в MySQL. И, кстати, в InnoDB (движок для хранения данных в MySQL) с уровнем изоляции REPEATABLE READ даже фантомное чтение не страшно. Почему? Там есть штука, которая называется MVCC — о ней мы чуть позже поговорим.
SERIALIZABLE
Самый жёсткий, но самый тяжёлый для БД и медленный для обработки запросов уровень. Он блокирует любые действия, пока запущена транзакция — получается, транзакции идут строго одна за другой и максимально изолируются друг от друга. Это достигается с помощью блокировки всей таблицы от любых взаимодействий с ней, но некоторые СУБД делают менее радикально — блокируют только те строки, которые задействует текущая транзакция.
Тут никакие аномалии нам не страшны — ни грязное, ни неповторяемое, ни фантомное чтение. Но и производительность тут страдает, ведь транзакции не могут работать параллельно.
Отсутствие неповторяющегося чтения в SERIALIZABLE
Отсутствие фантомного чтения в SERIALIZABLE
Это 4 основных уровня изоляции. Есть ещё несколько уровней не таких популярных, но об этом мы поговорим ниже.
В итоге, зачем нам все эти уровни? Почему не обойтись каким-нибудь самым безопасным SERIALIZABLE, который не допускает никаких аномалий, или каким-нибудь среднячком READ COMMITED? Потому что в реальной жизни бизнесовые задачи бывают совсем разные: где-то нам важно сохранять максимальную целостность данных и изолировать транзакции так, чтобы они не пересекались друг с другом, т. к. при конфликте или ошибке цена будет велика, а где-то мы можем смириться с какими-то кейсами нестыковок в данных, но зато у нас будет производительность — мы сможем запускать больше транзакций одновременно.
Что касается аномалий, то в примерах мы рассмотрели основные 3 аномалии:
Грязное чтение (Dirty Read)
Неповторяющееся чтение (Non-repeatable Read)
Фантомное чтение (Phantom Read).
Но также на уровнях изоляции READ UNCOMMITTED и READ COMMITTED могут ещё возникнуть такие аномалии, как:
Потерянное обновление (Lost Update) — когда две транзакции одновременно читают и изменяют одни и те же данные, и при этом одно из изменений может потеряться.
Неупорядоченное чтение (Out-of-order Read) — когда несколько чтений выполняются в произвольном порядке, что может привести к неправильным результатам в транзакциях.
Другие уровни изоляции
Кроме стандартных перечисленных уровней изоляции есть ещё специфичные уровни READ STABILITY и CURSOR STABILITY, которые используются в Db2 (СУБД от IBM) — они предлагают уже более тонкую настройку изоляции.
CURSOR STABILITY (CS) блокирует текущую строку, прочитанную через некий курсор (курсор мы специально ставим в запросе, и он будет, например, ходить по каждой строке нашего запроса по порядку). С уровнем CURSOR STABILITY мы блокируем только ту строку, на которой сейчас находится курсор.
READ STABILITY (RS) обеспечивает ситуацию, когда данные, прочитанные одной транзакцией, не будут изменены другой транзакцией до завершения первой транзакции. Т. е. тут нет проблемы неповторяющегося чтения, но могут быть фантомные чтения.
Тут углубляться не будем — эти уровни достаточно специфичные и актуальны именно для Db2 (по крайней мере, я их не встречал в других СУБД).
В SQL Server есть ещё SNAPSHOT ISOLATION (многоверсионная изоляция), которая позволяет транзакциям видеть снимок базы данных на момент начала транзакциии, устраняя тем самым фантомное чтение и неповторяющееся чтение и не блокирует данные при их чтении, что обеспечивает параллелизм (одновременное чтение одних и тех же данных разными транзакциями). Но SNAPSHOT ISOLATION не гарантирует полную сериализуемость, т. к. возможны конфликты при записи, когда две транзакции пытаются изменить одну и ту же запись (тогда одна из операций упадёт в ошибку).
Как можно переключаться между уровнями?
Конкретный уровень изоляции можно подключать с помощью команды SET ISOLATION LEVEL, и можно также назначить, на какие транзакции будет действовать подключаемый уровень.
На примере подключения уровня REPEATABLE READ в MySQL (в других СУБД команды могут отличаться):
--Будет действовать на 1 следующую транзакцию в текущей сессии
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
--Будет действовать на все транзакции в текущей сессии
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
--Вариант включения уровня изоляции в PostgreSQL
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
--Будет действовать на все транзакции
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Что такое MVCC
Помните, мы сказали, что благодаря некому MVCC InnoDB не допускает фантомное чтение уже на уровне REPEATABLE READ?
Так вот MVCC (Multiversion Concurrency Control) — это метод управления конкурентным доступом к данным в БД, который позволяет нескольким транзакциям работать с данными одновременно без конфликтов. MVCC поддерживает высокую производительность и изоляцию транзакций, минимизируя блокировки и улучшая параллелизм.
Но как он это делает?
Многоверсионность:
MVCC использует механизм многоверсионности для управления изменениями данных. Вместо того чтобы изменять данные в месте их хранения, MVCC создает новые версии данных. Каждая транзакция видит данные в том состоянии, в котором они были на момент начала транзакции, а не в текущем состоянии.
При записи данных создается новая версия строки. Новая версия включает информацию о том, какая транзакция создала ее, и становится видимой для транзакций, которые начинаются после ее создания.
Благодаря тому, что в версии указано, какая транзакция создала её и когда данные были созданы или удалены, можно определить, какая версия доступна для конкретной транзакции.
Время от времени СУБД очищает старые версии данных, которые больше не нужны (т. е. которые уже не используются ни одной транзакцией). Это называется сбором мусора и помогает поддерживать эффективность системы.
Изоляция транзакций:
Транзакции видят только те данные, которые были зафиксированы до их начала. Это предотвращает грязное чтение, неповторяющееся чтение и фантомное чтение.
Отсутствие блокировок для чтения:
Чтение данных не блокирует запись, а запись не блокирует чтение. Это повышает производительность системы, поскольку транзакции могут работать с данными параллельно.
Управление конфликтами:
Конфликты между транзакциями, например, две транзакции, пытающиеся изменить одну и ту же строку, решаются при коммите транзакций. Если одна из транзакций не может быть закоммичена из-за конфликта, её можно откатить.
В итоге, MVCC — это такая штука, которая позволяет каждой транзакции работать с её собственной версией базы (грубо говоря), не влияя на остальные транзакции и не блокируя данные.