
Hodie mihi, cras tibi
Небольшой технический ликбез простыми словами о транзакциях и ACID, основы баз данных.
В целом, транзакцией называется группа операций, которая должна выполниться как единое целое, иначе не имеет смысла. К примеру, когда вы переводите деньги по СБП, они должны списаться с карты банка А и зачислиться на карту банка Б. Эти операции имеют смысл только в комплексе.
ACID — это группа требований к транзакциям и системам, работающим с ними (например, базам данных). ACID требует, чтобы каждая транзакция:
- В случае проблемы в процессе выполнения, не зависала, а откатывалась, возвращая систему к предыдущему состоянию.
- После своего завершения не оставляла данные неконсистентными (консистентность). Что такое консистентность, напишу ниже.
- Не влияла на другие транзакции (на самом деле влияла, но как можно меньше — 4 уровня изолированности, о них тоже ниже).
- Вся система гарантировала, что выполненные транзакции будут зафиксированы системой, даже при возникновении аварий и форс-мажоров (стойкость, durability, и о ней ниже).
Подробнее о каждом требовании с примерами:
Atomicity/Атомарность
Выполняются либо все команды, либо ни одной.
На практике атомарность реализуется через версионирование и откаты (rollback) команд транзакции до первоначального состояния базы. Строго говоря, индексы могут обратно не откатиться, но чаще всего это разруливается на уровне СУБД.
Сonsistency/Консистентность
После исполнения транзакции, данные не должны содержать противоречий.
Пример: поле итога в табличке должно содержать в точности сумму всех значений выше (логическая консистентность.), запись одной таблицы не должна ссылаться на удалённый айдишник другой записи (техническая консистентность.)
Isolation/Изолированность
При параллельном выполнении, транзакции не должны влиять друг на друга и должны выполняться изолированно.
Пример: если два человека одновременно делают перевод третьему, одна транзакция может перезаписать другую с потерей денег. Изолированность исключает такую ситуацию.
Durability/Стойкость
Если транзакция завершилась, она не может быть откачена ни при каких условиях, даже если в ЦОД случится авария. Если такое произойдёт, система должна уметь восстанавливать транзакции.
Уровни изолированности в базах данных
Реализация изолированности технически сложна и требует больших вычислительных ресурсов. Поэтому существуют четыре уровня изолированности, от меньшей надёжности к большей. Соответственно, чем выше надёжность, тем более мощного железа решение требует:
Read uncommitted
Ограждает от случая из начала статьи — когда, например, два человека одновременно делают перевод по СБП третьему. На этом уровне UPDATE-транзакции резервируют данные для себя и блокируют для других UPDATE-транзакций.
Тем не менее, SELECT-запросы не блокируются и даже могут считать промежуточное состояние данных, возникающие между выполнением команд одной транзакции.
По факту, тут нормальная изоляция отсутствует. Где‑нибудь в аналитике больших данных такой уровень может ещё использоваться, где нам не так важна точность данных, но это достаточно редко.
Пример: транзакция на перевод денег состоит из команды на списание денег с одного счёта и команды пополнения другого счёта. На момент изменения баланса счёта первой транзакцией, вторая подобная транзакция встанет в очередь, пока данные не освободятся. Но SELECT-запрос может считать состояние, когда деньги списаны с одного счёта, но на второй ещё не зачислены.
Read committed
То же, что выше + решает проблему чтения «грязных» состояний незавершённых транзакций. Большинство баз данных по умолчанию работает на этом уровне изолированности.
На этом уровне транзакция может читать только те изменения в других параллельных транзакциях, которые уже были закоммичены. Это нас спасает от грязного чтения, но не спасает от неповторяющегося чтения и от фантомного чтения.
Однако если транзакция содержит две SELECT-команды и во время между ними другая UPDATE-транзакция успешно завершится, то результат этих SELECT-запросов может отличаться: один вернёт состояние до UPDATE-транзакции, второй — после. Важно, что это не «грязное» состояние, а чистое, после успешной транзакции.
Такой уровень по умолчанию используется, например, в PostgreSQL, MS SQL и Oracle.
Repeatable read (повторяемость чтения)
Оба пункта выше + гарантирует, что SELECT-запросы в рамках одной транзакции всегда будут возвращать один и тот же результат, даже если другие транзакции обновляют или удаляют эти же данные.
Этот уровень означает, что пока транзакция не завершится, никто параллельно не может изменять или удалять строки, которые транзакция уже прочитала. Т. е. данные, которые я прочитал своей транзакцией, точно никто не изменит, пока я не завершу свою транзакцию (по крайней мере, в классическом понимании этого уровня с блокировками).
Транзакция блокирует все строки, затрагиваемые её командами, включая SELECT, а другие транзакции с SELECT-, UPDATE- и DELETE-запросами к этим данным ждут её завершения. Естественно, это сильно снижает скорость обработки транзакций базой данных.
Уровень REPEATABLE READ используется по-умолчанию в MySQL.
Serializable
Три пункта выше + исключает «фантомные чтения». «Фантомное чтение» похоже на проблему с двумя последовательными SELECT-запросами одной транзакции, но возникает, когда между SELECT-запросами была выполнена именно вставка (INSERT).
Самый жёсткий, но самый тяжёлый для БД и медленный для обработки запросов уровень. Он блокирует любые действия, пока запущена транзакция — получается, транзакции идут строго одна за другой и максимально изолируются друг от друга. Это достигается с помощью блокировки всей таблицы от любых взаимодействий с ней, но некоторые СУБД делают менее радикально — блокируют только те строки, которые задействует текущая транзакция.
Пример: агрегационные запросы SELECT SUM(), SELECT COUNT(). Это максимальный уровень изолированности.





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