Транзакции

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Транзакцию можно отменить командой abort_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 узла. В таком случае узел может быть изменен между вычислением id и взятием блокировки, поэтому могут потребоваться дополнительные попытки.
- получите id snapshot-узла по lock_id. Получить lock_id можно методом lock, прочитав его по пути #<lock_id>/@node_id.

  • 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 была транзакцией верхнего уровня.

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

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