Лекция 1 1. Транзакции Транзакции служат для поддержания базы данных (БД) в целостном состоянии. Таковым называют состояние, соответствующее установленным для БД бизнес-правилам. На практике редко удается добиться того, чтобы целостное состояние сохранялось после каждой операции с данными. Типичный пример – перевод денег в банке с одного счета на другой. Деньги снимаются со счета-источника, а затем зачисляются на счет-приемник. Между снятием денег и их зачислением состояние БД не может считаться целостным, так как в этот момент нарушен баланс счетов. Когда же деньги будут зачислены на второй счет, баланс восстановится, и БД вновь перейдет в целостное состояние. 1.1. Концепция транзакций Понятие транзакции пронизывает всю теорию и практику работы с базами данных. Транзакция – это логический блок, объединяющий одну или более операций в БД, который должен быть целиком выполнен или целиком не выполнен. Не допускается окончательное выполнение никакой отдельной части этого блока. Важным свойством транзакции, обеспечиваемым СУБД, является возможность ее подтверждения (COMMIT) или отмены (ROLLBACK). Если часть операций блока завершена, но при выполнении еще одной произошла некоторая ошибка, то транзакция отменяется. При успешном выполнении всего блока – обычно подтверждается. В предыдущем примере, когда клиент инициирует перевод денег, начинается транзакция. Деньги снимаются со счета-источника, после чего помещаются на счет-приемник. Когда приходит подтверждение, что деньги переведены, транзакция завершается, то есть 2 именно в этот момент происходит «узаконивание» перевода денег. Если хотя бы один этап перевода не состоится, транзакция откатывается и все проведенные в ее рамках изменения отменяются. Операции, о которых говорится в определении транзакции – это SQL-команды манипулирования данными: SELECT или INSERT, UPDATE, DELETE. Говорят, что действия выполняются в контексте данной транзакции. Приведенное определение транзакции популярно среди разработчиков прикладных БД. Оно позволяет объяснить большинство ситуаций, возникающих в приложениях баз данных, и в то же время является достаточно простым для интуитивного понимания. Однако оно не охватывает некоторых важных особенностей транзакций, которые могут проявиться при работе с БД. Поэтому рассмотрим еще понятие механизма транзакций. Механизм транзакций – это встроенная в СУБД функциональность, позволяющая объединять различные действия в логические блоки (транзакции) и обеспечивающая возможность принятия решения об успешности действий блока операций в целом. Если механизм транзакций реализован в некоторой СУБД, то он работает для любых операций с данными. Несмотря на то, что некоторые высокоуровневые системы программирования берут на себя работу по управлению транзакциями, скрывая ее от программиста, тем не менее, транзакции запускаются и затем подтверждаются либо отменяются. Объединением некоторой последовательности операций в транзакцию обычно управляет клиентское приложение БД, с которым работает пользователь. Поэтому решение о подтверждении транзакции принимается на основе логики клиентского приложения и, следовательно, зависит от пользователя. Таким образом, подтверждение или отмена транзакции не всегда означает, что входящие в нее операции выполнились успешно или закончились ошибкой. Это решение клиентского приложения о том, следует ли оставить в БД результаты работы всех операций, образующих 3 транзакцию. Отмену транзакции наряду с клиентом может произвести и сама система (СУБД), если дальнейшее корректное выполнение транзакции окажется невозможным (произошло деление на 0 и тому подобное). При этом обычно генерируется определенный код ошибки или исключения, который доступен клиентской программе для обработки. Рассматривая пример с переводом денег, можно представить транзакцию как некоторый черный ящик, в котором производятся действия над содержимым БД. В этот ящик пользователю нельзя заглянуть до того как транзакция завершится подтверждением или откатом. Если бы была возможность посмотреть «внутрь» транзакции, в контексте которой осуществляется перевод денег, то можно было бы увидеть ситуацию, когда деньги уже ушли с одного счета, но на другой не пришли, или, наоборот, пришли, но «размножились» и существуют сразу на обоих счетах. Другими словами, внутри транзакции база данных в определенные моменты может находиться в нецелостном (логически неправильном) состоянии. Целостное состояние БД – это состояние, в котором БД содержит информацию, корректную с точки зрения правил бизнес-логики, применяющихся в данной модели предметной области. Отсюда следует еще одно определение механизма транзакции. Механизм транзакций – это функциональность, позволяющая переводить базу данных из одного целостного состояния в другое. Во время работы транзакции результаты выполняющихся в ее контексте операций невидимы извне до момента ее подтверждения (COMMIT). Поскольку любые действия с данными выполняются только в контексте транзакций, то результаты операций, выполняющихся в контексте одной транзакции, невидимы (или недоступны) для операций, осуществляющихся в это же время в контексте других транзакций. Сформулируем, наконец, наиболее общее определение. Механизм транзакций – это возможности СУБД, позволяющие объединять отдельные операции с данными в логические блоки и обеспечивать принятие решения 4 об успешности действий блока операций в целом. Логические блоки операций осуществляют перевод базы данных из одного целостного состояния (соответствующего бизнес-правилам задачи) в другое целостное состояние. Механизм транзакций служит для обеспечения изоляции изменений, совершаемых операциями в контексте одной транзакции, от операций в других транзакциях. Это определение применимо для всех реляционных СУБД. 1.2. Свойства транзакций и уровни изоляции Таким образом, идеальная транзакция обладает четырьмя важными свойствами: атомарностью, согласованностью, изолированностью и устойчивостью (Atomicity, Consistency, Isolation, Durability – ACID). Атомарность означает, что транзакция не делится на части, то есть в конечном счете выполняется целиком или не выполняется вовсе. Согласованность – это способность транзакции переводить БД из одного согласованного (целостного) состояния в другое (имеется в виду сохранение ограничений, определенных в БД). При этом речь идет лишь о моментах до начала выполнения транзакции и после него. В промежуточные моменты целостность данных не гарантируется. Изолированность – это независимость различных транзакций друг от друга. Каждая транзакция выполняется так, как будто других транзакций при ее выполнении не существует. Наконец, устойчивость означает надежное сохранение результатов выполнения подтвержденной транзакции, включая возможное их восстановление после программных ошибок и аппаратных сбоев. Как сказано выше, транзакция должна обеспечивать изолирование проводящихся в ее контексте изменений, чтобы эти изменения были невидимыми для других пользователей вплоть до подтверждения транзакции. Возникает вопрос: должна ли данная транзакция видеть изменения, которые были подтверждены другими транзакциями после ее запуска? Пусть есть таблица 5 БД, к которой обращаются два пользователя одновременно – один из них изменяет данные, а второй только читает. Должен ли (или может ли) пользователь, читающий таблицу, видеть изменения, производимые другим пользователем? Это взаимодействие определяется уровнями изоляции транзакций. Дело в том, что на практике из-за ограниченности программно-аппаратных ресурсов реализовать полную изолированность транзакций не всегда возможно. Например, число одновременно выполняемых транзакций, работающих с общими данными, обычно ограничивается. Поэтому для каждой транзакции предусматривается уровень изоляции. Он выбирается как компромисс между снижением производительности и допустимым нарушением изолированности. Он определяет, какие изменения, сделанные в других транзакциях, будут видны в данной транзакции. Уровень изоляции при старте каждой транзакции в реляционной БД устанавливается клиентской программой и остается неизменным в течение всей жизни транзакции. В стандарте ANSI SQL-92 возможны 4 уровня изолированности транзакций: Dirty Read – «грязное» (или «незафиксирование») чтение. Транзакция может читать неподтвержденные изменения, сделанные в других транзакциях. Например, если транзакции A и B стартовали и поменяли записи, то они обе видят изменения друг друга. Если при этом A использует данные B, а B будет отменена, то могут возникнуть серьезные проблемы. Read Committed – невоспроизводимое (или неповторяемое) чтение. Транзакция может читать только те изменения, которые были подтверждены другими транзакциями. Например, если транзакции A и B стартовали и поменяли записи, то они не видят изменения друг друга. Транзакция A увидит изменения транзакции B только тогда, когда транзакция B завершится с подтверждением. Перечитывание данных в транзакции может выдавать разные результаты. 6 Repeatable Read – воспроизводимое (или повторяемое) чтение. Транзакция видит те данные, которые существовали на момент ее старта, и их значения не меняются при повторном чтении. Однако при повторении запроса транзакция может увидеть новые («фантомные») кортежи данных, добавленные другими завершенными транзакциями. Serialized – сериализуемость. Транзакция выполняется так, как будто никаких других транзакций во время ее выполнения не существует. Другими словами, транзакции выполняются так, как будто они выполняются последовательно. Существуют две основные проблемы, осложняющие и функционирование механизма транзакций: системные отказы (сбои) и параллельная работа нескольких пользователей с БД. Несмотря на эти факторы, транзакции должны сохранять свои основные свойства и корректно работать. 2. Профилактика системных отказов и устранение их последствий Типичными причинами сбоев служат потеря электропитания (при отсутствии ИБП) или ошибки в программном обеспечении. Транзакции, как и любые программы, выполняются в виде последовательности определенных операций. Часто некоторые из этих операций связаны с изменением данных. Каждая транзакция характеризуется состоянием, представляющим текущую позицию внутри кода транзакции и значения всех ее локальных переменных, которые могут быть востребованы позднее. Сбои системы могут приводить к утрате информации о состоянии транзакции. В таких случаях система не в силах контролировать, какая часть транзакции была выполнена, а какая – нет. Повторное выполнение транзакции далеко не всегда решает проблему. Основная стратегия преодоления последствий системных сбоев – вести надежный протокол всех изменений в базе данных, используя отдельный файл на энергонезависимом устройстве, который при необходимости может быть использован для восстановления информации. Восстановление 7 целостности означает приведение БД к такому виду, когда незавершенные до сбоя транзакции отменены, завершенные – подтверждены. Существуют также методики ведения архивов, позволяющие восстановить содержимое базы данных не только после кратковременных сбоев системы, но и в случае полной утраты информации: на основе наиболее свежего архива и уцелевшей информации протокола удается реконструировать базу данных в состоянии, в котором она находилась в некоторый момент времени, предшествовавший катастрофическим событиям. Заметим, что упрощенной и «легальной» разновидностью сбоев можно считать операцию отката транзакции ROLLBACK. Дело в том, что процесс ее выполнения хорошо вписывается в общую идеологию восстановления БД, хотя и содержит сравнительно меньше операций, так как все они связаны только с одной транзакцией. Действия же по выполнению операции COMMIT содержатся среди других действий СУБД по обеспечению возможностей восстановления. 2.1. Протоколирование Протокол, или журнал, можно представить себе как файл, открытый только для записи. По мере осуществления операций транзакций менеджер протоколирования (специальная подсистема СУБД) сохраняет в журнале информацию о каждом существенном событии. Блоки протокола заполняются записями последовательно; каждая из записей соответствует одному событию. Блок протокола изначально создается в оперативной памяти и поддерживается менеджером буферов тем же образом, как и все другие блоки, используемые в процессе функционирования СУБД. Сохранение блоков протокола на носителе происходит в подходящие моменты времени. Непосредственное сохранение на диске каждой записи протокола существенно снижало бы производительность системы. В протоколе могут чередоваться описания операций нескольких транзакций. 8 Один из важных аспектов протоколирования и восстановления состоит в сокращении объема актуальных данных журнала за счет применения аппарата контрольных точек. В случае отказа системы протокол применяется для воссоздания картины действий транзакции в период, предшествовавший аварии. Для устранения последствий сбоя некоторые транзакции будут выполнены повторно, и изменения, произведенные ими и ранее сохраненные в базе данных, зафиксируются вновь. Действия других транзакций будут отменены. Заметим, что операции СУБД по восстановлению данных также выполняются с протоколированием, поскольку системный сбой может произойти и в процессе восстановления данных. Различают три модели ведения протокола, кратко называемые "undo" (отмена), "redo" (повторение) и "undo/redo" (отмена/повторение). Однако существует несколько видов записей журнала, общих для всех трех режимов протоколирования. <START T> – выполнение транзакции T начато. <COMMIT T> – транзакция T успешно завершена. Результаты всех операций транзакции T должны быть зафиксированы на диске. Поскольку решение о копировании блоков памяти на диск в общем случае принимает менеджер буферов, наличие в протоколе этой записи еще не служит гарантией того, что изменения действительно зафиксированы на диске. <ABORT T> – выполнение транзакции преждевременно остановлено. В подобном случае никакие изменения данных, инициированные транзакцией T, на диске не отображаются; если же в результате работы транзакции копия базы данных на диске уже была модифицирована, эти результаты аннулируются – подобные обязанности возлагаются на менеджер транзакций. 9 2.1.1. Протоколирование в режиме "undo" Этот способ протоколирования ориентирован на отмену незавершенных до сбоя транзакций. Помимо записей перечисленных выше типов, протокол "undo" содержит записи обновления, представляющие собой тройки вида <T, X, v>. Смысл подобной записи таков: транзакция T изменила прежнее содержимое v элемента базы данных X. Сюда мы относим не только изменение значений полей, но также вставку и удаление записей. Изменение БД, представленное в журнале записью обновления, первоначально обычно отображается в памяти, а не на диске. Заметим, что в протоколе "undo" сохраняется только старое значение элемента. В случае необходимости реставрации данных с помощью протокола "undo" менеджер восстановления сможет воссоздать прежние значения элементов, пользуясь записями данного типа. Существуют два правила, которым должны следовать подсистемы СУБД с целью обеспечения возможности восстановления данных с помощью протокола "undo". 1. Если транзакция T модифицирует элемент базы данных X, запись каждого обновления вида <T, X, v> должна быть занесена в протокол на диске до сохранения соответствующего нового значения элемента X. 2. При фиксации результатов транзакции T запись <COMMIT T> следует помещать в протокол на диске только после "сбрасывания" на диск всех измененных этой транзакцией значений элементов БД, причем интервал между этими дисковыми операциями должен быть максимально коротким. Транзакция считается завершенной только после появления этой записи. Уточним, что первое правило касается каждого отдельного элемента БД, а второе – всех элементов, подвергшихся изменению в процессе выполнения данной транзакции. Чтобы форсировать сохранение записей протокола на диске (для выполнения правил 1–2), менеджер протоколирования использует команду 10 «сброса протокола», заставляющую менеджер буферов копировать на диск содержимое всех блоков журнала, которые прежде еще не сохранялись. Менеджер транзакций, в свою очередь, должен иметь возможность «принуждения» менеджера буферов к выполнению операции сохранения на диске значения элемента X. 2.1.2. Восстановление с применением протокола "undo" Предположим, что во время работы системы, поддерживающей режим протоколирования "undo", произошел сбой. Возможна ситуация, когда некоторые изменения, внесенные в содержимое БД определенной транзакцией, оказались записанными на диске, в то время как другие результаты той же транзакции попасть на диск не успели. В подобном случае нарушается принцип атомарности транзакции. Ответственность за воссоздание согласованного состояния базы данных с использованием информации протокола возлагается на менеджер восстановления. В этом разделе рассматривается простейший вариант восстановления, предполагающий просмотр всего файла протокола. Позднее рассмотрим более рациональный подход, основанный на периодическом введении контрольных точек с целью уменьшения размера журнала и сокращения промежутка воспроизводимой истории выполнения операций. Первая задача менеджера восстановления – распределить все упомянутые в протоколе транзакции по двум категориям – зафиксированные и незафиксированные. Если в журнале существует запись <СОММIТ T>, на основании правила 2 можно утверждать, что все изменения, внесенные транзакцией T, сохранены на диске и к моменту возникновения отказа транзакция T сама по себе не могла оставить базу данных в несогласованном состоянии. Допустим теперь, что в протоколе содержится запись <START T>, но отсутствует соответствующая запись <СОММIТ T>. Тогда возможна 11 ситуация, когда часть изменений, внесенных транзакцией T, зафиксирована на диске до момента отказа, в то время как другие операции либо не были выполнены вовсе, либо их результаты остались только в буферах оперативной памяти, но не на диске. В этом случае транзакция T расценивается как незавершенная и подлежит отмене. Правило 1 гарантирует, что если транзакция T успела изменить элемент X в БД на диске до момента сбоя, то в журнале имеется запись вида <T, X, v>. Поэтому восстановление состояния элемента X сводится к присваиванию ему сохраненного в записи протокола значения v (в расширенном смысле, включая удаление или вставку записи). Поскольку протокол может содержать информацию о нескольких незафиксированных транзакциях, а также о таких, которые изменяли значение одного и того же элемента X, то воссоздание состояний элементов данных следует проводить в строгом порядке, начиная с конца (то есть с наиболее свежих записей) и перемещаясь к началу. По мере просмотра данных протокола менеджер запоминает, какие транзакции были отмечены записями <COMMIT>, а какие – <ABORT>. Встречая запись вида <T, X, v>, он выполняет следующее: если ранее была запись <COMMIT T>, то никакие действия не предпринимаются, поскольку транзакция зафиксирована, и ее результаты отмене не подлежат; в противном случае транзакция T трактуется как незавершенная; это значит, что содержимое X было изменено непосредственно перед отказом системы и элементу X необходимо присвоить значение v. Завершив просмотр журнала, менеджер восстановления обязан записать в него записи вида <ABORT T> для всех незавершенных транзакций, которые не были отмечены подобными записями прежде, а затем инициировать команду «сброса» протокола на диск. Теперь система готова к возобновлению нормального функционирования базы данных и к выполнению новых транзакций.