Транзакции
Транзакция – это законченная совокупность действий над БД, которая переводит ее из одного целостного состояния в другое.
То есть транзакция представляет собой набор взаимосвязанных изменений базы данных, которые либо завершаются полностью, либо отменяются, не оставляя в базе данных никаких изменений. Используются также термины физическая транзакция и коммит юнит (commit unit). В самом общем случае, OpenEdge гарантирует, что, когда вы присваиваете значения нескольким полям в таблице базы данных, будут сохранены все эти значения либо никакие из них.
Но во многих случаях необходимо изменить несколько связанных записей в базе данных, с гарантией, что все изменения выполнены вместе. Например, Вы изменяете Order, Order Lines и Customer (Balance) в одной транзакции. При этом другие пользователи параллельно изменяют эту же базу данных – и требуют тех же гарантий. Механизм транзакций OpenEdge, вместе с механизмом блокирования записей и встроенным механизмом обеспечения целостности всегда обеспечивают такие гарантии.
Приложение определяет область действия каждой транзакции, изменяющей базу данных, так что Вы имеете полный контроль над изменениями.
Транзакционные блоки
Область действия транзакций в Progress привязана к блокам. Область действия транзакций в большинстве случаев определяется аналогично области видимости записей и тесно с ней связана.
Некоторые блоки в ABL являются транзакционными, некоторые – нет, согласно следующим правилам:
- Вы можете явно включить указание TRANSACTION в заголовке блока FOR EACH, REPEAT или DO, с дополнительной опцией обработки ошибок ON ERROR. Любой блок, использующий указание TRANSACTION становится транзакционным.
- Любой блок, который непосредственно изменяет базу данных или непосредственно читает записи с блокировкой EXCLUSIVE-LOCK, становится транзакционным. Это может быть процедурный блок, триггерный блок или каждая итерация блоков DO (с опцией ON ERROR), FOR EACH или REPEAT.
Непосредственное изменение базы данных – это, например, операторы CREATE, DELETE или ASSIGN. Блок считается читающим записи с EXCLUSIVE-LOCK, если, по крайней мере, один оператор FIND или FOR EACH с такой блокировкой имеется в данном блоке.
Блок DO без фразы ON-ERROR не является автоматически транзакционным, даже если он блокирует и / или изменяет данные.
Процедура, содержащая транзакцию, может быть вызвана из другой процедуры, которая уже начала транзакцию. В этом случае вызываемая процедура становится частью большей транзакции в вызывающей процедуре. После начала транзакции, все изменения в базе данных являются частью транзакции, пока она не закончится. Каждая пользовательская сессия может иметь только одну активную транзакцию в данный момент времени. Если один транзакционный блок вложен в другой транзакционный блок, внутренний блок образует субтранзакцию (subtransaction).
Управление размером транзакций.
Блоки, которые автоматически начинают транзакцию:
- Блок FOR EACH, непосредственно изменяющий базу данных.
- Блок REPEAT, непосредственно изменяющий базу данных.
- Процедурный блок, непосредственно изменяющий базу данных.
- Блок DO с опциями ON ERROR или ON ENDKEY, содержащий операторы, изменяющие базу данных.
Вы можете управлять размером транзакции, используя указание TRANSACTION в блоках DO, FOR EACH или REPEAT. Это позволяет увеличить размер транзакции, но из-за операторов, начинающих транзакцию автоматически, Вы не можете сделать транзакцию меньше.
Рассмотрим простой условный пример (Программа 12). Здесь имеется блок DO TRANSACTION, охватывающий весь процесс изменения Order и OrderLine. Операторы FIND и UPDATE здесь использованы только для примера, чтобы в блоке DO реально было изменение базы данных, и чтобы сделать блок FOR EACH транзакционным автоматически. Если происходит любая ошибка при любом изменении, откатывается вся транзакция. Листинг компиляции данного примера приведен на Рис. 10. Среди прочих вещей, листинг содержит информацию о начале и конце транзакций. Эта информация очень важна. Вы должны всегда анализировать листинг сложных процедур, чтобы убедиться, что области видимости записей и области действия транзакций соответствуют Вашим ожиданиям.
Программа 12. Размер транзакций
DO TRANSACTION ON ERROR UNDO, LEAVE: DO: /* Order update block */ FIND FIRST Order. UPDATE Order. END. FOR EACH OrderLine: /* OrderLine update block */ UPDATE OrderLine. END. END. /* END Transaction block. */
.\trans1.p 12/14/2011 16:03:57 PROGRESS(R) Page 1 {} Line Blk -- ---- --- 1 1 DO TRANSACTION ON ERROR UNDO, LEAVE: 2 2 DO: 3 2 /* Order update block */ 4 2 FIND FIRST Order. 5 2 UPDATE Order. 6 1 END. 7 2 FOR EACH OrderLine: 8 2 /* OrderLine update block */ 9 2 UPDATE OrderLine. 10 1 END. 11 END. /* END Transaction block. */ 12 .\trans1.p 12/14/2011 16:03:57 PROGRESS(R) Page 2 File Name Line Blk. Type Tran Blk. Label -------------------- ---- ----------- ---- -------------------------------- .\trans1.p 0 Procedure No Buffers: sports2000.Order Frames: Unnamed .\trans1.p 1 Do Yes .\trans1.p 2 Do No .\trans1.p 7 For Yes Buffers: sports2000.OrderLine Frames: Unnamed
Рис. 10. Размер транзакций – Листинг компиляции
Из листинга видно, что блок DO TRANSACTION имеет уровень 1. DO-блок внутри него, где в действительности должно выполняться обновление Order, имеет уровень 2, так же, как и блок FOR EACH, обновляющий OrderLine.
В конце листинга приведена сводная информация по всем блокам.
Во-первых, видно, что процедура в целом не является транзакционным блоком. Никогда не допускайте, чтобы размер транзакции расширялся до размера процедуры. Это означает, что блокировки записей сохраняются дольше, чем необходимо. Возможно также, что в транзакцию оказываются вовлечены «лишние» записи.
Во-вторых, блок DO в строке 1 является транзакционным, как и ожидалось – в этом блоке транзакция указана явно. Вложенный блок DO не является транзакционным. Но следующий блок FOR EACH транзакционный. Так как он вложен в транзакционный блок, он представляет собой субтранзакцию. Progress может откатить изменения, сделанные в субтранзакции в рамках большей транзакции, или Вы можете сделать это сами (смотри ниже).
Увеличение размера транзакции.
Если в примере (Программа 12) закомментировать DO TRANSACTION и соответствующий END – Вы увидите, что теперь вся процедура стала транзакционным блоком. Листинг приведен на Рис. 11.
.\trans2.p 12/14/2011 16:14:34 PROGRESS(R) Page 1 {} Line Blk -- ---- --- 1 /* DO TRANSACTION ON ERROR UNDO, LEAVE: */ 2 1 DO: 3 1 /* Order update block */ 4 1 FIND FIRST Order. 5 1 UPDATE Order. 6 END. 7 1 FOR EACH OrderLine: 8 1 /* OrderLine update block */ 9 1 UPDATE orderLine. 10 END. 11 12 /* END. */ /* END Transaction block. */ 13 .\trans2.p 12/14/2011 16:14:34 PROGRESS(R) Page 2 File Name Line Blk. Type Tran Blk. Label -------------------- ---- ----------- ---- -------------------------------- .\trans2.p 0 Procedure Yes Buffers: sports2000.Order Frames: Unnamed .\trans2.p 2 Do No .\trans2.p 7 For Yes Buffers: sports2000.OrderLine Frames: Unnamed
Рис. 11. Размер транзакций – Листинг компиляции
Почему это происходит? Блок DO сам по себе, без указания TRANSACTION или ON ERROR, не начинает транзакцию. Поэтому Progress расширяет транзакцию до размеров процедуры. В данном случае это не имеет большого значения, так как тривиальная процедура в примере больше ничего и не делает. Но, как подчеркивалось ранее, это очень опасная практика, и Вы должны ее избегать. Если Вы анализируете листинг Вашей процедуры и видите, что вся процедура является транзакционным блоком, Вы должны указать транзакционный блок явно. Фактически, Вы всегда должны указывать транзакционные блоки явно, и затем проверять, что операторы вне этих блоков не приводят к расширению транзакции.
Уменьшение размера транзакций.
Выполним еще один эксперимент. Перенесем закрывающий транзакционный блок оператор END на строчку перед оператором FOR EACH (Программа 13). Как это повлияет на размер транзакций? Листинг компиляции этого примера приведен на Рис. 12.
Программа 13. Размер транзакций
DO TRANSACTION ON ERROR UNDO, LEAVE: DO: /* Order update block */ FIND FIRST Order EXCLUSIVE-LOCK. UPDATE Order. END. END. /* END Transaction block. */ FOR EACH OrderLine: /* OrderLine update block */ UPDATE orderLine. END.
.\trans3.p 12/14/2011 17:18:06 PROGRESS(R) Page 1 {} Line Blk -- ---- --- 1 1 DO TRANSACTION ON ERROR UNDO, LEAVE: 2 2 DO: 3 2 /* Order update block */ 4 2 FIND FIRST Order EXCLUSIVE-LOCK. 5 2 UPDATE Order. 6 1 END. 7 END. /* END Transaction block. */ 8 1 FOR EACH OrderLine: 9 1 /* OrderLine update block */ 10 1 UPDATE orderLine. 11 END. 12 .\trans3.p 12/14/2011 17:18:06 PROGRESS(R) Page 2 File Name Line Blk. Type Tran Blk. Label -------------------- ---- ----------- ---- -------------------------------- .\trans3.p 0 Procedure No Buffers: sports2000.Order Frames: Unnamed .\trans3.p 1 Do Yes .\trans3.p 2 Do No .\trans3.p 8 For Yes Buffers: sports2000.OrderLine Frames: Unnamed
Рис. 12. Размер транзакций – Листинг компиляции
Так как теперь блок FOR EACH вынесен за пределы большей транзакции, он становится самостоятельным транзакционным блоком уровня 1, как видно из листинга. Это означает, что два имеющихся в процедуре транзакционных блока теперь представляют собой отдельные и различные транзакции. Если произойдет ошибка при изменении Order, произойдет откат этой транзакции, выход из блока, и обработка продолжится для OrderLine. Если изменение OrderLine закончится успешно, то модифицированные OrderLine будут сохранены в базе данных, но Order останется без изменений из-за ошибки. Аналогично, если Order успешно обновится, но при обработке OrderLine возникнет ошибка, в базе данных будет измененный Order, но OrderLine останутся без изменений. Вам необходимо решить, какого размера транзакции Вам нужны, чтобы обеспечить целостность данных. В общем, необходимо стремиться иметь минимальный размер транзакций, так что не будет заблокировано больше записей (или на более долгий срок), чем это абсолютно необходимо. Но Ваши транзакции должны быть достаточно большими, чтобы включать все связанные изменения, которые должны быть выполнены (или откачены) вместе.
Транзакции и процедурные и триггерные блоки.
Если Ваш код начинает транзакцию в одной процедуре и затем вызывает другую процедуру, внешнюю или внутреннюю, вся вызванная процедура включается в транзакцию, которая началась до ее вызова.
Так как триггеры базы данных – это внешние процедуры, вызываемые при специальных условиях (в ответ на событие изменения данных где-либо в приложении) – на них распространяется это же правило. Триггеры базы данных всегда вызываются из активной транзакции (за исключением триггера FIND) – так что они полностью содержатся в большей транзакции, которая привела к их вызову.
Триггерные блоки определенные фразой ON event рассматриваются ABL как вызовы внутренних процедур. Если в момент исполнения такого блока (в ответ на событие) имелась активная транзакция, код триггерного блока включается в эту транзакцию.
Проверка активности транзакции.
Вы можете использовать встроенную функцию TRANSACTION, чтобы определить, активна ли в данный момент транзакция. Это может быть использовано, например, в процедуре, вызываемой из многих мест в приложении, если она должна реагировать по-разному, в зависимости от того, начала вызывающая процедура транзакцию, или нет.
Опция NO-UNDO для переменных и временных таблиц.
Общим правилом является описывать почти все переменные процедуры с опцией NO-UNDO. Это же правило применимо и к описанию временных таблиц (temp-tables).
Когда Вы определяете переменные, ABL выделяет для них память в структуре, аналогичной буферу записи. Фактически, создается два таких «буфера» – один для переменных, значения которых могут быть откачены при откате транзакции, и второй – для переменных, значения которых не могут быть откачены. Имеются дополнительные затраты на поддержку before-image для UNDO-переменных, кроме того, это действительно редко необходимо. Если Вы изменяете некоторую переменную внутри транзакционного блока (и для логики Вашего приложения важно, чтобы это изменение откатывалось при откате транзакции), тогда Вы должны определить эту переменную без опции NO-UNDO.
Тривиальный пример приведен в (Программа 14).
Программа 14. Откат значения переменной
DEFINE VARIABLE iCount AS INTEGER. REPEAT : CREATE Customer. iCount = iCount + 1. DISPLAY Customer.CustNum WITH FRAME CustFrame 5 DOWN. UPDATE Customer.Name WITH FRAME CustFrame. END. DISPLAY iCount "records created." WITH NO-LABELS.
Здесь блок REPEAT определяет транзакцию. Каждая итерация – отдельная транзакция, так что при успешном ее завершении запись Customer записывается в базу данных. Ввод записей продолжается до тех пор, пока пользователь не прервет исполнение цикла, например, клавишей Esc, которая в Windows сгенерирует состояние ERROR. На каждой итерации создается запись Customer, в триггере CREATE присваивается CustNum и предлагается ввести Name. В этот момент пользователь может нажать Esc. Произойдет откат текущей транзакции и выход из блока REPEAT, последняя созданная запись Customer будет удалена из базы данных. Что произойдет с переменной iCount? Ее значение будет откачено, и в конце программы будет показано правильно число созданных записей. Заметим, что значение поля CustNum, присвоенное в последней (прерванной) транзакции, в дальнейшем использовано не будет, так как оно определяется из sequence базы данных, а они исключены из транзакционной обработки. Если определить переменную iCount c опцией NO-UNDO, ее значение не будет восстановлено при откате последней транзакции и будет показано, что создано на одну запись больше, чем на самом деле.
Относительно немного переменных должны откатываться таким образом, так что для улучшения производительности следует определять все прочие переменные с опцией NO-UNDO.
Эти же принципы применимы и к временным таблицам. Так же как и переменные, временные таблицы более эффективны, если определены с опцией NO-UNDO. При определении временной таблицы Вы должны решить, должна ли она реально использоваться как часть транзакционной обработки. И если нет – укажите опцию NO-UNDO.
Использование оператора UNDO.
Progress откатывает транзакцию автоматически, если обнаруживается ошибка на уровне базы данных, например, нарушение уникальности ключа. Во многих случаях логика Вашего приложения может требовать отката транзакции, если обнаруживается нарушение правила бизнес-логики. Оператор UNDO позволяет Вам управлять прерыванием и откатом транзакции.
Синтаксис оператора:
UNDO [ label ]
[ , LEAVE [ label2 ] | , NEXT [ label2 ] | , RETRY [ label1 ] | ,
RETURN [ return-value | ERROR [ return-value | error-object-expression ] | NO-APPLY ]
| , THROW error-object-expression ]
В своей простейшей форме, просто UNDO, оператор выполнит откат самого внутреннего блока, имеющего свойство обработки ERROR:
- Блок FOR.
- Блок REPEAT.
- Блок процедуры.
- Блок DO с опцией TRANSACTION или ON ERROR.
Вы можете изменить это умолчание, указав метку блока, откат которого необходимо выполнить.
По умолчанию оператор UNDO после отката пытается повторить (RETRY) текущий блок. При вводе данных пользователем это позволяет пользователю ввести другое значение. При написании структурированных процедур, не связанных с интерфейсом пользователя, повторение блока смысла не имеет. Progress распознает такую ситуацию, и изменяет действие после отката на NEXT для итерационного блока и на LEAVE – для не итерационного. Это правило определяет умолчание для транзакций, не связанных с событиями пользовательского интерфейса. Вы можете изменить это умолчание. Если Вы указываете LEAVE, Вы можете указать имя блока, который Вы покидаете (по умолчанию – блок, который Вы откатываете). Если Вы указываете NEXT внутри итерационного блока, тогда будет выполнен переход к следующей итерации блока, который Вы указали (или который Вы откатили – по умолчанию). Опцию NEXT следует использовать, если каждая итерация блока представляет собой транзакцию, и если Вы хотите попытаться продолжить изменения для других записей. Если Вы указываете RETRY, Вы можете повторить текущий блок, или блок, который Вы откатили. Еще раз, в хорошо структурированном приложении нет необходимости в использовании RETRY. Наконец, вы можете использовать RETURN для выхода из текущей процедуры или триггерного блока. Вы также можете использовать структурную обработку ошибок (THROW), подробно описанную в соответствующей главе.
Субтранзакции.
Если Вы имеете транзакционный блок и несколько вложенных в него блоков, каждый из которых был бы транзакционным блоком, если бы использовался независимо, то внешний блок представляет собой транзакцию, а все вложенные транзакционные блоки становятся субтранзакциями. Это могут быть:
- Процедурный блок, вызванный из транзакционного блока в другой процедуре.
- Каждая итерация блока FOR EACH, вложенного в транзакционный блок.
- Каждая итерация блока REPEAT, вложенного в транзакционный блок.
- Каждая итерация блока DO TRANSACTION, DO ON ERROR, или DO ON ENDKEY, вложенного в транзакционный блок.
Если во время субтранзакции происходит ошибка, откатываются все изменения, выполненные с начала субтранзакции. Вы можете вкладывать субтранзакции в другие субтранзакции. Вы также можете программно откатывать субтранзакции с помощью оператора UNDO.
Замечание: Если происходит системная ошибка, например, из-за отключения питания, будет выполнен откат всей транзакции.
Механизм транзакций.
Во время выполнения транзакции информация о всей активности базы данных, связанной с этой транзакцией, записывается в файл before-image (BI), который связан с этой базой данных и находится на том же сервере, что и другие файлы базы данных. Информация, записываемая в файл before-image, координируется по времени с записью данных в базу данных. Так что, при возникновении ошибки во время транзакции, Progress использует файл before-image для восстановления базы данных в состояние, предшествующее началу транзакции. Информация, записываемая в файл before-image, не буферизуется. Она записывается на диск немедленно, так что в случае краха системы потеря информации минимальна.
Пространство в файле before-image выделяется единицами, называемыми кластерами. Progress автоматически выделяет новые кластеры при необходимости. После того, как все изменения, связанные с кластером, завершены и реально записаны в базу данных, Progress может использовать кластер повторно. Соответственно, пространство на диске, необходимое для файла before-image, зависит от нескольких факторов, включая размер кластера, размер транзакций и момент физической записи в файлы базы данных. Такие действия, как создание большого количества записей в пакетной процедуре в рамках одной транзакции, приводят к неконтролируемому росту файл before-image.
Когда Progress встречает транзакционный блок, вложенный в другой транзакционный блок, он начинает субтранзакцию. Вся активность базы данных, связанная с этой субтранзакцией записывается в local-before-image (LBI) файл. В отличие от файла BI базы данных, Progress поддерживает один файл LBI для каждого пользователя. Если во время субтранзакции происходит ошибка, Progress использует этот файл для отката базы данных к состоянию до начала этой субтранзакции. В любом случае, когда полный откат транзакции не выполняется, Progress использует для отката этот файл. Этот же файл используется для отката значений переменных, не описанных как NO-UNDO. Так как файл LBI не используется для восстановления при крахе системы, не требуется его запись на диск так же аккуратно и синхронно, как файл BI. Это минимизирует затраты на поддержку субтранзакций. Запись в файл LBI выполняется с помощью обычных, буферизованных операций ввода-вывода. Размер дискового пространства, необходимый для файла LBI зависит от числа и размера субтранзакций.
Состояние STOP и транзакции.
Состояние STOP возникает, когда OpenEdge обнаруживает невосстановимую системную ошибку, такую как потеря связи с базой данных или невозможность найти внешнюю процедуру для выполнения. ABL включает в себя оператор STOP, который позволяет установить состояние STOP программно. Состояние STOP может быть также установлено с клавиатуры, с помощью STOP-key (Ctrl-Break для Windows и Ctrl-C для UNIX).
При возникновении состояния STOP OpenEdge откатывает активную транзакцию полностью и перезапускает стартовую процедуру приложения.
Обработка состояния STOP возможна (с некоторыми ограничениями – смотри ниже) с помощью фразы ON STOP в заголовке блока.
Если теряется доступ к базе данных, клиент может продолжить работу. В этом случае происходит следующее:
- Устанавливается состояние STOP. Для этого специального случая Progress игнорирует любые фразы ON STOP.
- Удаляются все persistent процедуры, которые имеют ссылки на отключившуюся базу данных.
- Откатываются все активные исполняющиеся блоки, начиная с самого внутреннего, до уровня, не содержащего ссылок на отключившуюся базу данных.
- Устанавливается «нормальное» состояние STOP. С этого момента Progress вновь может обрабатывать фразы ON STOP. Продолжается откат блоков, пока не встретится фраза ON STOP. Если фраза ON STOP не встретилась, откатываются все активные блоки и перезапускается стартовая процедура.
Состояние QUIT и транзакции.
ABL включает оператор QUIT, предназначенный для полного прекращения работы приложения. При выполнении этого оператора устанавливается состояние QUIT. Обработка по умолчанию для него отличается от обработки состояния STOP следующим:
- OpenEdge завершает, а не откатывает текущую транзакцию.
- Даже если указана стартовая процедура приложения, происходит выход в операционную систему.
Состояние Quit также может быть обработано с помощью фразы ON QUIT.
Системный крах.
Если происходит системная аппаратная или программная ошибка, после которой восстановление невозможно, OpenEdge откатывает все незаконченные транзакции для всех пользователей. Это включает любую работу, выполненную в законченных или незаконченных субтранзакциях, входящих в незавершенные транзакции.