Режим транзакций: MVCC
Начиная с версии 2.6.1, в Tarantool доступен другой режим поведения транзакций, который допускает передачу управления ("yields") внутри memtx-транзакций. Этот процесс управляется менеджером транзакций.
Такой режим допускает параллельное выполнение транзакций, но может приводить к конфликтам. Его можно использовать с движком хранения memtx. Движок хранения vinyl также поддерживает режим MVCC, но имеет другую реализацию.
Менеджер транзакций предназначен для изоляции параллельных транзакций и
обеспечивает serializable уровень изоляции транзакций.
Он состоит из двух частей:
-
MVCC – механизм управления параллельным доступом с поддержкой нескольких версий, который хранит все действия по изменению всех транзакций. Он также создает транзакционное представление состояния базы данных и представление чтения (фиксированное состояние базы данных, которое никогда не изменяется другими транзакциями) при необходимости.
-
Conflict manager – менеджер конфликтов, который отслеживает изменения транзакций и определяет их корректность в порядке сериализации. Менеджер конфликтов объявляет транзакции конфликтующими или отправляет транзакции в представления чтения при необходимости.
Начиная с версии 2.10.1, менеджер конфликтов обнаруживает конфликты сразу после фиксации первой из нескольких конфликтующих транзакций. С этого момента любые операции CRUD в конфликтующей транзакции приводят к ошибкам до тех пор, пока транзакция не будет откащена.
Менеджер транзакций также обеспечивает неклассический уровень изоляции
snapshot. Снимок здесь не обязательно привязан ко времени начала
транзакции, как в классическом подходе, когда транзакция получает
согласованное состояние базы данных. Менеджер конфликтов определяет,
получает ли каждая транзакция снимок и когда именно. Это позволяет
избежать некоторых конфликтов по сравнению с классическим подходом
изоляции снимков.
По умолчанию менеджер транзакций отключен. Для его включения через
box.cfg используйте параметр memtx_use_mvcc_engine.
box.cfg{memtx_use_mvcc_engine = true}
Для уровня изоляции транзакций менеджер транзакций поддерживает следующие значения:
best-effort(по умолчанию)read-committedread-confirmedlinearizable(только для конкретной транзакции)
Использование best-effort в качестве значения по умолчанию позволяет MVCC
рассматривать действия транзакций независимо и определять для них
наилучший уровень изоляции. Это повышает
вероятность успешного завершения транзакции и помогает избежать
возможных конфликтов.
Чтобы задать другой уровень изоляции по умолчанию, например
read-committed, используйте следующую команду:
box.cfg { txn_isolation = 'read-committed' }
Учтите, что уровень изоляции linearizable нельзя задать по умолчанию — он доступен только
для конкретной транзакции. Задать уровень изоляции для конкретной транзакции можно в вызове box.begin():
box.begin({ txn_isolation = 'best-effort' })
В этом случае также можно использовать значение default. Оно
устанавливает уровень изоляции транзакции в значение, заданное в
box.cfg.
Уровень изоляции также можно задать в методе net.box stream:begin() и
в запросе бинарного протокола IPROTO_BEGIN.
Выбор подходящего значения зависит от наличия конфликтов. Если конфликтов много, следует задать другое значение или использовать режим транзакций по умолчанию.
Создайте файл init.lua со следующим содержимым:
fiber = require 'fiber'box.cfg{ listen = '127.0.0.1:3301', memtx_use_mvcc_engine = false }box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists = true})tickets = box.schema.create_space('tickets', { if_not_exists = true })tickets:format({{ name = "id", type = "number" },{ name = "place", type = "number" },})tickets:create_index('primary', {parts = { 'id' },if_not_exists = true})
Подключитесь к экземпляру с помощью команды tt connect:
tt connect 127.0.0.1:3301
Затем попробуйте выполнить транзакцию с yield внутри:
box.atomic(function() tickets:replace{1, 429} fiber.yield() tickets:replace{2, 429} end)
Будет получено сообщение об ошибке:
---- error: Transaction has been aborted by a fiber yield...
Кроме того, если оставить транзакцию открытой при возврате из запроса, будет выдано сообщение об ошибке:
127.0.0.1:3301> box.begin()⨯ Failed to execute command: Transaction is active at return from function
Измените значение memtx_use_mvcc_engine на true, перезапустите
Tarantool и попробуйте снова:
127.0.0.1:3301> box.atomic(function() tickets:replace{1, 429} fiber.yield() tickets:replace{2, 429} end)---...
Теперь проверим, успешна ли эта транзакция:
127.0.0.1:3301> box.space.tickets:select({}, {limit = 10})---- - [1, 429]- [2, 429]...
Начиная с 2.10.0, IPROTO реализует потоки (streams) и интерактивные транзакции, которые можно использовать
при включенном на сервере параметре memtx_use_mvcc_engine.
Поток (stream) поддерживает мультиплексирование нескольких транзакций через
одно соединение. Каждый поток имеет собственный идентификатор,
уникальный в пределах соединения. Все запросы с одинаковым ненулевым
идентификатором потока принадлежат одному потоку. Все запросы в потоке
выполняются строго последовательно.
Это позволяет реализовать интерактивные транзакции. Если
идентификатор потока запроса равен 0, запрос не принадлежит ни одному
потоку и обрабатывается обычным образом.
В net.box поток – это объект над соединением,
который реализует те же методы, но обеспечивает последовательное выполнение запросов.
Идентификатор генерируется автоматически на стороне клиента. Если
пользователь пишет собственный коннектор и хочет использовать потоки,
ему необходимо передавать stream_id через протокол IPROTO.
В отличие от потока выполнения (thread), который связан с многозадачностью и выполнением внутри программы, поток (stream) в контексте протокола передает данные между клиентом и сервером.
Интерактивная транзакция – это транзакция, которую не нужно
отправлять в одном запросе. Существует несколько способов начать,
зафиксировать и откатить транзакцию, и их можно комбинировать. Можно
использовать stream:begin(), stream:commit(), stream:rollback() или
соответствующие методы потока – call, eval или execute – с использованием синтаксиса транзакций SQL.
Создадим Lua-клиент (client.lua) и запустим его с помощью Tarantool:
local net_box = require 'net.box'local conn = net_box.connect('127.0.0.1:3301')local conn_tickets = conn.space.ticketslocal yaml = require 'yaml'local stream = conn:new_stream()local stream_tickets = stream.space.tickets-- Begin transaction over an iproto stream:stream:begin()print("Replaced in a stream\n".. yaml.encode( stream_tickets:replace({1, 768}) ))-- Empty select, the transaction was not committed.-- You can't see it from the requests that do not belong to the-- transaction.print("Selected from outside of transaction\n".. yaml.encode(conn_tickets:select({}, {limit = 10}) ))-- Select returns the previously inserted tuple-- because this select belongs to the transaction:print("Selected from within transaction\n".. yaml.encode(stream_tickets:select({}, {limit = 10}) ))-- Commit transaction:stream:commit()-- Now this select also returns the tuple because the transaction has been committed:print("Selected again from outside of transaction\n".. yaml.encode(conn_tickets:select({}, {limit = 10}) ))os.exit()
Затем вызовите его и увидите следующий вывод:
Replaced in a stream--- [1, 768]...Selected from outside of transaction---- [1, 429]- [2, 429]...Selected from within transaction---- [1, 768]- [2, 429]...Selected again from outside of transaction---- [1, 768]- [2, 429]...```