Транзакции

В данном разделе описаны транзакции, применительно к Кипарису и статическим таблицам, блокировки и версионированность объектов Кипариса.
Транзакционная модель динамических таблиц описана в разделе Мультиверсионность и транзакционность динамических таблиц.

Общие сведения

Система YTsaurus поддерживает транзакции, но имеет ряд отличий от классической транзакционной системы:

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

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

  • Атомарность (Atomicity). YTsaurus гарантирует, что транзакция не будет зафиксирована в системе частично. Будут либо выполнены все её подоперации, либо не выполнено ни одной. Изменение данных одного узла Кипариса в рамках одной команды (например, команды set) атомарно.
  • Согласованность (Consistency). Транзакция, фиксирующая свои результаты, сохраняет согласованность данных в узлах Кипариса и статических таблицах.
  • Изолированность (Isolation). В отличие от традиционных транзакционных систем, YTsaurus позволяет управлять изолированностью на уровне транзакции за счет разных режимов блокировок. Поведение YTsaurus соответствует уровню изоляции Read committed при взаимодействии с Кипарисом и Serializable при взаимодействии со статическими таблицами в рамках транзакций.
  • Долговечность (Durability). YTsaurus гарантирует сохранность изменений после фиксации транзакции. Сбои в оборудовании одного сервера, обесточивание или выключение системы не могут привести к потере сделанных изменений. Транзакции в системе могут переживать отключение системы и продолжать выполнение после восстановления системы.

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

Транзакции представляют собой объекты типа transaction. Список всех транзакций системы можно найти в Кипарисе, по адресу //sys/transactions.
У транзакции может быть родитель — другая транзакция. Транзакции образуют дерево, корнями которого являются транзакции без родителей, называемые также транзакциями верхнего уровня. Список всех транзакций верхнего уровня доступен по адресу //sys/topmost_transactions.

Транзакции делятся на мастер-транзакции и таблет-транзакции. Мастер-транзакции позволяют выполнять операции с метаинформацией мастера. Таблет-транзакции — только записывать данные в динамические таблицы.

Мастер-транзакции

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

Создание транзакции

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

Продление времени жизни транзакции

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

Завершение транзакции

Транзакцию можно отменить командой abort_tx или успешно завершить командой commit_tx.

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

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

Атрибуты транзакций

Помимо атрибутов, присущих всем объектам, транзакции имеют следующие атрибуты:

Атрибут Тип Описание Обязательный
timeout integer Таймаут транзакции в мс Может отсутствовать для некоторых системных транзакций. Нет
title string Текстовая строка-описание. Данный атрибут заполняется автоматически для всех системных транзакций, а для пользовательских — только если пользователь самостоятельно укажет его при создании транзакции. Нет
last_ping_time DateTime Время последнего продления жизни транзакции. Может отсутствовать для некоторых системных транзакций. Нет
parent_id Guid Идентификатор родительской транзакции. Да
start_time DateTime Время создания транзакции. Да
nested_transaction_ids array<Guid> Список идентификаторов вложенных транзакций. Да
staged_object_ids array<Guid> Список идентификаторов объектов, которыми временно владеет транзакция. Да
branched_node_ids array<Guid> Список идентификаторов ответвленных узлов Кипариса. Да
locked_node_ids array<Guid> Список идентификаторов заблокированных узлов Кипариса. Да
lock_ids array<Guid> Список идентификаторов блокировок, созданных в транзакции. Да
resource_usage ResourceUsageMap Атрибут, показывающий использование ресурсов в данной транзакции для каждого затронутого аккаунта. Да

Примечание

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

Блокировки

Версионирование узлов Кипариса связано с понятием блокировки. Беря блокировку на узел, транзакция выражает намерение работать с узлом в соответствии с режимом блокировки и в случае успеха гарантируется, что:

  • работать указанным образом с данным узлом разрешается;
  • узел для данной транзакции ответвлен.

Примечание

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

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

Блокировка является полноценным объектом и обладает идентификатором. Список всех блокировок в системе доступен по адресу //sys/locks. Для обращения к конкретной блокировке рекомендуется использовать адрес вида #lock-id.

Список блокировок, взятых транзакцией, отображается в ее атрибуте lock_ids.

Режимы блокировок

Существуют следующие режимы блокировок: snapshot, exclusive и shared.
Режим блокировки определяет список допустимых действий транзакции, а также возможность взять другие блокировки:

  • snapshot - транзакция получает возможность читать, но не модифицировать узел. Блокировка используется, чтобы взять read-only копию состояния узла Кипариса в контексте транзакции и зафиксировать состояние данного узла.

    Примечание

    ​ Snapshot-блокировка берется только на сам узел, но не на путь до него в Кипарисе. Если продолжать обращаться к узлу по его пути, можно получить новый узел, размещенный по тому же пути.
    Чтобы гарантированно обратиться к snapshot-версии узла, используйте id данного узла, который возвращает команда lock.

  • exclusive - транзакция получает возможность модифицировать состояние узла. При этом другие транзакции не могут менять узел.

  • shared - транзакция получает возможность модифицировать определенную часть состояния узла. У других транзакций при этом остается возможность изменять другие части этого узла.

    Существует три стандартных сценария использования данной блокировки:

    • Одновременное добавление данных в таблицу или файл из нескольких транзакций. При этом возможно только добавление, перезапись невозможна.
    • Одновременное создание нескольких различающихся по имени подкаталогов в пределах одного каталога из нескольких транзакций. Например, транзакция T1 может начаться и создать (или удалить) узел //tmp/a, а транзакция T2 — начаться и создать (или удалить) //tmp/b. При этом каждая из них возьмет по отдельной shared-блокировке на //tmp. Для выявления конфликтов каждая блокировка имеет атрибут child_key, указывающий на то, какой ключ (подкаталог) ей заблокирован.
    • Одновременное создание нескольких различающихся по имени атрибутов в пределах одного узла из нескольких транзакций. Например, транзакция T1 может начаться и установить (изменить, удалить) атрибут //tmp/@a, а транзакция T2 — начаться и установить (изменить, удалить) атрибут //tmp/@b. Для выявления конфликтов каждая блокировка имеет атрибут attribute_key, указывающий на то, какой ключ (имя атрибута) ей заблокирован.

Примечание

Транзакции бывают вложенными. Под «другими транзакциями» здесь понимаются транзакции, неродственные данной, т. е. не имеющие с ней общего предка. Из-за вложенных транзакций, в частности, на узле может быть более одной exclusive блокировки.

Неявные блокировки

Транзакция может брать блокировки как явно, с помощью команды lock, так и неявно. Неявное взятие блокировок может возникать при определенных взаимодействиях с узлами Кипариса, например:

  • Создание узла сопровождается взятием exclusive блокировки.
  • Запись в таблицу или файл означает взятие shared блокировки, если происходит добавление данных в таблицу, и exclusive блокировки, если происходит ее перезапись.
  • Создание новой записи в каталоге, а также изменение или удаление существующей записи сопровождаются взятием shared блокировки с соответствующим child_key.
  • Создание нового атрибута узла, а также изменение или удаление существующего атрибута сопровождаются взятием shared блокировки с соответствующим attribute_key.

Совместимость блокировок

Некоторые комбинации блокировок могут быть взяты одновременно. Формальные правила следующие:

  • snapshot блокировку можно взять всегда. Если данной транзакцией snapshot блокировка уже взята, попытка повторного взятия завершается без ошибок и не имеет эффекта.
  • shared или exclusive блокировку нельзя взять, если транзакцией или любым ее предком уже взята snapshot блокировка.
  • shared или exclusive блокировку нельзя взять, если другой транзакцией, не являющейся предком данной, уже взята exclusive блокировка.
  • exclusive блокировку нельзя взять, если другой транзакцией, не являющейся предком данной, уже взята shared блокировка.
  • shared блокировку с заданным child_key нельзя взять, если другой транзакцией, не являющейся предком данной, уже взята shared блокировка с тем же child_key.
  • shared блокировку с заданным attribute_key нельзя взять, если другой транзакцией, не являющейся предком данной, уже взята shared блокировка с тем же attribute_key.
  • shared блокировку без child_key и attribute_key можно взять, несмотря на любые другие shared блокировки.

Операции над блокировками

Для работы с блокировками существуют две команды: lock и unlock.

Команда lock позволяет взять блокировку на узел Кипариса в указанной транзакции.

Команда unlock выполняет обратное действие: снимает все явные блокировки с узла для заданной транзакции — как уже взятые, так и еще стоящие в очереди на взятие блокировки.
Блокировку можно снять, только если заблокированная ответвленная версия узла не содержит изменений по сравнению с оригиналом. Следовательно, явную snapshot блокировку можно снять всегда. В противном случае команда завершится ошибкой.

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

Очередь на взятие блокировки

С каждым узлом Кипариса может быть связана очередь ожидающих блокировок.

По умолчанию команда lock пытается взять блокировку, и в случае, если узел уже заблокирован, возвращает ошибку. Если в команде lock указать параметр waitable, равный true, блокировка встанет в очередь.
Чтобы узнать, находится ли блокировка в очереди, запросите атрибут state. Если блокировка находится в очереди, он будет равен pending, если нет - acquired.

Внимание

Чтобы блокировка была действительно взята, например, эксклюзивно, ее состояние должно перейти в acquired.
Чтобы взять блокировку:

  • вызовите команду lock;
  • наблюдайте в цикле за state, пока он не примет значение acquired.

Атрибуты блокировок

Помимо атрибутов, присущих всем объектам, блокировки имеют следующие атрибуты:

Атрибут Тип Описание
state string Состояние блокировки: pending или acquired.
transaction_id Guid Идентификатор транзакции, взявшей блокировку.
mode string Режим блокировки: shared, exclusive или snapshot.
child_key string Ключ, по которому взята блокировка. Только для типа shared.
attribute_key string Имя атрибута, по которому взята блокировка (только для типа shared)

Версионирование

Изменение состояния узла транзакцией представляет собой следующий трехфазный процесс:

  1. Транзакция T берет на узел N блокировку. При этом у узла N возникает версия N:T, которая образуется следующим образом:

    • для snapshot блокировки она ответвляется от версии N:T', где T' — ближайший предок T, ответвивший N;
    • для shared и exclusive блокировок от N:T' ответвляется N:T'', где T'' — дочка T' ; от N:T'' ответвляется N:T''', где T''' — дочка T'' и т.д. вплоть до N:T. Иными словами, создается цепь ответвленных друг от друга версий узла для каждой транзакции от T' до T.

    В случае, если такого T' нет, используется реальная версия N. Для данного описания не имеет значения, была ли взята блокировка явно или неявно.

  2. Транзакция T работает с узлом N, при этом фактические изменения накапливаются в его версии N:T.

  3. Транзакция T успешно завершается, при этом внесённые ей в N:T изменения, если таковые имеются, вливаются в версию, от которой версия N:T была ответвлена. Таким образом, эти изменения становятся видны родительской транзакции — или вообще всем, если T была транзакцией верхнего уровня.

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

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

Предыдущая
Следующая