Delphi-Help

Главная Статьи FireBird/Interbase Работа с транзакциями и их использование в FIBPlus. Часть 2

Работа с транзакциями и их использование в FIBPlus. Часть 2

Оцените материал
(0 голосов)


Работа с транзакциями и их использование в FIBPlus. Часть 2

Характеристики транзакции

Все действия, выполняемые с базой данных — любые изменения как данных, так и метаданных, а также любая выборка данных — осуществляются в контексте какой-либо транзакции. Все изменения, выполненные в контексте транзакции, можно либо подтвердить (при отсутствии ошибок базы данных), либо все отменить. Если в любой операции, выполняемой в контексте транзакции, произошла ошибка, то подтвердить такую транзакцию нельзя. Можно только отменить все действия.

При использовании операторов языка SQL для работы с базой данных для запуска транзакции и задания ее характеристик используется оператор SET TRANSACTION. В API серверов InterBase/Firebird используется функция isc_start_transaction() и буфер параметров транзакции TPB.

Для подтверждения транзакции используется оператор SQL COMMIT или эквивалентная функция API isc_commit_transaction(), для отмены всех действий транзакции используется оператор ROLLBACK или функция API isc_rollback_transaction().

В компонентах FIBPlus обращения к серверу базы данных осуществляются с помощью соответствующих функций API. Для запуска транзакции с заданными характеристиками проблемный программист должен сформировать характеристики транзакции одним из возможных способов и обратиться к методу StartTransaction компонента транзакции или установить в True значение свойства Active.

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

Существуют еще две функции API и, соответственно, два метода в FIBPlus у компонента транзакции: подтверждение транзакции с сохранением контекста (функция isc_commit_retaining(), метод CommitRetaining) и откат транзакции с сохранением контекста (функция isc_rollback_retaining(), метод RollbackRetaining). Эти средства позволяют выполнить «мягкое» подтверждение/откат. Они позволяют упростить жизнь программисту. При обычном завершении транзакции программисту следовало бы запомнить текущую запись (обычно запоминается значение первичного ключа), заново запустить транзакцию, открыть набор данных и перейти к нужной записи. При сохранении контекста ничего этого делать не надо. Недостатком является увеличение использования ресурсов сервера. Кроме того, при использовании уровня изоляции SNAPSHOT после мягкого завершения транзакции она не будет видеть изменения, выполненные другими процессами.

В InterBase/Firebird и FIBPlus также поддерживаются замечательные средства создания контрольных точек сохранения транзакции и отката на любую из существующих точек. Эти средства мы рассмотрим несколько позже.

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

Вот несколько упрощенный синтаксис оператора SET TRANSACTION:

SET TRANSACTION
[READ WRITE | READ ONLY] /* режим доступа */
[WAIT | NO WAIT] /* режим разрешения блокировок */
[[ISOLATION LEVEL] /* уровень изоляции */
{SNAPSHOT |
SNAPSHOT TABLE STABILITY |
READ COMMITTED [{RECORD_VERSION |
NO RECORD_VERSION}]]
[RESERVING <предложение резервирования>]

Предложение RESERVING задает необязательное резервирование таблиц. Подробнее резервирование таблиц мы рассмотрим позже. Синтаксис предложения резервирования следующий:

<таблица> [, <таблица> ...]
[FOR [SHARED | PROTECTED] {READ | WRITE}]
[, <предложение резервирования> ...]

Значением по умолчанию для транзакции (когда не заданы характеристики в операторе SET TRANSACTION или буфер параметров транзакции пустой) является:

SET TRANSACTION READ WRITE WAIT SNAPSHOT;

Этому набору характеристик по умолчанию соответствует следующий состав параметров TPB:

isc_tpb_concurrency
isc_tpb_write
isc_tpb_wait

Буфер параметров транзакции при использовании компонентов FIBPlus можно сформировать, поместив в свойство TRParams компонента TpFIBTransaction список мнемонических констант, определенных в модуле ibase.pas.

Самая простая характеристика — режим доступа. READ WRITE (isc_tpb_write в TPB) позволяет в рамках данной транзакции читать и изменять данные базы данных. В случае READ ONLY (isc_tpb_read в TPB) в контексте данной транзакции допустимы только операции чтения данных.

Важнейшая характеристика транзакции — уровень изоляции. В следующей таблице представлены три существующих уровня изоляции транзакции.

Уровни изоляции транзакции в InterBase/Firebird.

SQL

Константа TPB

Значение

READ COMMITTED

isc_tpb_read_committed

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

При этом уровне изоляции используются еще два взаимоисключающих параметра:

По умолчанию NO RECORD_VERSION (isc_tpb_no_rec_version в TPB) требует, чтобы было выполнено подтверждение всех измененных другими транзакциями данных.

RECORD_VERSION (isc_tpb_rec_version в TPB) позволяет читать самую последнюю подтвержденную версию изменений, даже если существуют другие неподтвержденные версии.

SNAPSHOT

isc_tpb_concurrency

Мгновенный снимок (образ). Значение по умолчанию. Другое название — повторяемое чтение (Repeatable Read). Дает состояние базы данных на момент старта транзакции. Изменения, выполненные другими транзакциями, в данной транзакции не видны. Естественно, транзакция «видит» все изменения, выполненные в контексте этой транзакции.

SNAPSHOT TABLE STABILITY

isc_tpb_consistency

Изолированный образ или упорядочиваемый, сериализуемый (Serializable) образ. Аналогичен уровню SNAPSHOT с тем отличием, что другим транзакциям разрешено чтение данных из таблиц данной транзакции, однако они не могут вносить в них никаких изменений.

Помимо перечисленных в таблице уровней изоляции в литературе по базам данных описывается еще один уровень — Dirty Read, грязное чтение, или, другими словами, неподтвержденное чтение, Read Uncommitted. Этот уровень позволяет транзакции читать неподтвержденные изменения, выполненные другими транзакциями. В InterBase и Firebird такой уровень изоляции не поддерживается.

Еще одной характеристикой транзакции является режим разрешения блокировок. Если установлено WAIT (isc_tpb_wait), то при появлении конфликтов обновления данная транзакция будет ожидать разрешения блокировки со стороны других транзакций путем выдачи ими оператора подтверждения или отмены транзакции. Если же задано NO WAIT (isc_tpb_nowait), то при появлении блокировки данная транзакция немедленно вызывает исключение и формирует значения кодов. Напомним, что WAIT устанавливается по умолчанию.

Средства резервирования рассмотрим несколько позже.

Если свойство транзакции TPBMode установить в значение tpbReadCommitted (уровень изоляции READ COMMITTED; это значение, которое будет установлено для транзакции, когда вы помещаете компонент на форму), то независимо от того, какие вы будете задавать значения в свойстве TRParams, после запуска транзакции это свойство будет содержать следующие константы:

isc_tpb_write
isc_tpb_nowait
isc_tpb_rec_version
isc_tpb_read_committed

Аналогично, если для TPBMode установить tpbRepeatableRead (уровень изоляции SNAPSHOT), то TRParams после запуска транзакции будет содержать:

isc_tpb_write
isc_tpb_nowait
isc_tpb_rec_version

В этом случае по умолчанию будет установлено также

isc_tpb_concurrency

Следовательно, чтобы из программы осуществлять управление характеристиками транзакции напрямую, необходимо для TPBMode задать tpbDefault.

Обратите внимание, что первым параметром, передаваемым в буфере параметров транзакции, всегда является isc_tpb_version3, задающий версию транзакции, однако не пытайтесь передавать этот параметр в случае использования FIBPlus, так как этот параметр формируется и передается функции API isc_start_transaction() автоматически. При работе программы, когда вы щелкните по кнопке ParamTransact, появится форма, в которой будут представлены как мнемонические имена параметров транзакции, которые были вами заданы в свойстве TRParams, так и фактические числа, содержащиеся в буфере параметров транзакции. Первым числом всегда будет 3 — это числовое значение параметра isc_tpb_version3.

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

isc_tpb_version1 = 1;
isc_tpb_version3 = 3;
isc_tpb_consistency = 1;
isc_tpb_concurrency = 2;
isc_tpb_shared = 3;
isc_tpb_protected = 4;
isc_tpb_exclusive = 5;
isc_tpb_wait = 6;
isc_tpb_nowait = 7;
isc_tpb_read = 8;
isc_tpb_write = 9;
isc_tpb_lock_read = 10;
isc_tpb_lock_write = 11;
isc_tpb_verb_time = 12;
isc_tpb_commit_time = 13;
isc_tpb_ignore_limbo = 14;
isc_tpb_read_committed = 15;
isc_tpb_autocommit = 16;
isc_tpb_rec_version = 17;
isc_tpb_no_rec_version = 18;
isc_tpb_restart_requests = 19;
isc_tpb_no_auto_undo = 20;
isc_tpb_last_tpb_constant = isc_tpb_no_auto_undo;

Исследование характеристик транзакций

Начнем эксперименты. Заметьте, что в нашей программе мы имеем так называемую «длинную» транзакцию — пользователь вручную запускает транзакцию, выполняет различные изменения данных и подтверждает или отменяет транзакцию, когда ему заблагорассудится. Это может приводить к блокировкам. Пример «короткой» транзакции — пользователь в программе щелкает по кнопке ОК на форме добавления или изменения данных в базе данных, после чего программа вызывает метод Insert или Edit для соответствующего набора данных, устанавливает значения столбцов и вызывает метод Post, который отправляет изменения в базу данных; транзакция обновления запускается в момент вызова метода Post; сразу после Post выполняется подтверждение транзакции. Как правило, время жизни такой короткой транзакции исчисляется долями секунды, что уменьшает вероятность блокировок при многопользовательской работе с базой данных. Нашей задачей является выяснение условий появления блокировок, поэтому мы будем использовать длинные транзакции.

Уровень изоляции READ COMMITTED

Пожалуй, наиболее часто используемым является уровень изоляции READ COMMITTED. Он позволяет транзакции видеть подтвержденные изменения, выполненные другими транзакциями. Рассмотрим его использование. Запустите два экземпляра созданной программы. В одной программе задайте следующие характеристики транзакции, щелкнув по кнопке Characteristics:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_rec_version
isc_tpb_nowait

Это означает, что транзакция может читать и писать данные (isc_tpb_write), имеет уровень изоляции READ COMMITTED (isc_tpb_read_committed), то есть, транзакция видит подтвержденные изменения, выполненные в других транзакциях. При этом транзакцию «устроит» любая последняя подтвержденная версия записи, даже если существуют другие неподтвержденных версии той же записи (isc_tpb_rec_version). При возникновении конфликтов блокировки транзакция не будет ожидать разрешения конфликта со стороны другой транзакции, а сразу выдаст исключение (isc_tpb_nowait).

Для второй транзакции задайте следующие характеристики:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_rec_version
isc_tpb_wait

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

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

Вперед. Начнем создавать конфликтные ситуации и героически их разрешать.

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

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

Что произошло? Похоже, нашу вторую программу заклинило. Хорошо, если еще виден курсор мыши в виде песочных часов или привычной стрелочки. В некоторых случаях и курсора не видно.

Дело в том, что для второй программы вы задали режим ожидания разрешения конфликта блокировки (isc_tpb_wait). Щелкните по кнопке подтверждения транзакции в первой таблице. Вторая программа сразу оживет и выдаст соответствующее сообщение об ошибке (это сработал наш обработчик ошибок).

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

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

Измените режим ожидания во второй программе: в характеристиках транзакции уберите параметр isc_tpb_wait и добавьте isc_tpb_nowait.

Внимание! Параметр isc_tpb_wait применяется по умолчанию. Вам всегда нужно явно задавать isc_tpb_nowait.

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

Смоделируем еще один конфликт. В первой программе перейдите к записи USA и удалите ее, нажав клавиши Ctrl+Del и подтвердив удаление в появившемся стандартном диалоговом окне. Не подтверждайте транзакцию. Во второй программе также перейдите к строке USA и удалите ее. Возникнет конфликт. Отмените транзакции в обеих программах. Удаленная запись опять появится в списке. Опять удалите ее в первой программе. Во второй программе попытайтесь изменить или удалить любую запись в таблице регионов (нижняя сетка DBGridRegion) этой же страны. Здесь также возникнет конфликт. Конфликт возникнет и в том случае, когда вы измените в первой программе ключевой столбец (код страны), а во второй программе попытаетесь изменить или удалить подчиненную запись региона. В таблице регионов столбец CodCtr является внешним ключом, ссылающимся на код страны в таблице стран. В описании внешнего ключа было указано:

CONSTRAINT "Region_FOREIGN_KEY"
FOREIGN KEY (CodCtr) REFERENCES REFCOUNTRY (CodCtr)
ON DELETE CASCADE
ON UPDATE CASCADE

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

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

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

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

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

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

Внимание! Параметр isc_tpb_no_rec_version применяется по умолчанию. Вам нужно явно задавать isc_tpb_rec_version.

Интересные результаты можно получить, задавая параметр isc_tpb_no_rec_version вместе с isc_tpb_wait.

Вы помните, когда для транзакции задается параметр isc_tpb_rec_version вместе с isc_tpb_wait, то при возникновении конфликта вторая программа перейдет в состояние ожидания, а при подтверждении транзакции в первой программе вторая программа выдаст исключение.

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

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

Теперь одновременно заклиним обе программы — создадим настоящую «смертельную блокировку» (dead lock, или взаимную блокировку), когда первая программа ожидает подтверждения или отмены транзакции второй программы, а вторая программа ожидает того же от первой программы.

В обеих программах установите следующие характеристики транзакций:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_wait
isc_tpb_rec_version

Запустите транзакции и откройте наборы данных. Во второй программе выполните изменение в одной строке. В первой программе — изменения в другой строке. После этого во второй программе попытайтесь изменить ту же строку, которая была изменена в первой программе. В этот момент вторая программа перейдет в режим ожидания, поскольку среди параметров ее транзакции присутствует isc_tpb_wait., задающий режим ожидания разрешения конфликта блокировки другой транзакцией. Затем в первой программе измените строку, которая была изменена второй программой. Первая программа также перейдет в режим ожидания. В этот момент и происходит действительно «смертельная» взаимная блокировка, deadlock, когда обе программы ожидают друг друга.

Однако на самом деле ничего страшного не происходит. Через несколько секунд первая программа выдает исключение со следующим сообщением: deadlock. update conflicts with concurrent update (взаимная блокировка, изменение конфликтует с параллельным изменением). Сервер базы данных имеет средства для выявления ситуаций взаимных блокировок. Анализ блокировок осуществляет Менеджер блокировок (lock Manager). Для уменьшения накладных расходов и повышения производительности Менеджер не постоянно отслеживает ситуацию, а периодически запускается через определенное количество секунд. Интервал времени запуска Менеджера блокировок задается параметром DeadlockTimeout в файле конфигурации firebird.conf для Firebird 1.5 или параметром DEADLOCK_TIMEOUT в файле конфигурации ibconfig для InterBase. Значением по умолчанию является 10 секунд.

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

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

Уровень изоляции SNAPSHOT

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

В первой программе сохраните уровень изоляции READ COMMITTED, задав параметры:

isc_tpb_write
isc_tpb_read_committed
isc_tpb_rec_version
isc_tpb_nowait

Во второй программе установите следующие параметры транзакции:

isc_tpb_write
isc_tpb_concurrency
isc_tpb_nowait

Измените в первой программе какую-нибудь строку, но не подтверждайте транзакцию. Во второй программе попытайтесь изменить ту же самую строку. Как и ожидалось, такое изменение вызывает ошибку блокировки.

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

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

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

Уровень изоляции SNAPSHOT TABLE STABILITY

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

Измените во второй программе характеристики транзакции. Создайте следующий список параметров:

isc_tpb_write
isc_tpb_nowait
isc_tpb_consistency

Запустите в ней транзакцию и откройте набор данных.

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

Такой уровень изоляции можно назвать приблизительно следующим образом: «по-настоящему работаю только я и больше никто». Однако здесь есть один подводный камень. Если при запуске транзакции с уровнем изоляции SNAPSHOT TABLE STABILITY и открытии в ее контексте набора данных другая параллельная транзакция уже изменила данные в том же наборе данных и еще не подтвердила эти изменения, то открытие набора данных в нашей транзакции приведет к исключению.

Проверим это на практике. Остановите транзакцию во второй программе. В первой программе измените любую строку в таблице стран. Сейчас это можно выполнить, поскольку вторая транзакция неактивна. После этого запустите транзакцию во второй программе и попытайтесь открыть набор данных. Вы тут же получите исключение lock conflict on no wait transaction. Здесь следует остановить транзакцию во второй программе, подтвердить или отменить изменения в первой программе и снова запустить транзакцию и открыть набор данных во второй программе.

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

Прочитано 6774 раз

Авторизация



Счетчики