Транзакция в Tarantool - это группа операций над данными, которая применяется как единое целое: либо вся целиком (commit), либо никак (rollback). Транзакционная модель удовлетворяет свойствам ACID: атомарность, согласованность, изоляция, долговечность. Ключевая особенность Tarantool - очень высокий уровень изоляции по умолчанию: каждая транзакция видит согласованное состояние базы и фиксируется атомарно, поэтому базовым уровнем изоляции считается serializable (сериализуемость) - самый строгий из стандартных.
Есть два режима поведения транзакций:
- Default - быстрые монопольные атомарные транзакции без конкуренции. Включён всегда, пока не активирован менеджер транзакций.
- MVCC - многоверсионный режим для длинных конкурентных транзакций, разрешает yield (передачу управления) внутри транзакции в движке memtx.
Чтобы понять транзакции, надо понять, кто их выполняет. Запрос проходит через три потока ОС: сетевой поток (iproto) парсит запрос, поток обработки транзакций (TX thread) выполняет саму логику над индексами и кортежами, и WAL-поток пишет изменения на диск. Важнейший факт: TX-поток в инстансе ровно один. Параллельного доступа к данным двумя потоками не бывает - один читает строку x, другой пишет строку y одновременно, как в классических СУБД, тут не происходит.
Внутри единственного TX-потока работают файберы - кооперативные сопрограммы. Планировщик кооперативный: файбер выполняется до точки yield, затем управление передаётся другому файберу. Именно yield - центральное понятие транзакций. В режиме default любая операция изменения (replace, insert, update) не делает yield, а yield случается только на commit. Поэтому многооператорная транзакция в default не прерывается посередине и не может быть оборвана конкурентом.
Жизненный цикл выглядит так: box.begin() открывает транзакцию, операции накапливают изменения в памяти, box.commit() формирует одну запись в WAL (батчем, в определённом порядке), приостанавливает файбер и ждёт ответа от WAL-потока. Когда WAL подтвердил запись на диск (это и есть durability), файбер просыпается, и результат уходит клиенту. box.rollback() откатывает изменения полностью, а rollback_to_savepoint() - до именованной точки.
В режиме default yield внутри memtx-транзакции = аборт. Сообщение "Transaction has been aborted by a fiber yield" означает, что вы вызвали что-то уступающее управление (сетевой запрос, fiber.sleep, fiber.yield) между begin и commit.

Жизненный цикл транзакции и MVCC в memtx
Как устроен MVCC внутри memtx
Менеджер транзакций включается опцией memtx_use_mvcc_engine и состоит из двух частей.
- MVCC-движок хранит все версии изменений всех транзакций. Для каждой транзакции он строит её собственный взгляд на состояние базы (transaction view), а при необходимости создаёт read view - зафиксированный снимок состояния, который уже не меняется другими транзакциями. Благодаря этому файбер может уступать управление (yield) внутри транзакции, читая консистентные данные, не блокируя других.
- Менеджер конфликтов отслеживает изменения транзакций и проверяет, можно ли выстроить их в корректный порядок сериализации. Если транзакция T1 коммитится и нарушает сериализуемость для T2, менеджер либо переводит T2 на read view, либо помечает её как "conflicted". Начиная с 2.10.1 конфликт детектируется сразу после коммита первой из конфликтующих транзакций: любая последующая CRUD-операция в конфликтной транзакции вернёт ошибку, пока её не откатят.
Уровни изоляции в MVCC
Код: Выделить всё
Уровень Что видит Где применяется
----------------- --------------------------------- ----------------------
read-committed транзакции, начавшие commit write-транзакции
read-confirmed транзакции, завершившие commit read-транзакции
(данные на диске/репликах)
best-effort write -> read-committed по умолчанию
(default) read -> read-confirmed
linearizable гарантирует чтение последней только на begin(),
подтверждённой записи синхронные спейсы
Ключевые команды и код
Код: Выделить всё
-- Включение MVCC и уровня по умолчанию (классический box.cfg для 1.x/2.x/3.x)
box.cfg{ memtx_use_mvcc_engine = true, txn_isolation = 'read-committed' }
-- Явная транзакция
box.begin()
box.space.accounts:update(1, {{'-', 2, 100}})
box.space.accounts:update(2, {{'+', 2, 100}})
box.commit() -- здесь происходит запись в WAL и yield
-- box.atomic: неявный begin в начале, commit при успехе, rollback при ошибке
box.atomic(function()
box.space.accounts:update(1, {{'-', 2, 100}})
box.space.accounts:update(2, {{'+', 2, 100}})
end)
-- Уровень изоляции для конкретной транзакции
box.begin({ txn_isolation = 'best-effort' })
-- Savepoint и частичный откат
box.begin()
box.space.log:insert{1, 'a'}
local sp = box.savepoint()
box.space.log:insert{2, 'b'}
box.rollback_to_savepoint(sp) -- откатит только вставку {2,'b'}
box.commit()
Код: Выделить всё
# config.yaml (трек 3.x)
groups:
group001:
replicasets:
replicaset001:
instances:
instance001:
memtx:
use_mvcc_engine: true
txn_isolation: 'best-effort'
Код: Выделить всё
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
- "MVCC даёт более высокий уровень изоляции." Наоборот: serializable есть и в default. MVCC нужен не ради изоляции, а чтобы разрешить yield и конкурентные длинные транзакции.
- Yield внутри транзакции в default. Любой сетевой вызов, fiber.sleep, явный yield между begin и commit = аборт. В default это особенно коварно, потому что часть изменений уже видна, а затем откатывается.
- Открытая транзакция при возврате из функции. Ошибка "Transaction is active at return from function" - забыли commit/rollback. Транзакция не должна переживать границу запроса (кроме interactive-транзакций через iproto-стримы).
- Игнор конфликтов. В MVCC при конкуренции транзакция может стать "conflicted" и откатиться. Прикладной код обязан ретраить такие транзакции; считать commit всегда успешным нельзя.
- Смешивание движков. Нельзя в одной транзакции работать с memtx и vinyl. У vinyl своя реализация MVCC.
- BITSET и RTREE. В MVCC эти индексы реально дают read-committed, а не serializable - возможны аномалии.
Запустите инстанс с включённым MVCC и проверьте разницу с default:
Код: Выделить всё
box.cfg{ memtx_use_mvcc_engine = true }
local s = box.schema.create_space('t', {if_not_exists=true})
s:create_index('pk', {if_not_exists=true})
local fiber = require('fiber')
-- С MVCC=true транзакция с yield проходит успешно:
box.atomic(function()
s:replace{1, 'a'}
fiber.yield() -- yield внутри транзакции
s:replace{2, 'b'}
end)
print(#s:select()) -- ожидаем 2
Контрольные вопросы
- Сколько TX-потоков в одном инстансе Tarantool и почему это устраняет классические race condition между потоками?
- В какой момент жизненного цикла транзакции происходит запись в WAL и yield файбера, и какое свойство ACID это обеспечивает?
- Из каких двух частей состоит менеджер транзакций и что такое read view?
- Почему best-effort выбирает read-committed для пишущих и read-confirmed для читающих транзакций, и к какому итоговому уровню изоляции это приводит?