Транзакции
В данном разделе описаны транзакции, применительно к Кипарису и статическим таблицам, блокировки и версионированность объектов Кипариса.
Транзакционная модель динамических таблиц описана в разделе Мультиверсионность и транзакционность динамических таблиц.
Общие сведения
Система 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 ) |
Версионирование
Изменение состояния узла транзакцией представляет собой следующий трехфазный процесс:
-
Транзакция
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
. Для данного описания не имеет значения, была ли взята блокировка явно или неявно. - для
-
Транзакция
T
работает с узломN
, при этом фактические изменения накапливаются в его версииN:T
. -
Транзакция
T
успешно завершается, при этом внесённые ей вN:T
изменения, если таковые имеются, вливаются в версию, от которой версияN:T
была ответвлена. Таким образом, эти изменения становятся видны родительской транзакции — или вообще всем, еслиT
была транзакцией верхнего уровня.
Использование транзакций в операциях
При запуске операций планировщик создает набор транзакций, чтобы обеспечить определенную атомарность обработки данных в операции. Подробнее про схему работы можно прочитать в разделе Транзакционность обработки данных.