Транзакции

3.4. Транзакции

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

Например, допустим, что есть база данных, которая содержит балансы для нескольких клиентов и общие депозитные балансы для филиалов. Предположим, что мы хотим внести поступление $100.00 от клиента Alice для клиента Bob. Простейшая команда, которая выполняет данную операцию может выглядеть так

UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
UPDATE branches SET balance = balance - 100.00
    WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Bob';
UPDATE branches SET balance = balance + 100.00
    WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');

Делали этих команд сейчас не важны; важно что здесь мы имеем дело с несколькими отдельными обновлениями (операторы update), которые реализуют нужную нам операцию. Наши банковские работники захотят сделать так, чтобы все эти обновления происходили сразу или чтобы не происходило ни одно из них. Это обусловлено тем, что в результате какой-либо системной ошибки может получиться так, что Bob получит $100.00, которые не будут вычтены у Alice. Или может случиться так, что у Alice будет вычтена эта сумма, но Bob её не получит. Нам нужна гарантия, что если что-либо пойдет не так во время операций обновления, счетов, то никаких изменений фактически внесено не будет. Такую гарантию можно получить, если сгруппировать операторы update в транзакцию. Транзакция является атомарным действием с точки зрения других транзакций и либо она завершится полностью успешно, либо никакие действия, составляющие транзакцию выполнены не будет.

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

Другое важное свойство транзакционных СУБД состоит в строгой изоляции транзакций: когда несколько транзакций запускаются конкурентно, каждая из них не видит тех неполных изменений, которые производят другие транзакции. Например, если одна транзакция занята сложением всех балансов филиалов, она не должна учитывать как денег снятых со счета Alice так и денег пришедших на счет Bob. Таким образом транзакции должны выполнять принцип все-или-ничего не только в плане нерушимости тех изменений, которые они производят в базе данных, но и также в плане того, что они видят в момент работы. Обновления, которые вносит открытая транзакция являются невидимыми для других транзакций пока данная транзакция не завершиться, после чего все внесенные ей изменения станут видимыми.

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

BEGIN;
UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
-- и т.д. ....
COMMIT;

Если во время выполнения транзакции мы решаем, что не хотим завершать её (например мы получили извещение о том, что счет Alice отрицательный), то мы вместо команды COMMIT выдаем команду ROLLBACK и все наши изменения от начала транзакции, будут отменены.

PostgreSQL фактически считает каждый оператор SQL запущенным в транзакции. Если вы не указываете команду BEGIN, то каждый отдельный оператор имеет неявную команду BEGIN перед оператором и (при успешной отработке оператора) команду COMMIT после оператора. Группа операторов заключаемая в блок между BEGIN и COMMIT иногда называется транзакционным блоком.

Note: Некоторые клиентские библиотеки выполняют команды BEGIN и COMMIT автоматически, так что вы можете без вопросов организовывать транзакционные блоки. Проверьте документацию по тому интерфейсу, который вы используете.

Возможно управлять операторами в транзакции и на более детализированном уровне с помощью "точек сохранения" (savepoints). Точки сохранения позволяют выборочно отбрасывать части транзакции, в то же время выполняя остаток транзакции. После того как вы зададите точку сохранения с помощью оператора SAVEPOINT, вы можете, если понадобится, откатить транзакцию до этой точки сохранения с помощью оператора ROLLBACK TO. Все изменения базы данных внутри транзакции между точкой сохранения и местом откуда вызван откат теряются, но изменения, которые были сделаны до точки сохранения остаются.

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

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

Возращаясь к базе данных банка, предположим, что мы списали $100 со счёта Alice и положили их на счёт Bob, только для того, чтобы потом обнаружить, что мы должны были положить их на счёт Wally. Мы могли бы сделать это, используя точки сохранения как здесь:

BEGIN;
UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Bob';
-- ой! ... забудем это и используем счёт Wally
ROLLBACK TO my_savepoint;
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Wally';
COMMIT;

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

Back to top

(С) Виктор Вислобоков, 2008-2023