Транзакции
Транзакция — это последовательность действий, которые мы хотим выполнить с базой данных как единую атомарную операцию. Отдельная транзакция может включать в себя комбинацию чтения, создания, обновления и удаления данных.
В MySQL и PostgreSQL мы начинаем новую транзакцию с помощью команды begin;start и завершаем её командой commit;. Между этими двумя командами можно выполнить любое количество SQL-запросов для поиска и обработки данных.
В некоторых ситуациях транзакции не фиксируются. Иногда это происходит из-за непредвиденных событий в физическом мире, таких как сбой жесткого диска или отключение электроэнергии. Базы данных, такие как MySQL и Postgres, разработаны для корректной обработки многих из этих непредвиденных сценариев с использованием методов аварийного восстановления. Postgres, например, обрабатывает это с помощью механизма WAL(write-ahead log) - журнал предзаписи.
Бывают также ситуации, когда нам необходимо намеренно отменить частично выполненную транзакцию. Это происходит, когда в середине транзакции мы сталкиваемся с отсутствующими/неожиданными данными или получаем запрос на отмену от клиента. Для этого базы данных поддерживают соответствующую rollback;команду.
Одним из полезных ключевых свойств транзакций заключается в возможности одновременного выполнения множества запросов без взаимного влияния. Ниже представлен сценарий с двумя различными сессиями, подключенными к одной и той же базе данных. Сессия A начинает транзакцию, выбирает данные, обновляет их, снова выбирает данные и затем фиксирует транзакцию. Сессия B выбирает те же данные дважды во время транзакции и еще раз после завершения обеих транзакций.
Сессия B не видит обновления имени с benна joeдо тех пор, пока сессия A не подтвердит транзакцию.
Рассмотрим ту же последовательность событий, но вместо commit транзакции в сессии А, выполним `rollback.
Вторая сессия никогда не видит эффекта от изменений, внесенных в первой, из-за этого rollback. Это хороший переход к другой важной концепции в транзакциях: согласованные чтения.
Согласованное чтение
В процессе выполнения транзакции нам необходимо обеспечить согласованное представление базы данных. Это означает, что даже если другая транзакция одновременно добавляет, удаляет или обновляет информацию, наша транзакция должна получать собственное изолированное представление данных, не затронутое этими внешними изменениями, до момента подтверждения транзакции.
MySQL и Postgres поддерживают эту возможность при работе в REPEATABLE READсоответствующем режиме (а также во всех более строгих режимах). Однако для достижения этой цели они используют разные подходы.
PostgreSQL решает эту проблему с помощью многоверсионного хранения строк . Каждый раз, когда строка вставляется или обновляется, создается новая строка вместе с метаданными, чтобы отслеживать, какие транзакции могут получить доступ к новой версии. MySQL делает это с помощью журнала отмены(undo log). Изменения в строках немедленно перезаписывают старые версии, но запись об изменениях сохраняется в файле журнала на случай, если их потребуется восстановить.
Версионирование нескольких строк в PostgreSQL
Что произошло:
beginначинает новую транзакцию- Вносятся изменения в данные пользователя с ID
4, имя меняется с “liz” на “aly”. Это приводит к созданию новой версии строки, при этом предыдущая версия сохраняется. - В старой версии строки значение было
xmaxустановлено на 10 (xmax = максимальный идентификатор транзакции). - В новой версии строки также было
xminустановлено значение 10 (xmin = минимальный идентификатор транзакции). - Транзакция подтверждается, делая обновление видимым для всей базы данных.
Но теперь у нас есть две версии строки с ID = 4. Ключевое слово здесь — xminи xmax.
xmin в этом хранилище находится идентификатор транзакции, создавшей версию строки, а также xmaxидентификатор транзакции, вызвавшей создание заменяющей строки. Postgres использует эти данные для определения того, какую версию строки видит каждая транзакция.
Давайте посмотрим на следующее видео:
До подтверждения транзакции сессия B не видела изменений, внесенных сессией A. Она видела имя «liz», в то время как сессия A видела «aly» в рамках транзакции. На этом этапе это не связано с xminдругими xmaxтранзакциями, а скорее с тем, что другие транзакции не видят незафиксированные данные. После подтверждения транзакции сессией A сессия B теперь может видеть новое имя «aly», поскольку данные зафиксированы, а идентификатор транзакции больше 10.
Если же транзакция получает ошибку rollback, эти изменения строк не применяются и база данных остается в состоянии, как если бы транзакция никогда и не начиналась.
Это простой сценарий. Только одна из транзакций изменяет данные. Сессия B только выполняет select операторы! Когда обе транзакции одновременно изменяют данные, каждая из них сможет «видеть» внесенные изменения, но эти изменения не будут передаваться другим транзакциям до момента фиксации. Вот пример, где каждая транзакция выбирает данные, обновляет данные, снова выбирает данные, фиксирует изменения, и, наконец, обе выполняют окончательный выбор.
Параллельные транзакции не могут видеть изменения друг друга до тех пор, пока данные не будут зафиксированы. Те же механизмы используются для управления видимостью данных при наличии сотен одновременных транзакций в загруженных базах данных Postgres.
Прежде чем перейти к MySQL, еще одно важное замечание. Что происходит со всеми этими дублирующимися строками? Со временем у нас могут накопиться тысячи дубликатов строк, которые больше не нужны. PostgreSQL использует несколько способов для решения этой проблемы, но я сосредоточусь на VACUUM FULLкоманде. При выполнении она удаляет версии строк, которые настолько устарели, что мы знаем, что в дальнейшем они не понадобятся для транзакций. При этом происходит сжатие таблицы.
Обратите внимание, что при vacuum full выполнении команды все неиспользуемые строки удаляются, а пробелы в таблице сжимаются, освобождая неиспользуемое пространство.
Журнал отмены в MySQL
MySQL обеспечивает согласованное поведение при чтении, используя другой подход. Вместо хранения множества копий каждой строки, MySQL немедленно перезаписывает старые данные новой строкой при их изменении. Это означает, что со временем требуется меньше обслуживания строк (другими словами, нам не нужно выполнять очистку, как в Postgres).
Однако MySQL по-прежнему нуждается в возможности отображать разные версии строки для разных транзакций. Для этого MySQL использует журнал отмены (undo log ) — журнал недавно внесенных изменений в строку, позволяющий транзакции восстанавливать прошлые версии на лету.
Обратите внимание, что каждая строка MySQL имеет два столбца метаданных (выделены синим цветом). В них хранится идентификатор транзакции, которая обновила строку последней ( xid), и ссылка на последнее изменение в журнале отмены ( ptr).
При одновременном выполнении нескольких транзакций транзакция A может перезаписать версию строки, необходимую транзакции B. Транзакция B может увидеть предыдущие версии строки, проверив журнал отмены, который хранит старые значения до тех пор, пока любая выполняющаяся транзакция может нуждаться в их просмотре.
В журнале отмены транзакций может одновременно находиться несколько записей об отмене для одной и той же строки. В таком случае MySQL выберет правильную версию на основе идентификаторов транзакций.
Уровни изоляции
Концепция повторяющихся чтений важна для баз данных, но это лишь один из нескольких уровней изоляции, поддерживаемых такими базами данных, как MySQL и Postgres. Этот параметр определяет, насколько «защищена» каждая транзакция от доступа к данным, которые изменяются другими одновременными транзакциями. Настройка этого параметра позволяет пользователю контролировать компромисс между изоляцией и производительностью.
Как MySQL, так и Postgres имеют четыре уровня изоляции: от самого сильного к самому слабому — это: Serializable , Repeatable Read , Read Committed , Read Uncommitted.
Более высокий уровень изоляции обеспечивает лучшую защиту от проблем несогласованности данных в транзакциях, но в некоторых сценариях это приводит к снижению производительности.
Режим Serializable является самым надежным. В этом режиме все транзакции ведут себя так, как если бы они выполнялись в четко определенной последовательности, даже если в действительности многие из них выполняются одновременно. Это достигается за счет сложных механизмов блокировки и ожидания.
Остальные три постепенно смягчают строгие правила и могут быть описаны по нежелательным явлениям, которые они допускают или запрещают.
Фантомное чтение
Фантомное чтение — это ситуация, когда транзакция выполняется несколько SELECT раз, но при повторном выполнении выдает разные результаты. Обычно это происходит из-за того, что данные были вставлены и зафиксированы другой транзакцией. Приведенная ниже временная шкала визуализирует такой сценарий. Горизонтальная ось отображает течение времени в базе данных с двумя клиентами.
После serializable следующим наименее строгим уровнем изоляции является repeatable read . В соответствии со стандартом SQL, уровень repeatable read допускает фантомные чтения, хотя в Postgres они по-прежнему невозможны.
Неповторяемые произведения
Это происходит, когда транзакция считывает строку, а затем повторно считывает ту же строку, обнаруживая изменения, внесенные другой уже зафиксированной транзакцией. Это опасно, потому что мы могли уже сделать предположения о состоянии нашей базы данных, но эти данные изменились прямо у нас под ногами.
Уровень изоляции «read commited» , следующий после «repeatable read» , допускает как такие, так и фантомные чтения. Компромисс заключается в немного лучшей производительности транзакций базы данных.
Грязное чтение(dirty reads)
Последний и, пожалуй, самый худший вариант — это «грязное чтение». «Грязное чтение» — это ситуация, когда транзакция видит данные, записанные другой транзакцией, выполняющейся одновременно, которые ещё не зафиксированы. Это действительно плохо! В большинстве случаев мы никогда не хотим видеть незафиксированные данные от других транзакций.
Самый слабый уровень изоляции, режим “read uncommited” , допускает “грязное чтение” и два других описанных выше режима. Это самый опасный, но и самый производительный режим.
Одновременная запись
Что если двум транзакциям необходимо одновременно изменить одну и ту же строку?
То, как именно это обрабатывается, зависит как от (A) системы баз данных, так и от (B) уровня изоляции. Для простоты обсуждения мы сосредоточимся на том, как это работает для самого строгого (SERIALIZABLE) уровня в Postgres и MySQL. И снова, две самые популярные в мире реляционные базы данных используют здесь совершенно разные подходы.
MySQL: Блокировка на уровне строк
Проще говоря, MySQL обрабатывает конфликтующие операции записи с помощью блокировок.
Блокировка — это программный механизм, позволяющий передать право собственности на фрагмент данных одной транзакции (или набору транзакций). Транзакции получают блокировку на строку, когда им необходимо «владеть» ею без прерывания работы. Когда транзакция завершает работу со строками, она снимает блокировку , чтобы разрешить доступ другим транзакциям.
Хотя на практике существует множество типов блокировок, два основных — это разделяемые блокировки(shared locks) и эксклюзивные блокировки(exclusive locks) .
Разделяемая блокировка(S) может быть получена несколькими транзакциями одновременно на одной и той же строке. Как правило, транзакции получают разделяемые блокировки на строку при чтении из нее, поскольку несколько транзакций могут делать это одновременно безопасно.
Эксклюзивная блокировка(X) может принадлежать только одной транзакции для любой заданной строки в любой момент времени. Когда транзакция запрашивает X-блокировку, никакие другие транзакции не могут иметь никаких блокировок на этой строке. Они используются, когда транзакции необходимо записать данные в строку, поскольку мы не хотим, чтобы две транзакции одновременно изменяли значения столбцов!
В SERIALIZABLE режиме все транзакции всегда должны получать X блокировок при обновлении строки. В большинстве случаев это работает нормально, за исключением накладных расходов на производительность, связанных с блокировками. В сценариях, когда две транзакции пытаются одновременно обновить одну и ту же строку, это может привести к ==взаимоблокировке(deadlock)==!
MySQL способен обнаруживать взаимоблокировки и прерывает одну из участвующих транзакций, чтобы позволить другой продолжить выполнение.
PostgreSQL: Сериализуемая изоляция снимков(Serializable Snapshot Isolation)
PostgreSQL обрабатывает конфликты записи в SERIALIZABLE режиме с меньшим количеством блокировок и полностью избегает проблемы взаимоблокировки.
При чтении и записи строк транзакциями Postgres создает предикатные блокировки(predicate locks), которые представляют собой “блокировки” на наборы строк, указанные предикатом. Например, если транзакция обновляет все строки с идентификаторами 10–20, она получит блокировку на предикате WHERE id BETWEEN 10 AND 20. Эти блокировки используются не для блокировки доступа к строкам, а для отслеживания того, какие строки используются какими транзакциями, и последующего обнаружения конфликтов данных в режиме реального времени(on-the-fly).
В сочетании с многострочным версионированием(MVCC) это позволяет Postgres использовать оптимистическое разрешение конфликтов. Он никогда не блокирует транзакции, ожидая получения блокировки, но прервет транзакцию, если обнаружит нарушение гарантий SERIALIZABLE.
Давайте рассмотрим аналогичную временную шкалу из примера с MySQL, но на этот раз обратим внимание на оптимистичный подход Postgres.
Разница визуально незначительна, но реализована она совершенно по-разному. И Postgres, и MySQL используют прерывание одной транзакции в пользу сохранения гарантий сериализации. Приложения должны учитывать такой исход и иметь логику повторных попыток для важных транзакций.
Заключение
Транзакции — это лишь небольшая часть всего удивительного инженерного потенциала баз данных! Но фундаментальное понимание того, что они собой представляют, как работают и какие гарантии обеспечивают четыре уровня изоляции, полезно для более эффективной работы с базами данных.
Перевод статьи Database Transactions