InterBase: тормозология и глюконавтика
Страница 7. Работа с транзакциями


Работа с транзакциями

Есть в InterBase одна сторона, которая делает его очень сильным продуктом. Это как раз та технология, с помощью которой решаются различные аспекты обработки транзакций. Не скажу, что у Борланда здесь всё идеально, но заложенные базовые идеи принципиально более эффективны и прогрессивны, чем у большинства других. Благодаря этому при разработке, основанной на interbase можно просто забыть о многих неприятностях, особенно связанных с блокировками. В общем, изложу сведения на эту тему, почерпнутые из множества мест.

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

Разумеется, реальные схемы блокировок сложнее - они делятся на уровни, типы, разрабатываются сложные протоколы наложения блокировок. Но в целом этот "классический" метод работает по принципу: раз надо предотвратить последствия параллельности, предотвратим саму параллельность. Единственное его достоинство простота ... даже не реализации, а осмысления разработчиком. В семидесятых, когда он появился, это было приемлемо, но сейчас я просто поражаюсь, как можно гнать на рынок блокировочную халтуру, да ещё в таком ассортименте!

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

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

В interbase журнала транзакций не то, чтобы совсем нет. Он есть, но содержит только номера транзакций, отметки времени, и состояние транзакции. А вот записи данных содержат номера тех транзакций, которыми были сформированы. Что и позволяет interbase разобраться, что кому показывать. Какие данные соответствуют текущему моменту, а какие - нет. При этом ничья работа не останавливается.

Итак, каждой запущенной транзакции присваивается уникальный номер (по возрастанию, как из генератора). Кроме этого транзакция так же получает список состояний других транзакций - какие из них завершены, а какие - нет. Когда впоследствии транзакция ищет запись, она начинает просмотр списка версий с последней. Размер списка, как правило невелик, так как очень редко множество транзакций модифицируют одновременно одну и ту же запись. Если последняя версия принадлежит не текущей транзакции и та транзакция на момент старта не была зафиксирована, то делается переход к предыдущей по времени версии записи. Эти правила могут чуть-чуть меняться в зависимости от режима изоляции (repeatable read, read committed, dirty read), но в целом суть примерно та же. Когда же запись модифицирует запись, она реально добавляет новую версию, уже со своим идентификатором. Удаление - это так же добавление новой версии особого рода.

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

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

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

Если Вы пострадали из-за чужой транзакции, то уже ничего не сделаешь. Но если Вы разрабатываете свою программу, то лучше самому такие вещи другим не оставлять. То есть если обновляется одна запись, то можно ничего особенного не делать. Если же идёт массовое обновление таблицы, то лучше всего сразу после этого сделать по ней select count(*) - это прочистит версии и предотваратит дальнейшие аномалии производительности.

В самом предельном случае, если у Вас в базе одна большая таблица, и вы её полностью очищаете одним оператором, то размер базы из-за повторных версий может удвоиться, а время полной выборки таблицы после этой операции будет вдвое-втрое больше, чем было до удаления. Хотя в результате вы получите 0 записей, как и положено. Задержка будет объясняться очисткой версий. На практике, конечно, такое бывает редко, но бывает. Уж лучше бы они завели в сервере какой-нибудь фоновый процесс для очистки.

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

В общем, это работает и в большинстве случаев прекрасно. Если Ваша транзакция только читает данные, то её никто вообще остановить не сможет - здесь параллельность вообще идеальная. Правда её подпортили при переходе к многопоточной реализации сервера. Где-то у них там проблемы с синхронизацией потоков. По крайней мере в версии 4.2. В результате чего тяжёлые запросы имеют свойство блокировать друг друга, а на многопроцессорных машинах - скапливаться в одном процессоре. Старые версии (до 4.2) и версии для Linux и SCO этим не страдают.

Ещё кое-какие замечания по управлению транзакциями есть в разделе по BDE.

 
« Предыдущая статья