TCL. Транзакции. ACID

TCL. Транзакции. ACID

Дорогу осилит идущий

В рамках сегодняшней статьи начнем знакомство с одной из ключевых концепций как в СУБД (не только реляционных), так и в разработке в целом.

Транзакция представляет собой набор операций (запросов) к БД, которые считаются логической единицей работы с БД.

Т.е. транзакция - это группа объединенных определенным синтаксисов запросов, которые в рамках СУБД рассматриваются как одно действие (по крайней мере, верхнеуровнево).

Из определения вытекают ряд свойств (они важны и спросят вас о них почти на любом собесе независимо от уровня), которые образуют акроним - ACID.

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


Синтаксис

Базовая синтаксическая конструкция достаточно проста:

begin;
-- Место для ваших запросов
commit;
* Синтаксис транзакций может незначительно отличаться в разных СУБД, но общий принцип остается неизменным.

В рамках примеров будем взаимодействовать с простенькой табличкой:

create table tt (
  col1 int    primary key,
  col2 int,
  col3 int
);

Итак, пример транзакции:

begin;
insert into tt values (0, 0, 0);
insert into tt values (1, 0, 0);
commit;

В данном случае мы открываем транзакцию оператором BEGIN, выполняем две операции вставки, после чего завершаем - коммитим - транзакцию с помощью оператора COMMIT.

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

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

begin;
insert into tt values (2, 0, 0); -- запрос выполнен успешно
insert into tt values (1, 0, 0); -- ошибка: такое значение РК уже есть
insert into tt values (1, 0, 0); -- этот запрос не будет запущен
commit;

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

Наиболее корректный способ это сделать - использовать оператор ROLLBACK:

rollback;

Он откатит текущую транзакцию, в т.ч. отменит запросы, которые были выполнены успешно.

Однако в PostgreSQL вместо явного вызова ROLLBACK можно использовать и COMMIT - если в транзакции возникла ошибка, этот оператор тоже произведет откат транзакции.

!NB: если вы запускаете несколько запросов разом (например, через клиент с GUI), запросы после первого ошибочного выполняться не будут. В т.ч. команда COMMIT. Ее (или ROLLBACK) потребуется вызвать отдельно

На этом базовое знакомство с синтаксисом можно завершить - скорее всего, этого за глаза хватит при взаимодействии с БД в рамках Java-разработки.

Однако SQL предоставляет еще одну возможность в рамках TCL - создавать промежуточные точки сохранения внутри транзакции, чтобы была возможность не откатывать транзакцию целиком. 

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

Как бы там ни было, рассмотрим синтаксис точек сохранения:

begin;
insert into tt values (2, 0, 0);
savepoint s;
insert into tt values (1, 0, 0);
rollback to s;
insert into tt values (3, 0, 0);
commit;

С помощью оператора SAVEPOINT мы создаем точку сохранения с заданным именем (здесь - s, но может быть любой литерал). После операции, которая завершается с ошибкой, можем указать необходимость отката к точке сохранения через оператор ROLLBACK TO. После чего продолжаем работу в рамках транзакции в штатном режиме. Если же ошибка не возникла - не вызываем ROLLBACK TO, можно выполнять другие запросы.

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

Также стоит отметить, что различные СУБД по-разному определяют перечень транзакционных (которые могут быть отменены в случае ошибки) операций. Обычно к таковым относится DML. А вот с DDL-запросами могут быть варианты. Например, PostgreSQL поддерживает транзакционность почти всех DDL-операций (кроме кластерных, которые мы не затрагиваем в рамках курса), а, скажем, Oracle Database DDL считает не транзакционным - в случае ошибки в рамках транзакции, примененные в ней DDL-запросы не будут откачены. Поэтому рекомендую проверять ограничения СУБД, прежде чем использовать не DML-запросы в рамках транзакции - во избежание неприятных последствий.

Теперь, познакомившись с синтаксисом, вернемся к ACID-свойствам.


ACID

ACID является акронимом от четырех основополагающих свойств:

  1. Atomicity - атомарность;
  2. Consistency - согласованность;
  3. Isolation - изоляция;
  4. Durability - устойчивость.

Чтобы транзакция являлась таковой, она обязана обеспечивать сохранение всех четырех свойств.


Атомарность

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

Механизм точек сохранения и ролбэка к ним в данном случае вводит свою атомарную логическую единицу внутри транзакции - запрос/запросы между созданием точки сохранения и ролбэком (если он есть). Запросы внутри этой единицы могут быть как полностью откачены, если ролбэк был. Если же его не было - то и некой внутренней атомарной единицы не было. Оба варианта будут корректны в рамках атомарности всей транзакции.

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

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

* в ряде СУБД - также принимается сохранение структуры данных - если СУБД поддерживает транзакционность DDL.


Согласованность

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

В узком смысле под согласованностью можно подразумевать выполнение всех constraint’ов и других механизмов (например, триггеров, с которыми мы познакомимся через несоклько уроков), которые существуют в рамках БД. 

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

Надо понимать, что если мы говорим о транзакциях на уровне БД - мы не можем гарантировать сохранения консистентности данных в широком смысле, поскольку не можем это валидировать на уровне БД. Но если мы говорим о транзакционности в рамках системы (транзакционность - это не только про БД) - такая трактовка уже имеет право на жизнь.


Изоляция

Изоляция транзакции определяет правила и допущения при одновременном выполнении нескольких транзакций.

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

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


Устойчивость

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


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


Вместо вывода

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


С теорией на сегодня все!

Переходим к практике:

Задача 1

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

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


Задача 2

Добавьте еще двух пассажиров, но теперь они берут билеты туда-обратно (Скажем, Минск-Москва и Москва-Минск). Поэтому при возникновении проблем с добавлением одного из билетов, второй тоже не должен быть добавлен. Сам пассажир, при этом, должен быть сохранен.


Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)

Канал: https://t.me/ViamSupervadetVadens

Мой тг: https://t.me/ironicMotherfucker

 

Дорогу осилит идущий!

Report Page