Транзакции. Параллельное исполнение. Уровни изоляции
В контексте баз данных очень часто возникает параллельное исполнение транзакций, если в системе параллельно с одними и теми же данными работает более одного полльзователя. Тем не менее, при такой работе мы все еще должны уметь обеспечивать все четыре свойства ACID (атомарность, согласованность, изоляцию и устойчивость). Следовательно, при проектировании СУБД необходимо учесть проблемы, которые могут возникнуть при параллельной обработке транзакций. Транзакция — это логическая единица работы, в ходе которой может выполняться некоторый набор действий с объектами базы данных.
Изоляция транзакций
Свойство изолированности говорит нам о том, что:
- в системе могут параллельно исполняться две и более транзакции;
- при этом транзакция должна уметь выполняться так, как будто она в сисетме одна;
- кроме того, если мы выполнили по отдельности набор транзакций параллельно, то при попытке посмотреть на все эти транзакции в совокупности мы должны увидеть, что их совместный результат также корректен.
Проблемы (аномалии) при параллельной обработке транзакций
Косая запись
Аномалия "косой записи" — аномалия, которая возникает при ситуации когда мы из двух разных транзакций пытаемся изменить одни и те же данные (например, ячейки А и В) так, что первая транзакция затронет ячейку A, а вторая — ячейку В и мы получим неконсистентный результат.
Пример возникновения аномалии:
Было:
t_1 = 50 t_2 = 50 Δ = 60
Транзакция Т1:
if t_1 + t_2 ≥ Δ begin t_1 = t_1 − Δ end if
Транзакция Т2:
if t_1 + t_2 ≥ Δ begin t_2 = t_2 − Δ end if
Стало:
t_1 = -10 t_2 = -10
В таком примере видно, что мы не могли выполнить обе транзакции так, чтобы сохранился инвариант данных, следовательно мы получаем аномалию. При этом стоит отметить, что такая пара транзакций успешно завершится, поскольку каждая из них проверит неизменность данных только в тех ячейкх базы, которые были изменены в ходе транзакции (t_1 для T1, t_2 для T2).
Фантомная запись
Аномалия "фантомная запись" — это аномалия, которая возникает в том случае, когда одна и та же транзакция пытается повторно считать данные из какой-то таблицы, но между двумя этими считываниями какая-то другая транзакция занесла изменения в эту таблицу. В таком случае при повторном чтении мы увидим новые данные, которых не было ранее.
Неповторяемое чтение
Аномалия "неповторяемое чтение" — это аномалия, которая возникает при повторном чтении ячейки таблицы, в которую были между этими чтениями внесены изменения. Таким образом, при повторном чтении мы можем увидеть вовсе не те данные, что были там раньше.
Грязное чтение
Грязное чтение — это аномалия, которая возникает при чтении еще не зафиксированных другой транзакцией изменений. Таким образом, мы можем увидеть данные, которые еще даже не были помечены как внесенные в базу.
Уровни изоляции транзакций
Сводная таблица
Ниже приведена сводная таблица, где для каждого из существующих уровней изоляции указано, какие аномалии ему характерны (минус означает, что соответствующей гарантии нет и возможна аномалия):
Аномалия / Уровень | Serializable | Snapshot | RepeatableRead | ReadCommitted | ReadUncommitted |
---|---|---|---|---|---|
Косая запись | - | + | + | + | + |
Фантомная запись | - | - | + | + | + |
Неповторяемое чтение | - | - | - | + | + |
Грязное чтение | - | - | - | - | + |
Уровень изоляции Serializable
При этом уровне изоляции мы гарантируем полную упорядочиваемость всех совершаемых транзакций, вследствие чего не возникнет ни одна из аномалий, перечисленных выше. Пример создания такой транзакции:
create transaction isolation level serializable; -- your query goes here commit;
Уровень изоляции Snapshot
Название этого уровня изоляции на русский язык можно перевести как слепок, что, на самом деле, приводит к тому, что если две транзакции совершаются прааллельно, то каждой из них выдают свой "слепок" базы данных в какой-то определенный момент.
После выполнения всех операций со слепком базы данных (удаление/запись/изменение/чтение) все изменения "вливаются" в основную версию базы. Транзакция будет завершена успешно, если в основной версии базы данных к моменту окончания транзакции ни в одной из ячеек базы, измененных в ходе транзакции, не было изменений за время ее выполнения. Таким образом, если две транзакции выполняли операции над разными частями базы данных, то конфликтов у нас не возникнет и соответствующее слияние произойдет безболезненно. Если же изменялись одни и те же данные, мы можем получить аномалию "косой записи" (см. выше).
Стоит отметить, что формально такого уровня изоляции нет в стандарте языка SQL.
Уровень изоляции Repeatable read
При данном уровне изоляции выполняется гарантия, что при повторном чтении одного и того же поля записи в базе мы будем получать одни и те же значения в ходе транзакции. Исключение составляют те изменения, которые мы сами внесли в базу.
В базах данных, которые реализуют изоляцию посредством блокировок обеспечение такого уровня изоляции будет выполняться за счет блокировки или отдельных записей в таблицах, или страниц в целом.
Тем не менее, можно заметить, что описанная выше гарантия ничего не говорит о том, что мы не увидим при повторном чтении новых данных в базе. То есть может возникнуть ситуация, когда таблица при повторном чтении увеличится в размере. Такая аномалия называется аномалией "фантомной записи" (см. выше).
Пример создания такой транзакции:
create transaction isolation repeatable read; -- your query goes here commit;
Уровень изоляции Read committed
При этом уровне изоляции существует гарантия, что мы увидим любые изменения, которые были зафиксированы другими транзакциями. При этом можно заметить, что если мы дважды читаем информацию из одной ячейки, то между этими чтениями другая транзакция могла внести свои изменения, что влечет за собой проблему "неповторяемого чтения".
Стоит отметить, что при таком уровне изоляции базе данных не нужно полностью брать блокировку на запись или таблицу — в этом случае можно использовать частичную блокировку записей или страниц.
Пример создания такой транзакции:
create transaction isolation read committed; -- your query goes here commit;
Уровень изоляции Read uncommitted
При таком уровне изоляции транзакций у нас вовсе отсутствуют какие-либо блокировки, следовательно мы ничего не можем гарантировать пользователю. При таком уровне изоляции пользователь увидит любые текущие данные в базе данных, в том числе, он сможет увидеть "грязные данные" — то есть данные, которые еще не были зафиксированы ни одной из транзакций и впоследствии могут быть вовсе откачены.
Вследствие того, что идея изменять данные в базе основываясь на еще незакомиченных данных звучит совсем безумно, стандартом SQL прописано, что транзакции, работающие на уровне изоляции read uncommitted, могут только читать данные, но не изменять их.
Стоит отметить, что использование таких транзакций является разумным в том случае, когда мы хотим посчитать некоторую статистику (например количество студентов в институте или сумму всех денег в банке). В таком случае мы будем довольно часто выполнять запросы на подсчет количества и нам не очень важно, совсем ли точно мы получаем число. Давайте разберем пример со студентами более подробно. Пусть у нас было два момента времени t1 - начало транзакции, t2 - конец транзакции. Пусть количество студентов в эти моменты времени не совпадало. В таком случае, мы понимаем, что за этот промежуток времени было совершено какое-то количество операций с интересующими нас данными, и мы, на самом деле, не можем достоверно назвать ответ о количестве студентов в этот временной интервал. Таким образом, для таких статистических запросов нам важно понимать, как в целом меняется количество студентов во времени — растет оно или падает, но нам не очень важно однозначно знать, сколько студентов было в институте сейчас с точностью до одного. В таких запросах важнее, чтобы они не тормозили базу, а данные, которые мы получаем в каждый момент могут быть не до конца точными.
Пример создания такой транзакции:
create transaction isolation read uncommitted; -- your query goes here commit;