Привет, хабр. Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

Специально к старту нового набора на курс «Архитектор высоких нагрузок» я написал небольшой материал, которым с радостью делюсь с вами.



Введение


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

Версионники


Как я уже сказал, один из подходов основан на версионности. Его также называют оптимистичным подходом или MVCC (multiversion concurrency control). Фактически происходит хранение версий для активных транзакций.

Где хранятся версии


Реализация механизма зависит от базы данных. Приведу примеры некоторых из них.

PostgreSQL


Каждая транзакция характеризуется некоторым id-шником. Id-шники транзакций монотонно растут. У любой строчки есть 2 атрибута, которые представляют метаинформацию для обеспечения механизма: updated_by_id — id-шник транзакции, которая последней осуществила обновление этой записи и deleted_by_id — id-шник транзакции, которая удалила данную запись. Когда приходит новая транзакция, база данных определяет id-ники выполняемых на данный момент транзакций, изменения, внесенные этими транзакциями, будут в рамках пришедшей транзакции проигнорированы. Получается, что пришедшая транзакция работает как бы со своей версией данных. Старые версии данных хранятся там же, где и актуальные. Если приходит Update, то добавляется еще одна строка и эта запись становится активной. Также понятно, что для корректной работы этой схемы необходим «сборщик мусора»: если у какой-то записи стоит deleted_by_id = 100, а минимальный id-шник среди выполняемых в данный момент транзакций это 150, то такую запись необходимо удалить.

MySQL (движок InnoDB)


В базе данных MySQL хранится только актуальная версия. Когда приходит Update, правятся данные внутри файла с таблицей. После поправки данных предыдущая версия находится в журнале отката. В MySQL есть несколько журналов отката (они разные для insert'ов и update'ов). Если транзакции нужна предыдущая версия строки, система идет в undo log и восстанавливает нужную версию. Версия также определяется id-шником транзакции.

Oracle


Схема похожа на MySQL: в файле с данными хранятся актуальные версии, старые версии восстанавливаются благодаря undo log'у. Undo log в Oracle циклически перезаписывается. Поэтому возможна ситуация когда транзакции нужна очень старая версия записи, а в undo log'е ее уже нет. Если такая ситуация произошла, то транзакция завершится с ошибкой.

MS SQL


MS SQL допускает включение режима как версионника, так и блокировочника. Для включения режима версионника в настройках базы необходимо разрешить работу со snapshot'ами. В MS SQL существует системная таблица (tempdb), которая предназначена для хранения временных таблиц. Именно tempdb используется для хранения старых версий, тогда как в таблице хранится актуальные версии. Системный процесс мониторит, что в tempdb есть версии, на которые никто не ссылается, их можно удалять. Если транзакция долгоиграющая, то для нее будут сохраняться версии. Tempdb растет, достигает своего максимального размера, MS SQL выделяет немного места на диске. Если заканчивается и оно, то транзакции со snapshot isolation не могут выполняться. Если используется такой режим работы, необходимо мониторить долгие транзакции, потому что откат такой транзакции по времени может стоит столько же, сколько она работает, а может и несколько больше.

Конфликты


Такой подход называется оптимистичным, потому что мы надеемся, что в случае выполнения параллельных изменяющих данные транзакций конфликтов не будет. Что происходит в случае конфликта? Предположим, что транзакция T1 изменяет 10000 записей, а транзакция Т2 изменяет параллельно 1 запись. Если так получилось, что эта 1 запись входит в те 10000, то будет осуществлен откат одной из транзакций: если T1 выполнилась первой, то будет осуществлен откат T2, а иначе наоборот.

Механизм отката


Механизм отката транзакции в версионниках зависит от реализации. Например, в PostgreSQL траназкция помечается как откаченная и vacuum освобождает место на диске. Такой механизм работает достаточно быстро. В Oracle же для осуществления отката происходит восстановление данных из undo log'а. В этом случае время работы увеличивается, однако это все еще намного быстрее чем в блокировочниках. MySQL работает по той же схеме, что и Oracle.

Заключение


Оптимистичный подход хорош тем, что писатель не блокирует читателя, читатель просто вычитывает свою версию данных. Поэтому версионность выгодна, если основная нагрузка идет на чтение, а не на запись (блог, отчетность и другие случаи, когда необходимо вычитывать часто и много).



Узнать о курсе подробнее можно тут