Как работает версионность данных?

Автор: Ann Harrison
Перевод и комментарии: Дмитрий Кузьменко

Предварительно (или одновременно) рекомендуется ознакомиться со статьей "Состояния транзакций, сборка мусора, ..."
 

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

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

>... сервер предполагает что транзакция является "заинтересованной" пока она не закончится

Гмм, хорошо. Что касается словаря терминов, то IB использует термин "заинтересованная транзакция" только для описания транзакции, которая не завершена (not committed). Обычно, старейшая заинтересованная транзакция это та, которая первой завершилась rollback после очередного (или последнего) sweep. Любая активная транзакция, даже если она не держит открытым какой-либо набор записей, является заинтересованной. Другими словами, активные транзакции являются подмножеством заинтересованных транзакций. Активная транзакция это любая транзакция, которая стартовала но не была завершена по commit/rollback или не завершилась по причине сбоя клиентского приложения или сервера.
 

>я полагаю что разностные записи являются одним из факторов стоимости транзакций

Когда запись изменяется или удаляется, IB всегда сохраняет обратную версию (back version, или "разностную" версию) записи, даже если операция производилась в "однопользовательском" режиме. Обратная версия используется как пре-версия, когда изменения отменяются по rollback. Транзакция, которая держит открытым большой набор записей, никак не увеличивает количество обратных версий. Обратные версии используются  только транзакциями, изменяющими данные.

Стоимость транзакций увеличивается лишь при наличии транзакции, которая длится очень долго, даже если она не читает ни одной записи. IB сохраняет состояние базы данных – всей базы данных – на момент старта старейшей активной транзакции. Дальнейшее описание покажет, как это все работает.
 

>Кроме того, показалось что запрос вернет результат не на текущий момент,
>а будет извлекать данные с момента A, затем когда пользователь
>прокрутит данные вниз, записи добавленные после момента B, и так далее.

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

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

Это простая вставка. Сервер находит страницу с достаточным местом для хранения записи и ее заголовка, и помещает на ней запись, маркируя ее идентификатором транзакции 10 (TID).
 
Транзакция 11 стартует
Транзакция 10 завершается commit
Транзакция 12 стартует
Транзакция 12 модифицирует запись, созданную транзакцией 10

Транзакция 12 вычисляет разницу для воспроизведения версии, созданной транзакцией 10. Транзакция 12 помечает обратную версию записи номером транзакции 10, и записывает ее на свободное место. Затем Транзакция 12 заменяет оригинальную запись, сохраненную Транзакцией 10 (и маркированную идентификатором 10), новой версией записи, маркированной идентификатором Транзакции 12. Новая запись содержит указатель на обратную версию предыдущей версии записи.
 
Примечание ДК – другими словами. На свободное место записывается delta, маркированная как запись 10-ой транзакции, а на старое место, занимаемое записью транзакции 10 пишется новая запись транзакции 12. Указатель в записи транзакции 12 на дельту транзакции 10 позволяет восстановить из записи 12-ой транзакции запись 10-ой транзакции. Понятно? :-)
Транзакция 13 стартует
Транзакция 14 стартует
Транзакция 12 завершается commit
Транзакция 15 стартует

Транзакция 11 перечитывает данные таблицы

Когда транзакция стартует, то она получает снимок состояний заинтересованных и активных транзакций, который позволяет определить доступные для чтения версии записей. Если проигнорировать уровень изоляции read committed, то транзакция (snapshot = concurency = repeatable read) может видеть только те версии записей, которые были сохранены (committed) до ее старта. Поэтому Транзакция 11 не видит версию Транзакции 10. Разумеется, сервер не передает клиенту "лишние" версии. Он читает главную версию – на текущий момент это версия Транзакции 12. Затем сервер проверяет состояние этой транзакции в локальном TIP (локальном для транзакций repeatable read и глобальном для read committed), и не обнаруживает там 12-ой Транзакции. Проверяет запись и находит там указатель на обратную версию. Читает обратную версию и обнаруживает, что она была создана Транзакцией 10, которая есть в локальном TIP, но не в состоянии committed (а в active). Поскольку далее обратных указателей нет, то никаких записей клиенту не возвращается.

Транзакция 13 читает таблицу

Когда стартовала Транзакция 13, то Транзакция 10 уже была в состоянии committed, поэтому Транзакция 13 видит версию записи,  созданную Транзакцией 10. Сервер производит те же действия, что и в предыдущем случае, но обнаруживает что версии записей Транзакции 10 могут быть считаны Транзакцией 13, и поэтому возвращает соответствующие записи.
 
Транзакция 15 читает таблицу

Когда стартовала Транзакция 15, Транзакции 10 и 12 были завершены по commit. Сервер находит и возвращает версию записи Транзакции 12 для Транзакции 15. Он также обнаруживает что у этой версии есть указатель на обратную версию, и сверяется с глобальной таблицей активных транзакций для того, чтобы определить, не устарела ли эта версия записи. Поскольку Транзакция 11 все еще "жива" и не может видеть изменений, произведенных Транзакцией 12, версия записи Транзакции 10 не является устаревшей, и Транзакция 15 (или сервер) оставляет эту версию на своем месте.
 
Транзакция 14 пытается обновить запись, созданную Транзакцией 10

Транзакция 14 стартовала после завершения (commit) Транзакции 10, но после старта Транзакции 12. Сервер обнаруживает, что Транзакция 14 пытается обновить старую версию записи, в то время как уже существует новая версия записи. Транзакциям не разрешается перезаписывать изменения конкурирующих транзакций. Поэтому сервер проверяет состояние Транзакции 12. Если она на момент старта Транзакции 14 была активна, то Транзакция 14 должна либо подождать, либо немедленно получить сообщение об ошибке (зависит от параметра wait транзакции 14). Если был указан параметр WAIT, то Транзакция 14 будет ждать завершения Транзакции 12 по commit или rollback. Если Транзакция 12 отменена, то Транзакция 14 успешно завершит обновление и сможет продолжать дальше. Если Транзакция 12 завершится по commit, то в этот момент Транзакция 14 получит сообщение "update conflict". В нашем случае Транзакция 12 уже завершена по commit, поэтому Транзакция 14 немедленно получает сообщение об ошибке, отменяется (rolled back) и рестартует в виде Транзакции 16.
 
Примечание ДК. Разумеется, сервер сам не отменяет и не завершает транзакции. Имеется в виду, что транзакции repeatable read при ошибке update conflict не должны завершаться по commit в клиентских приложениях. Единственным выходом для программиста при такой ошибке является отмена (rollback) этой транзакции и старт новой. Кстати, если в транзакции была только единственная операция обновления данных, которая завершилась подобным образом, т. е. update conflict, то ее не обязательно отменять по rollback – все равно никаких изменений в БД сделано не было, и можно завершать транзакцию по commit, чтобы rollback не привел к образованию "старейшей заинтересованной транзакции" ).
Транзакция 16 стартует
Транзакция 16 пытается обновить запись, созданную Транзакцией 10

Поскольку Транзакция 16 стартовала после завершения Транзакции 12, она может читать и обновлять главную версию записи. В конце такого обновления на диске остается: главная версия записи с идентификатором Транзакции 16, обратная версия Транзакции 12 и еще одна обратная версия Транзакции 10. Ясно? Обратили внимание, что обратная версия Транзакции 10 осталась на своем месте и в нетронутом состоянии?
 
Транзакция 11 читает таблицу
Транзакция 13 читает таблицу
Транзакция 15 читает таблицу
Транзакция 16 читает таблицу

Каждая транзакция видит обратные версии записей Транзакций 12 и 10 (за исключением Транзакции 11, которая так до сих пор не может видеть ни одной версии). Каждая транзакция читает только версию, сохраненную на момент ее старта. Все транзакции получают воспроизводимое чтение (repeatable read).
 
Транзакция 11 не видит ни одной из версий
Транзакция 13 видит версию Транзакции 10
Транзакция 15 видит версию Транзакции 12
Транзакция 16 видит свою собственную версию (не сохраненную по commit)

В это время, Транзакции 15 и 16 видят, что читаемая ими запись имеет указатель на обратную версию, и пытаются определить, не является ли эта обратная версия ненужной. Обе транзакции обнаруживают, что старейшей активной является Транзакция 11, которая не может видеть изменений, произведенных Транзакцией 12. Поэтому никакие обратные версии не являются устаревшими и сборка мусора не производится.
 
Транзакция 11 завершается commit
Транзакция 13 завершается commit
Транзакция 16 завершается commit
Транзакция 17 стартует
Транзакция 17 читает таблицу

Когда Транзакция 17 (или сервер) читает запись, о которой мы говорим уже полчаса, она читает версию, созданную Транзакцией 16. Поскольку у этой версии есть указатель на обратную версию, то Транзакция 17 предполагает выполнение сборки мусора. В соответствии с локальным TIP Транзакции 17 старейшей активной является Транзакция 15. Она может видеть изменения, сделанные Транзакцией 12. Таким образом, версия Транзакции 10 становится никому не нужной и убирается как мусор.

Для развлечения давайте сделаем один rollback.
 
Транзакция 18 стартует
Транзакция 18 модифицирует запись

Все то же самое во второй (третий, пятый?) раз. Окончательно мы имеем главную версию записи с идентификатором Транзакции 18, первую обратную версию с идентификатором Транзакции 16, и вторую обратную версию с идентификатором Транзакции 12. Помните старую добрую Транзакию 12? Это было так давно.
 
Транзакция 18 завершается по rollback
Транзакция 19 стартует
Транзакция 19 читает таблицу

В копии TIP Транзакции 19 записано, что Транзакция 18 завершена по rollback. Когда она читает запись Транзакции 18 (ДК: по commit или rollback никаких копирований или удалений записей не происходит, меняется только состояние транзакции в глобальном TIP. Как вы уже поняли, работа с версиями записей идет только при их чтении), то видит указатель на обратную версию, и применяет обратную версию к главной записи. Этот процесс восстанавливает запись Транзакции 16 в оригинальном виде. Транзакция 19 помещает эту версию записи на место главной записи (там где была версия Транзакции 18), и указатель обратной версии теперь показывает на обратную версию Транзакции 12. И вот... чудо! Обратная версия Транзакции 12 опять не сдвинулась с сместа.

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

>Теперь добавим несколько пользователей, и у каждого открыт большой набор данных.
>Похоже что нагрузка сервера будет значительной, не так ли?

Любая длительная транзакция предотвращает сборку мусора. Большой набор записей, маленький набор записей  не имеет значения. Если транзакция существует и видит какую-то версию записи, то эта версия будет оставаться на диске.
 
Примечание ДК. Можно ускорить чтение данных, если запретить сборку мусора, указав в параметрах подключения (FIBC, IBX или IB API) константу isc_dpb_no_garbage_collect).
Надеюсь, что объяснение было интересным,
Ann
 
  1. Несколько лет назад (и сейчас) не было способа открыть базу данных в однопользовательском режиме – таком режиме, который не позволял бы другим пользователям подключиться, но одному пользователю давал полный доступ к данным. На ум приходят некоторые специальные режимы подключения – например, sweep, которые требуют однопользовательского доступа, но они не дают полного доступа.
Если вы являетесь первым пользователем, подключившимся к базе данных, то IB произведет над базой данных некоторые действия (например, сжатие глобального TIP), но сразу после этого другие пользователи смогут подключиться к этой базе данных.
  1. Режим read-committed был добавлен после меня. Нужно сказать, что этот режим не обеспечивает воспроизводимого чтения.
  2. Поскольку при реальной работе транзакции перекрываются, то старейшая активная транзакция не является самой старой стартовавшей транзакцией. В нашем примере старейшей активной транзакцией является Транзакция 10, пока существует Транзакция 11.

Подпишитесь на новости Firebird в России

Подписаться