Транзакции | Tarantool
Документация на русском языке
поддерживается сообществом
Транзакции

Транзакции

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

Как Tarantool выполняет основные операции? Для примера возьмем такой запрос:

tarantool> box.space.tester:update({3}, {{'=', 2, 'size'}, {'=', 3, 0}})

Это соответствует следующему SQL-выражению для таблицы, где первичные ключи — поле field[1]:

UPDATE tester SET "field[2]" = 'size', "field[3]" = 0 WHERE "field[1]" = 3

Предположим, что этот запрос Tarantool получил по сети, — тогда три потока операционной системы будут обрабатывать этот запрос:

  1. Сетевой поток на стороне сервера получает запрос, разбирает выражение, проверяет его на правильность и преобразует его в специальную структуру — сообщение, которое содержит готовый для исполнения запрос и его опции.

  2. Сетевой поток отправляет это сообщение в поток обработки транзакций с помощью шины передачи сообщений без блокировок. Lua-программы выполняются непосредственно в потоке обработки транзакций и не требуют разбора и подготовки.

    Чтобы найти нужный кортеж, поток обработки транзакций использует индекс на поле первичного ключа field[1]. Он проверяет, что этот кортеж можно обновить (вряд ли что-то пойдет не так, если мы всего лишь меняем значение не индексированного поля).

  3. Поток обработки транзакций отправляет сообщение в поток упреждающей записи в журнал (WAL) для коммита транзакции. По завершении поток WAL отправляет результат транзакции — COMMIT или ROLLBACK — в поток обработки транзакций, который передает его сетевому потоку, а тот возвращает результат клиенту.

Обратите внимание, что в Tarantool есть только один поток обработки транзакций. Некоторые уже привыкли к мысли, что в базе данных может быть множество потоков для обработки данных (например, один поток читает данные из строки x, а другой в это время записывает данные в столбец y). В Tarantool такого нет. Только у потока обработки транзакций есть доступ к базе, и на каждый экземпляр Tarantool есть только один такой поток.

Как и любой другой поток Tarantool, поток обработки транзакций может управлять множеством файберов. Файбер — это набор команд, среди которых могут быть и сигналы «передачи управления». Поток обработки транзакций выполняет все команды, пока не увидит такой сигнал, и тогда он переключается на выполнение команд из другого файбера. Например, таким образом поток обработки транзакций сначала выполняет чтение данных из строки x для файбера №1, а затем выполняет запись в строку y для файбера №2.

Без передачи управления поток обработки транзакции застрянет на одном файбере. Есть два типа передачи управления:

  • неявная передача управления: любое изменение данных или доступ к сети вызывает неявную передачу управления; любая команда, которая проходит через клиент Tarantool, вызывает неявную передачу управления.
  • явная передача управления: в Lua-функции можно (и нужно) добавлять операторы «передачи управления», чтобы не дать им захватить процессорное время. Это называется кооперативной многозадачностью.

Кооперативная многозадачность означает, что пока запущенный файбер не передает управление явно, его не вытесняет какой-либо другой файбер. Однако запущенный файбер явно передаст управление, если обнаружит “точку передачи управления”: коммит транзакции, системный вызов или запрос на явную «передачу управления». Любой системный вызов, который может блокировать файбер, будет асинхронным. Запущенный файбер, который должен ждать системный вызов, будет вытеснен: другой готовый к работе файбер занимает его место и становится запущенным файбером.

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

При небольших запросах, таких как простые UPDATE, INSERT, DELETE или SELECT, планирование файберов будет справедливым: мало времени требуется на обработку запроса, планирование записи на диск и передачу управления на файбер, обслуживающий следующего клиента.

Однако функция может выполнять сложные расчеты или может быть написана так, что управление не передается в течение длительного времени. Это может привести к несправедливому планированию, когда отдельный клиент перекрывает работу остальной системы, или к явным задержкам в обработке запросов. Автору функции следует не допускать таких ситуаций.

В отсутствие транзакций любая функция, в которой есть точки передачи управления, может видеть изменения в состоянии базы данных, вызванные вытесняющими файберами. Составные транзакции предназначены для изоляции: каждая транзакция видит постоянное состояние базы данных и делает атомарные коммиты изменений. Во время коммита происходит передача управления, а все транзакционные изменения записываются в журнал упреждающей записи в отдельный пакет. Или, при необходимости, можно откатить изменения – полностью или на определенную точку сохранения.

В Tarantool транзакции изолированы полностью — на уровне serializable (упорядочиваемость) с оговоркой: «если нет сбоев при записи в WAL». В случае такого сбоя, например при переполнении дискового пространства, транзакции изолированы на уровне read uncommitted (чтение незафиксированных данных).

In vynil, to implement isolation Tarantool uses a simple optimistic scheduler: the first transaction to commit wins. If a concurrent active transaction has read a value modified by a committed transaction, it is aborted.

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

При тестировании механизма транзакций в Tarantool иногда можно заметить, что передача управления после box.begin(), но перед любой операцией чтения/записи не приводит к прерыванию, как это должно происходить согласно описанию. Причина в том, что на самом деле box.begin() не запускает транзакцию: это просто метка, которая показывает Tarantool, что надо запустить транзакцию после некоторого последующего запроса к базе данных.

In memtx, if an instruction that implies yields, explicit or implicit, is executed during a transaction, the transaction is fully rolled back. In vynil, we use more complex transactional manager that allows yields.

Примечание

На сегодняшний день нельзя использовать разные движки базы данных в одной транзакции.

Единственные запросы явной передачи данных в Tarantool’е отправляют fiber.sleep() и fiber.yield(), но многие другие запросы «неявно» подразумевают передачу управления, поскольку цель Tarantool’а – избежать блокировок.

Запросы к базе данных подразумевают передачу управления тогда и только тогда, когда происходят дисковые операции ввода-вывода. Так как все данные в memtx находятся в оперативной памяти, во время запроса на чтение дискового ввода-вывода не происходит. В vinyl некоторые данные могут находиться не в оперативной памяти, поэтому возможны дисковые операции при чтении (для получения данных с диска) или при записи (потому что может произойти сбой в ожидании освобождения памяти). Запросы на изменение данных и в memtx, и в vinyl должны записываться в WAL, и обычно происходит коммит. Коммит происходит автоматически после каждого запроса в режиме «автоматических коммитов» (autocommit) по умолчанию или в конце транзакции в режиме «транзакция» (transaction), когда пользователь вручную производит коммит, вызывая box.commit(). Поэтому и для memtx, и для vinyl некоторые операции с БД могут подразумевать передачу управления при наличии дискового ввода-вывода.

Многие функции в модулях fio, net_box, console и socket (запросы «ОС» и «сети») передают управление.

Поэтому выполнение отдельных команд, таких как select(), insert(), update() в консоли внутри транзакции, приведет к прерыванию транзакции. Это связано с тем, что после выполнения каждого фрагмента кода в консоли происходит неявная передача управления (yield).

Пример №1

  • Когда движок базы данных - memtx
    В последовательности select() insert() управление передается один раз в конце вставки, что вызвано неявным коммитом; select() ничего не записывает в WAL, поэтому не передает управление.
  • Когда движок базы данных - vinyl
    В последовательности select() insert() управление передается от одного до трех раз: select() может передавать управление, если данных нет в кэше; insert() может передавать управление, пока ожидает доступную память; и при коммите будет неявная передача управления.
  • Последовательность begin() insert() insert() commit() передает управление только при коммите, если движок = memtx, и может передавать управление до 3 раз, если движок = vinyl.

Пример №2

Предположим, что в спейсе ‘tester’ в memtx есть кортежи, в которых третье поле представляет собой положительную сумму в долларах. Начнем транзакцию, снимем со счета из кортежа №1, пополним счет в кортеж №2 и закончим транзакцию, подтвердив изменения.

tarantool> function txn_example(from, to, amount_of_money)
         >   box.begin()
         >   box.space.tester:update(from, {{'-', 3, amount_of_money}})
         >   box.space.tester:update(to,   {{'+', 3, amount_of_money}})
         >   box.commit()
         >   return "ok"
         > end
---
...
tarantool> txn_example({999}, {1000}, 1.00)
---
- "ok"
...

Если wal_mode = ‘none’, то при коммите управление не передается неявно, потому что не идет запись в WAL-файл.

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

conn.space.test:select{1}
conn.space.test:select{2}
conn.space.test:select{3}

вызывает передачу управления три раза при отправке запросов в сеть и ожидании результатов. На стороне сервера те же самые запросы выполняются в общем порядке, возможно, смешиваясь с другими запросами из сети и локальных файберов. Что-то подобное происходит, если использовать клиент, который работает через telnet, с помощью одного из коннекторов или сторонних модулей MySQL и PostgreSQL или в интерактивном режиме, когда Tarantool используется в качестве клиента.

После того, как файбер передал управление, а затем вернул его, он незамедлительно вызывает testcancel.

Нашли ответ на свой вопрос?
Обратная связь