Транзакции: ACID, изоляция, MVCC

Рейтинг: 55.2% · 12 голосов
Исчерпывающий курс по Tarantool 3.x: модель данных, движки memtx и vinyl, Lua и файберы, транзакции и MVCC, SQL, конфигурация (box.cfg и декларативная 3.x), репликация и Raft, шардирование vshard, эксплуатация, безопасность. 47 уроков со схемами.
Ответить
Аватара пользователя
denis_tnt
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Транзакции: ACID, изоляция, MVCC

Сообщение denis_tnt »

Оглавление курса (47)
  1. Что такое Tarantool: in-memory СУБД и сервер приложений
  2. Архитектура изнутри: процесс, потоки, event-loop
  3. Установка и первый запуск: tt CLI, пакеты, Docker
  4. Интерактив: консоль, admin-консоль, первые команды
  5. Спейсы и кортежи: форматы, типы данных
  6. Типы индексов и их применимость
  7. Движки хранения: memtx vs vinyl
  8. DDL: схема, создание спейсов и индексов, миграции
  9. DML и выборки: insert/update/upsert, итераторы
  10. Персистентность: WAL, снапшоты, recovery
  11. Внутренности memtx: аллокаторы slab/arena, память
  12. Внутренности vinyl: LSM, компакция, тюнинг
  13. Lua и LuaJIT в Tarantool: box, модули, rocks
  14. Файберы: кооперативная многозадачность, каналы
  15. Транзакции: ACID, изоляция, MVCC (вы здесь)
  16. Хранимые процедуры, модули, организация приложения
  17. net.box: удалённые вызовы, async
  18. Пулы соединений, балансировка, реконнект
  19. Ошибки и диагностика: box.error, pcall
  20. Типы и сериализация: MsgPack, decimal, datetime, uuid
  21. SQL в Tarantool: возможности и связь с box
  22. SQL: таблицы, JOIN, подзапросы, представления
  23. SQL: подготовленные выражения, транзакции, Lua-интероп
  24. Классическая конфигурация box.cfg (legacy 1.x/2.x)
  25. Декларативная конфигурация 3.x: config.yaml, иерархия
  26. Роли и приложения в 3.x
  27. Централизованная конфигурация: etcd / config storage
  28. tt CLI глубоко: разработка, сборка, запуск
  29. Cartridge (официальный legacy) и миграция на 3.x
  30. Репликация: replicaset, топологии
  31. Механика репликации: WAL-стриминг, vclock
  32. Синхронная репликация и выборы лидера (Raft)
  33. Жизненный цикл узла: bootstrap, join, rejoin
  34. vshard: router/storage, виртуальные бакеты
  35. Решардинг и rebalancing бакетов
  36. Запросы поверх шардов: map-reduce, crud
  37. Мониторинг: метрики, Prometheus, Grafana
  38. Логирование и аудит
  39. Бэкапы и восстановление
  40. Безопасность: аутентификация, RBAC, TLS
  41. Производительность: профилирование, тюнинг
  42. Обновления: схема, rolling upgrade
  43. Деплой в продакшен: Docker, топология (официальные паттерны)
  44. Администрирование через официальный TCM (Tarantool Cluster Manager)
  45. Коннекторы: Python, Go, Java
  46. Ключевые модули (rocks): crud, metrics, queue, expirationd
  47. Capstone: шардированный отказоустойчивый кластер
Обзор

Транзакция в Tarantool - это группа операций над данными, которая применяется как единое целое: либо вся целиком (commit), либо никак (rollback). Транзакционная модель удовлетворяет свойствам ACID: атомарность, согласованность, изоляция, долговечность. Ключевая особенность Tarantool - очень высокий уровень изоляции по умолчанию: каждая транзакция видит согласованное состояние базы и фиксируется атомарно, поэтому базовым уровнем изоляции считается serializable (сериализуемость) - самый строгий из стандартных.

Есть два режима поведения транзакций:
  • Default - быстрые монопольные атомарные транзакции без конкуренции. Включён всегда, пока не активирован менеджер транзакций.
  • MVCC - многоверсионный режим для длинных конкурентных транзакций, разрешает yield (передачу управления) внутри транзакции в движке memtx.
Механика: один поток, файберы и WAL

Чтобы понять транзакции, надо понять, кто их выполняет. Запрос проходит через три потока ОС: сетевой поток (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-операция в конфликтной транзакции вернёт ошибку, пока её не откатят.
Важная деталь о снимке: snapshot в Tarantool не классический. Он не обязательно привязан к моменту начала транзакции. Менеджер конфликтов сам решает, какой и когда снимок дать транзакции, что позволяет избежать части конфликтов по сравнению с классической snapshot isolation.

Уровни изоляции в MVCC

Код: Выделить всё

Уровень            Что видит                          Где применяется
-----------------  ---------------------------------  ----------------------
read-committed     транзакции, начавшие commit         write-транзакции
read-confirmed     транзакции, завершившие commit       read-транзакции
                   (данные на диске/репликах)
best-effort        write -> read-committed              по умолчанию
(default)          read  -> read-confirmed
linearizable       гарантирует чтение последней         только на begin(),
                   подтверждённой записи                синхронные спейсы
best-effort выбирает уровень на первой операции транзакции (анализировать всю транзакцию заранее MVCC не может) и стремится к serializable с минимумом конфликтов. linearizable нельзя задать как default - только в конкретном box.begin(), и он применим к синхронным, локальным или временным memtx-спейсам.

Ключевые команды и код

Код: Выделить всё

-- Включение 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()
В декларативной конфигурации 3.x MVCC включается в YAML-секции:

Код: Выделить всё

# config.yaml (трек 3.x)
groups:
  group001:
    replicasets:
      replicaset001:
        instances:
          instance001:
            memtx:
              use_mvcc_engine: true
            txn_isolation: 'best-effort'
В SQL транзакции управляются стандартно:

Код: Выделить всё

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
Задание: перезапустите инстанс с memtx_use_mvcc_engine = false и выполните тот же box.atomic. Зафиксируйте текст ошибки и объясните, почему в default тот же код падает, а в MVCC работает.

Контрольные вопросы
  • Сколько TX-потоков в одном инстансе Tarantool и почему это устраняет классические race condition между потоками?
  • В какой момент жизненного цикла транзакции происходит запись в WAL и yield файбера, и какое свойство ACID это обеспечивает?
  • Из каких двух частей состоит менеджер транзакций и что такое read view?
  • Почему best-effort выбирает read-committed для пишущих и read-confirmed для читающих транзакций, и к какому итоговому уровню изоляции это приводит?
👍5 ❤️1 🔥4 😄 🤔
Ответить
← Предыдущая глава
Файберы: кооперативная многозадачность, каналы
Следующая глава →
Хранимые процедуры, модули, организация приложения

Все главы курса «Tarantool: in-memory СУБД и сервер приложений с нуля до продакшена»

Поделиться темой: ✈ Telegram VK
  • Похожие темы

Вернуться в «Tarantool: СУБД и сервер приложений»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость