Статьи

Уровни изоляции в терминах MVCC, часть 2: индексы и латентность

Привет еще раз, бесстрашные читатели. Этот пост станет продолжением начатой здесь дискуссии., Я продолжу рассуждать об уровнях изоляции в виде конструкции SQL и о том, как понимать их в распределенной базе данных MVCC, такой как NuoDB. NB: этот пост, как и предыдущий, будет использовать графическое представление параллельных временных шкал. Первый столбец — транзакция1, которая работает на том же уровне изоляции, что и три других столбца. Каждый из оставшихся столбцов демонстрирует поведение трех поддерживаемых уровней изоляции в NuoDB. Способ чтения этих временных шкал заключается в том, что время течет сверху вниз и слева направо (то есть в данном горизонтальном срезе действия в транзакции 1 выполнялись до транзакции 2). Все эти примеры выполняются для образца БД, инициализированного с помощью:

SQL> create table t1 (f1 integer);
SQL> insert into t1 values (1), (3), (5), (7), (9);
SQL> select * from t1;
 F1  
 --- 
  1  
  3  
  5  
  7  
  9  

Обновите Obscura и проиндексированные обновления

Без дальнейших церемоний, давайте разберемся с примерами.

Пример 1 , ВЫБРАТЬ ДЛЯ ОБНОВЛЕНИЯ

Выбор для обновления — интересная конструкция SQL. Выглядит так, как будто это чтение (в конце концов, оно содержит «SELECT») и возвращает набор результатов. Однако SELECT FOR UPDATE фактически является формой записи. По сути, SELECT FOR UPDATE записывает каждую строку, соответствующую предложению WHERE, но перезаписывает его точным значением, которое оно имело ранее. Затем он возвращает перезаписанные строки как набор результатов. Поскольку стандарт требует, чтобы эффективные «блокировки» на строках удерживались до фиксации транзакции, SELECT FOR UPDATE полезен, когда пользователям необходимо выполнить собственную двухфазную блокировку. Давайте рассмотрим пример SELECT FOR UPDATE в действии:

Выбрать для обновления

Здесь мы видим, что все три уровня изоляции будут блокироваться для выбора для обновления, однако все они имеют различное поведение.

READ_COMMITTED блокирует, потому что не может решить, какую версию записи записать, пока не закончится Txn 1

CONSISTENT_READ блокирует, потому что он обнаружил одновременное обновление и не знает, завершится ли сбой или обновление, пока txn 1 не закончил

WRITE_COMMITTED блокируется, потому что он просто ожидает установки новой версии записи поверх записи версии Txn 1

Вот где обоснование WRITE_COMMITTED может быть интуитивно понятно . WRITE_COMMITTED позволяет выполнять последовательную семантику коммутативных записей более эффективно, чем другие уровни изоляции. Рассуждения здесь аналогичны рассуждениям о существовании ПОСЛЕДОВАТЕЛЬНОСТИ. Если я, как разработчик приложения, знаю, что обновление (или набор обновлений) коммутирует с обновлениями, которые другие транзакции будут выполнять для той же таблицы, то WRITE_COMMITTEDэто единственный уровень изоляции, который позволяет всем обновлениям транзакции накапливаться одновременно. Осложняющим фактором является то, что строгая семантика SQL требует, чтобы UPDATE (или DELETE) не мог вернуть, пока его изменения не будут согласованы. Следовательно, улучшение производительности будет на уровне единого оператора. В настоящее время это также относится к хранимым процедурам, однако здесь существует большая семантическая гибкость.

Пример 2. Индексированные обновления

Некоторые из вас, возможно, заметили, что все примеры были выполнены для таблицы без индексов. Это означает, что выбор записей является неточным. Несмотря на то, что UPDATE может потребоваться изменить одну строку, транзакция не имеет другого выбора, кроме как сканировать каждую строку в таблице, чтобы найти все совпадения. Конечно, именно это и должно помочь индексам. Давайте посмотрим, что (если что-нибудь) изменится, если мы прикрепим индекс к таблице:

> CREATE INDEX idx1 ON t1(f1);

 Обновление с индексом

В этом примере транзакция 2 не должна была блокироваться для фиксации транзакции 1, потому что индекс позволил обоим запросам на обновление выбрать точно совпадающие записи. Поскольку наборы записей для обновлений Txn 1 и Txn 2 не перекрываются, конфликта записи не существует, поэтому обе транзакции могут продолжаться. Важно отметить, что неблокирующее и блокирующее поведение соответствуют определению READ_COMMITTED . Разница в том, что неблокирующее поведение — это повышение производительности, обеспечиваемое индексами. Если бы обе транзакции 1 и 2 пытались обновить одну и ту же версию записи (независимо от точности выбора набора записей), все равно возник бы конфликт обновления.

Что это значит? CONSISTENT_READ (изоляция моментального снимка) обеспечивает точность по определению, поэтому индексы не ускоряют запись туда. Но если ваше приложение использует READ_COMMITTED или WRITE_COMMITTED , вы можете уменьшить конкуренцию, добавив индекс, который увеличивает точность выбора набора для записи. Это хорошее эмпирическое правило, которое необходимо учитывать при диагностике проблем с производительностью.

Производительность и задержка:

NuoDB — это распределенная база данных. Это здорово, потому что это означает, что NuoDB может масштабироваться и адаптироваться так, как это не может сделать традиционное решение с одним узлом. Но это также означает, что разработчики систем и разработчики приложений теперь должны учитывать задержку, когда думают о характеристиках производительности своего приложения.

Читает :
CONSISTENT_READ и WRITE_COMMITTED как сделать СНАПШОТ версий для обычного ЗЕЬЕСТА. Следовательно, чувствительность к задержке для этих уровней изоляции отсутствует. Таким образом, независимо от того, насколько медленным является соединение между двумя половинами базы данных, читатели по обе стороны от ссылки не будут испытывать снижение производительности из-за задержки.

Интуиция такого поведения заключается в том, что это является прямым следствием релятивистской логики распределенной системы. В NuoDB транзакционный контроль является локальным для одного узла. Поэтому, когда транзакция начинается на TE, набор транзакций «произошло до» на этом узле точно известен. Это именно тот набор транзакций, который, как известно, совершается на этом узле в данный момент. Следовательно, не имеет значения, что такое «глобальный» порядок (и, следовательно, нет необходимости в атомных часах), любая транзакция, о которой еще не известно, что она зафиксирована, затем считается выполняющейся одновременно, и вычисления видимости выполняются соответствующим образом.

READ_COMMITTED — это отдельная история. Поскольку определение уровня изоляции заключается в чтении только самых последних зафиксированных версий записи, поведение чтения READ_COMMITTED зависит от времени сообщений фиксации, которые поступают в TE, обрабатывающий транзакцию READ_COMMITTED . Конечно, никто, пишущий приложение READ_COMMITTED, не может ожидать, что любые два выполнения одного и того же SELECT вернут одно и то же значение (согласно стандарту SQL). В зависимости от семантики приложения, READ_COMMITTEDможет связывать время завершения локальной транзакции с временем завершения удаленной транзакции. Любой данный SELECT всегда будет возвращать самые последние зафиксированные версии, но если локальная транзакция вращается в ожидании распространения удаленной фиксации, она будет страдать задержкой. Это ситуация, когда локальность данных определенно улучшит задержку транзакции.

Иными словами, READ_COMMITTED позволяет приложению выйти из строгой изоляции и видеть зафиксированные изменения по мере их публикации, но если приложение помещает транзакцию записи в одну сторону мира, а читатель — в другую, читатель получит ждать, пока изменения писателя распространятся по всему миру. В этом утверждении нет ничего радикального, это всего лишь напоминание тем разработчикам приложений, впервые окунувшимся в распределенную воду. Всегда уважайте местность вашего приложения.

Пишет :
Здесь уместно обсудить (кратко) способ, которым NuoDB выполняет обновления. Обновления выполняются оптимистично с асинхронной координацией. Данная таблица будет разбита на множество «атомов», которые являются независимыми распределенными объектами. Для каждой записи есть атом, в котором она находится. Для каждого атома есть выделенный узел, который мы называем «председатель». Председатель существует для разрыва связей, когда несколько узлов пытаются обновить одну и ту же версию записи. Когда узел обновляет версию записи:

  1. Транзакция оптимистично устанавливает новую версию с обновленным значением
  2. Транзакция передает обновление всем узлам (асинхронно)
  3. Председатель атома записи дважды проверяет это обновление на наличие конфликтов
  4. При конфликте отклонение отправляется на узел обновления
  5. В случае успеха подтверждение отправляется на узел обновления

Узел обновления продолжит обрабатывать набор записей для обновления, даже в ожидании подтверждения от председателя записи. Таким образом, массовые обновления чередуются наиболее эффективным способом. Этот оптимистический подход означает, что в общем случае (успешное обновление) больше не требуется никакой дополнительной работы, и обновленные версии уже передаются по сети всем партнерам. Следствием этого является то, что при неудачном обновлении узел обновления должен выполнить небольшую дополнительную работу и отправить сообщения возврата для отмены неудачного обновления.

Транзакции READ_COMMITTED и WRITE_COMMITTED будут блокироваться при любом конфликте обновлений, происходящем с таблицей (одно или несколько конфликтующих обновлений строк). Это предотвращает появление фантомов и других уродливых аномалий. Однако это означает, что если некоторые записи обновляются глобально, READ_COMMITTED и WRITE_COMMITTEDтранзакции будут навязывать своего рода глобальный 2PL (двухфазная блокировка) для всего. Если строка обновляется транзакцией, расположенной на другом конце канала с высокой задержкой, это означает, что эти обновления будут иметь время выполнения, которое будет увеличиваться с этой задержкой. Как мы видели выше, если каждое обновление не имеет или имеет очень низкую вероятность совпадения с другими транзакциями, использование индекса, чтобы сделать набор записи настолько конкретным, насколько это возможно, может уменьшить или устранить блокировку из-за ложных конфликтов.

CONSISTENT_READ — это изоляция моментального снимка, которую можно использовать так, что только если две транзакции обновляют одну и ту же версию записи, им придется ждать друг друга. Из-за того, как обновления координируются через председателей, возможно, что обновление в одной строке может ожидать получения сообщения об обновлении в обоих направлениях. Тем не менее, массовые обновления используют асинхронность и отправляют все запросы на обновление асинхронно, так что обновление N-строки не потребует N последовательных циклов, а обмен сообщениями, координируемый с председателем, будет частью потока версий обновления, которые имеют распространять на всех сверстников в любом случае.

Здесь рекомендации по производительности интуитивно понятны. Прекрасно иметь таблицу, доступ к которой осуществляется по всему миру. Однако избегайте регулярного обновления определенных строк в глобальном масштабе. Вообще, глобально мутирующее глобальное состояние — это хитрое предложение (вдвойне в распределенных системах). Большинство вещей в конечном итоге имеют вид естественной местности, которая предотвращает возникновение проблемы. Например, можно представить глобальную таблицу «CUSTOMERS» с миллиардами записей. Однако маловероятно, что какое-либо реалистичное приложение будет одновременно обновлять одну и ту же клиентскую строку в Европе, Азии и Северной Америке. Даже если есть несколько долгосрочных аналитических запросов, которые работают по всей таблице, если они выполняются в CONSISTENT_READ,они никогда не задержат ОБНОВЛЕНИЯ на любом уровне изоляции. Время от времени может быть причина для обновления строки глобально, но приложение, которое делает это постоянно и с высокой частотой, будет плохо работать в любом распределенном контексте. Чтобы продолжить этот пример, поскольку каждый TE постоянно удаляет неиспользуемые копии атомов из своей памяти, пока приложение демонстрирует некоторую локальность, NuoDB автоматически начнет кэширование данных таблицы, чтобы отразить эту локальность. Это означает, что разработчик приложения не должен мучиться с созданием пользовательского уровня кэширования, естественная локальность приложения будет отражена в поведении кэширования самого NuoDB (более подробную информацию см. В разделе Введение в кэш NuoDB , Memcached против NuoDB ).

Надеюсь, это помогло заполнить некоторые пробелы, оставшиеся после предыдущего поста, и дало моим преданным читателям некоторую интуицию о том, как рассуждать об уровнях изоляции в NuoDB. Я надеюсь, что вы отошли от этого с некоторыми практическими правилами, которые помогут вам рассуждать о вашем приложении базы данных, поскольку оно работает на NuoDB.