Что такое ACID и причем тут базы данных? - LEFT JOIN

Свяжитесь с нами в любой удобной для вас форме

Менеджер

Написать в телеграмм

Онлайн
Телеграмм
или
Заполните форму

1 минута чтения

*

30 января 2024 г.

Что такое ACID и причем тут базы данных?

Что такое ACID и причем тут базы данных

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

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

Пример исчерпывающе доносит смысл, но дьявол, как всегда, кроется в деталях, а точнее в деталях реализации. Во-первых, СУБД, поддерживающие транзакции, должны удовлетворять гарантиям функциональной безопасности ACID (это аббревиатура, которая ничего общего с химией не имеет). Во-вторых, есть понятие уровней изоляции транзакции, всего существует 4 различных уровня. Давайте начинать!

A — Atomicity

Как мы уже сказали, ACID — аббревиатура, каждая буква которой обозначает свойство транзакций. Первая буква «А» — atomicity, то есть атомарность.

В большинстве СУБД для того, чтобы начать транзакцию, необходимо выполнить запрос с выражением START TRANSACTION. Все дальнейшие действия будут относиться к начатой транзакции. Чтобы завершить транзакцию, нужно выполнить выражение COMMIT, а если вы хотите отменить все действия транзакции, то можно «откатиться», используя выражение ROLLBACK.

И именно атомарность гарантирует, что ВСЕ операции между START TRANSACTION и COMMIT либо выполнятся, либо не выполнятся — промежуточного состояния не будет.

В качестве примера вспомним опять про банковское приложение:

Николай хочет перевести с одного счета на другой 200 рублей. Для этого сначала с первого счета будет списано 200 рублей, а уже потом на второй счет эти деньги будут зачислены. На первом счету у него 400 рублей, на втором — 500.

Если после первого UPDATE питание в здании отключится и сервер базы данных выключится, то на счетах останется 400 и 500 рублей, как будто никакой транзакции и не было.

C — Consistency

Вторая буква «C» — consistency, то есть согласованность или консистентность.

Согласованность в рамках баз данных можно понимать как выполнение некоторых утверждений относительно данных в таблицах. На примере про перевод между счетами Николая можно сформировать такое утверждение: «При переводе между счетами сумма денег на обоих счетах не должна измениться». Это мы и наблюдаем: до транзакции было 400+500=900 рублей, а после стало 600+300=900 рублей.

I — Isolation

Третья буква — «I» — isolation, то есть изолированность одной транзакции от любой другой.

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

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

Последнее действие, выполненное в рамках начисления через банкомат, перезатрёт все изменения от другой транзакции, что вызовет нарушение целостности данных в базе: у Николая на первом счету будет 600 рублей, а на втором 800 рублей, хотя должно быть 600. Эта ситуация называется «потерей обновления».

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

D — Durability

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

Тут все просто: все транзакции, которые были успешно «закоммичены», не должны быть потеряны ни при каких сбоях. Если база данных реплицируется (запись идет на еще один или несколько работающих БД), то сохраняемость может означать успешную запись в БД на другом сервере. Если база данных не реплицируется, то сохраняемость означает запись на энергонезависимый носитель информации (например, жесткий диск).

Уровни изоляции

READ UNCOMMITED

Самый первый уровень изоляции, который мы рассмотрим — READ UNCOMMITED. То есть он позволяет читать все изменения незавершенных (или «незакоммиченных») транзакций.

Чтение изменений незафиксированных транзакций называется «грязным чтением». Этот уровень изоляции не позволяет изменять внутри транзакции объекты (строки или таблицы или что-то другое, это зависит от конкретной СУБД и движка таблицы), уже измененные другой одновременной транзакцией («грязная запись»). Транзакция просто повиснет в ожидании COMMITа другой транзакции.

Рассмотрим похожий пример, в котором Николай хочет оплатить покупку на 100 рублей с первого счет, а чуть позже Маргарита захотела начислить ему 100 рублей через банкомат. Изначально у него на счетах было по 500 рублей. Тогда схема выполнения будет следующей:

Обновление баланса второй транзакции — это «грязная» запись, именно на этом моменте транзакция и повиснет, изменение произойдет после фиксации перевода Николая, но его можно будет отменить командой ROLLBACK, ведь будет понятно, что произошла «грязная запись».

READ COMMITED

Следующий уровень изоляции READ COMMITED. Как можно догадаться из названия, он позволяет читать только те изменения, которые были зафиксированы (или «закоммичены»).

Этот уровень изоляции, как и read uncommitted, позволяет избежать ситуации «грязной записи», но также позволяет избежать ситуации «грязного чтения». То есть внутри транзакции можно прочитать только те изменения объектов, которые были совершены внутри уже завершенных транзакций.

Вспоминая пример с предыдущей карточки, если бы уровень изоляции был «READ COMMITED», то чтение внутри операции пополнения счета Маргаритой вернуло бы неизмененные балансы счетов Николая (по 500 рублей на каждом, а не 500 и 400, как в предыдущем примере).

Как же эти два уровня изоляции реализованы технически?

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

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

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

REPEATABLE READ

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

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

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

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

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

MVCC

Для реализации такого уровня используется мультиверсионное управление конкурентным доступом (MVCC).

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

Идентификатор транзакции аудитора 9. Таким образом, все данные, которые будут прочитаны или изменены, должны иметь идентификатор не больше 9. Txid транзакции Николая — 10, внутри нее было произведено изменение двух объектов, которые были созданы транзакциями с txid 6 и 4. Изменение будет означать удаление этих версий объектов и создание новых. Когда аудитор попытается прочитать баланс первого счета Николая, СУБД увидит, что объект был создан транзакцией с бОльшим txid и возьмет версию, созданную ранее.

SERIALIZABLE

Закончим нашу статью рассказом про самый сильный уровень изоляции — SERIALIZABLE.

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

Этот уровень изоляции может быть реализован тремя способами:

1. Действительно последовательное выполнение транзакций

2. Применение двухфазной блокировки (2PL)

3. Использование сериализуемой изоляции снимков состояния (SSI).

Обо всем по порядку:

1. По-настоящему последовательное выполнение предполагает выполнение всех транзакций одна за другой, в одном потоке. Такой уровень изоляции имеет смысл применять только если транзакции выполняются быстро, чтобы другим не пришлось ждать выполнения одной большой и долгой транзакции. К тому же, если объем данных одной транзакции не помещается в память, то обращение к диску может очень сильно замедлить работу с БД.

2. Двухфазная блокировка — это усиленная версия уже упоминавшейся блокировки. Во время одновременного выполнения нескольких транзакций допускается чтение одного объекта (строки или нескольких строк), но для его изменения нужен монопольный доступ. То есть если транзакция «А» читает строки, которые транзакция «В» хочет изменить, то вторая должна дождаться завершения первой. И наоборот.

3. Сериализуемая изоляция снимков состояния — относительно свежий способ обеспечения сериализуемости транзакций. По сравнению с предыдущими двумя этот метод обеспечивает оптимистичное управление доступом, то есть без блокировок. Все чтение из базы выполняется из согласованного снимка состояния базы данных, а изменения происходят свободно, в расчете, что они не затрагивают объекты других транзакций. При фиксации транзакции происходит проверка, не были ли прочитаны или изменены уже измененные другой транзакцией объекты. Если да, то транзакция прерывается и ее выполнение приходится выполнить еще раз.

Все транзакции при этом методе должны иметь уровень изоляции Serializable.

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

2362 просмотров

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

[ Рекомендации ]

Читайте также

[ Дальше ]