С двух сторон: может ли MVCC в MySQL разрешить фантомное чтение?

MySQL
  • ** Прежде чем сказать MVCC? Прежде всего, нам нужно популяризировать некоторые базовые знания о MySQL для тех братьев, которые мало что знают о MySQL, а затем увидеть конкретный контент MVCC.

Давайте посмотрим, что представляет собой MySQL MVCC, шаг за шагом. **

  • В MySQL только таблицы базы данных, которые используют механизм хранения InnoDB, поддерживают транзакции.
  • В случае транзакций его можно использовать для обеспечения целостности и непротиворечивости данных, а также для обеспечения того, чтобы пакеты операторов SQL выполнялись либо все, либо не все.
  • Транзакции используются для управления операторами вставки, обновления и удаления.

1. Четыре характеристики (ACID):

  • **Атомарность:** Все операции в транзакции (транзакции) либо завершены, либо не завершены, и не будут заканчиваться на определенной ссылке в середине. Если во время выполнения транзакции возникает ошибка, происходит ее откат (Rollback) до состояния до начала транзакции, как если бы транзакция никогда не выполнялась.
  • **Консистентность:** целостность базы данных не нарушается ни до начала транзакции, ни после ее завершения. Это означает, что записанные данные должны полностью соответствовать всем заданным правилам, включая точность данных, конкатенацию и последующую базу данных, которая может самопроизвольно выполнять заданную работу.
  • **Изоляция.** Способность базы данных разрешать нескольким параллельным транзакциям одновременно читать, записывать и изменять свои данные. Изоляция может предотвратить несогласованность данных из-за перекрестного выполнения, когда несколько транзакций выполняются одновременно. Изоляция транзакций делится на разные уровни, включая незафиксированное чтение (Read uncommitted), чтение зафиксированное (read commit), повторяемое чтение (repeatable read) и сериализуемое (Serializable).
  • **Сохраняемость:** После завершения транзакции изменение данных является постоянным и не будет потеряно даже в случае сбоя системы.
在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

2. Уровень изоляции

**

  • Чтение незафиксированных (Read uncommitted) транзакция может считывать данные, которые не были зафиксированы в других транзакциях. Произойдут грязные чтения, неповторяемые чтения и фантомные чтения.
  • Чтение зафиксировано (Read commit), транзакция может считывать только те данные, которые были зафиксированы в других транзакциях. Будет неповторяемость, феномен фантомного чтения.
  • Повторяемое чтение (Repeatable read), данные, запрашиваемые несколько раз в рамках одной и той же транзакции, являются согласованными. галлюцинации
  • Сериализуемый (Serializable) — это высокий уровень изоляции, он требует, чтобы блокировка чтения и блокировка записи для выбранного объекта оставались до тех пор, пока конец транзакции не будет освобожден, поэтому он может предотвратить все проблемы, но поскольку он сериализован, поэтому менее эффективен. .

3. Фантомное чтение, неповторяемое чтение, грязное чтение

Грязное чтение: когда транзакция считывает данные, которые еще не зафиксированы другими транзакциями, поскольку незафиксированные данные не обязательно являются окончательными действительными данными. Поэтому мы называем это чтением грязных данных. То есть грязное чтение. Неповторяющееся чтение: после того, как одна транзакция A считывает данные, другая транзакция B изменяет данные. В это время транзакция A снова запрашивает и обнаруживает, что данные отличаются. Это неповторяемое чтение. Также называется фантомным чтением. Фантомное чтение: также известное как «фантомное чтение», это особый сценарий «неповторяемого чтения»: когда транзакция 1 дважды выполняет «SELECT ... WHERE» в середине операции для извлечения данных в пределах определенный диапазон, транзакция 2. В этой таблице создается новая строка данных (например, [[INSERT]]), которая просто удовлетворяет условию «WHERE» транзакции 1.Примечание: это может немного сбить с толку.В общем, «неповторяющееся чтение» и «фантомное чтение» имеют примерно одно и то же значение. Просто на линии данных происходит неповторяемость, то есть происходит операция обновления, а потом данные считываются, и происходит неповторяемое чтение. Фантомное чтение происходит в таблице данных, то есть выполняются операции вставки и удаления. Перейдите к чтению этой таблицы еще раз, записи данных или количество строк (записей) отличаются. Это было похоже на галлюцинацию. **

4. MVCC (Multiversion Concurrency Control) управление многоверсионным параллелизмом.

Метод, используемый базой данных для разрешения конфликтов чтения и записи, целью которого является представление производительности пропускной способности в сценарии с высокой степенью параллелизма базы данных. Цепочка версий: Для таблицы, использующей механизм хранения InnoDB, ее запись кластеризованного индекса содержит два необходимых скрытых столбца (row_id не является обязательным, мы Если созданная таблица имеет первичный ключ или уникальный ключ, отличный от NULL, столбец row_id не будет включен): trx_id: каждый раз, когда запись изменяется, соответствующий идентификатор транзакции будет назначен скрытому столбцу trx_id. roll_pointer: каждый раз, когда запись изменяется, в этом скрытом столбце будет храниться указатель, который можно использовать для поиска записи. Запишите информацию перед внесением изменений. Например, сейчас есть такая таблица: t

ID Name
1 Сяо Ли

Давайте сначала предположим, что идентификатор транзакции новой записи равен 80, тогда список версий этой записи на данный момент выглядит следующим образом (поскольку это новое дополнение, roll_pointer, соответствующий этой цепочке версий, пуст):image.pngЕсли есть два идентификатора транзакции 100 и 200, для этой записи выполняется операция обновления. Конкретный поток выглядит следующим образом:image.png

贴心小课堂:
两个事务中不能交叉更新同一条记录哦?第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。

Каждый раз, когда мы изменяем запись данных, MySQL будет записывать журнал, который мы называем журналом отмены.Каждый журнал отмены также имеет атрибут roll_pointer (журнал отмены, соответствующий операции вставки, не имеет этого атрибута, поскольку запись не имеет этого атрибута. Более ранней версии нет), вы можете соединить эти журналы отмен, чтобы сформировать связанный список, поэтому текущая ситуация похожа на следующий рисунок:image.pngПосле каждого обновления этой записи старая запись будет помещена в журнал отмен, даже если это историческая версия записи, так как количество обновлений увеличивается снова и снова, все версии будут связаны в связанный список по Атрибут roll_pointer Мы называем этот связанный список [цепочка версий], а головной узел цепочки версий — это последнее значение текущей записи. Кроме того, каждая версия также содержит идентификатор транзакции, соответствующий сгенерированной версии, Этот идентификатор (идентификатор транзакции) очень важен, и принцип реализации уровня изоляции последующих транзакций основан на этом идентификаторе (идентификаторе транзакции).

ReadView

** Для транзакций, использующих уровень изоляции [read uncommitted READ_UNCOMMITTED], рекомендуется напрямую читать последнюю версию записи.Для транзакций, использующих уровень изоляции [serialized SERIALIZABLE], используйте блокировку для доступа к записи. Для транзакций, использующих уровень изоляции [read commit READ COMMITTED] и [repeatable read REPRATABLE_READ], нам нужно использовать [цепочку версий], о которой мы упоминали выше. Основная проблема заключается в следующем: нам нужно определить цепочку версий в данных, которая версия видна для текущей транзакции. Таким образом, дизайн MySQL официально предложил концепцию ReadView.Этот ReadView в основном содержит, какие активные транзакции чтения и записи в настоящее время находятся в MySQL, и помещает их идентификаторы транзакций в список.Мы называем этот список как m_ids (массив ). Таким образом, когда мы обращаемся к записи, нам нужно только выполнить следующие шаги, чтобы определить, видна ли версия записи (официальные правила дизайна):

  • Если значение атрибута trx_id доступной версии меньше наименьшего идентификатора транзакции в списке m_ids, это указывает на то, что транзакция, сгенерировавшая эту версию, была зафиксирована до создания ReadView, поэтому эта версия может быть доступна для текущей транзакции.

  • Если значение атрибута trx_id доступной версии больше, чем наибольший идентификатор транзакции в списке m_ids, это указывает на то, что транзакция, сгенерировавшая версию, была сгенерирована после того, как был сгенерирован ReadView, поэтому текущая транзакция не может получить доступ к версии. .

  • Если значение атрибута trx_id доступной версии находится между наибольшим идентификатором транзакции и наименьшим идентификатором транзакции в списке m_ids, необходимо определить, находится ли значение атрибута trx_id в списке m_ids.Если он все еще активен, версия не может быть доступным; если это не так, это означает, что транзакция, сгенерировавшая версию при создании ReadView, была зафиксирована, и к версии можно получить доступ.

Если данные определенной версии не видны для текущей транзакции, продолжайте находить запись данных следующей версии по цепочке версий и по-прежнему следуйте шагам, которые мы упоминали выше, чтобы определить, видны ли данные, и т. д. , до цепочки версий Последняя версия данных в , если последняя версия данных мне не видна, значит запись не видна транзакции, и результат запроса не содержит записи. В MySQL очень большая разница между уровнями изоляции READ COMMITTED (зафиксированное чтение) и REPEATABLE READ (повторяющееся чтение) заключается в том, что они генерируют ReadView в разное время Давайте рассмотрим конкретный пример.В соответствии с цепочкой версий, которую мы нарисовали выше, давайте проанализируем, как эта цепочка версий генерируется шаг за шагом, и как MySQL определяет, доступны ли данные для чтения (видимости) через цепочку версий, когда мы запрашиваем. --[1]--[R****EAD COMMITTED --- Генерировать ReadView перед каждым чтением данных]Предположим, что сейчас в системе выполняется транзакция с идентификатором 100:

# Transaction 100
BEGIN;
 
UPDATE t SET name = '小B' WHERE id = 1;
 
UPDATE t SET name = '小C' WHERE id = 1;
# 注意哦:我们这个事务,我并没有提交。没有commit指令哦
# Transaction 200
BEGIN;
 
# 更新了一些别的表的记录
...
贴心小课堂:事务执行过程中,只有在第一次真正修改记录时(比如使用INSERT、DELETE、UPDATE语句),才会被分配一个单独的事务id,这个事务id是递增的。

На данный момент список версий, полученный записью с id 1 в таблице t, выглядит следующим образом:image.pngОбратите внимание, у меня есть транзакция 100 выше, и я еще не отправил ее, я не выполнил команду фиксации. Предположим, что теперь есть транзакция, начинающаяся с уровня изоляции READ COMMITTED:

# 使用READ COMMITTED隔离级别的事务(读已提交)
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'

Поток выполнения этого SELECT1 выглядит следующим образом:

  • При выполнении оператора SELECT сначала будет сгенерирован ReadView, а содержимое списка массива m_ids ReadView равно [100,200].

  • Затем выберите видимые записи из цепочки версий.Как видно из рисунка, содержимое столбца имя последней версии — 'маленькая С', а значение trx_id этой версии — 100, что находится в списке m_ids , так что это не соответствует нашей видимости. просьба, перейти к следующей версии на основе roll_pointer.

  • Содержимое столбца имени следующей версии — 'маленькая Б', и значение trx_id этой версии тоже 100, что тоже есть в списке m_ids, поэтому не соответствует требованиям, и продолжает переходить к следующему версия.

  • Содержимое имени столбца следующей версии — «маленькая А», значение trx_id этой версии — 80, что меньше наименьшего идентификатора транзакции 100 в списке m_ids, поэтому эта версия соответствует требованиям, и возвращена окончательная версия. пользователю это имя столбца Records для «Small A».

После этого мы отправляем транзакцию, идентификатор транзакции которой равен 100, следующим образом:

# Transaction 100
BEGIN;
 
UPDATE t SET name = '小B' WHERE id = 1;
 
UPDATE t SET name = '小C' WHERE id = 1;
 
COMMIT;    //提交了哦

Затем обновите запись с идентификатором 1 в таблице t в транзакции с идентификатором транзакции 200:

# Transaction 200
BEGIN;
 
# 更新了一些别的表的记录
...
 
UPDATE t SET name = '小D' WHERE id = 1;
 
UPDATE t SET name = '小F' WHERE id = 1;

На данный момент цепочка версий записи с id 1 в таблице t выглядит так:image.pngЗатем перейдите к транзакции, которая только что использовала уровень изоляции READ COMMITTED, чтобы продолжить поиск записи с идентификатором 1 следующим образом:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
 
# SELECT1:Transaction 100、200均未提交的时候执行的查询
SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'
 
# SELECT2:Transaction 100提交,Transaction 200未提交的时候执行的查询
SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小C'

Процесс выполнения этого SELECT2 выглядит следующим образом:

  • При выполнении оператора SELECT сначала будет создан ReadView, а содержимое списка m_ids ReadView будет [200] (транзакция с идентификатором транзакции 100 была зафиксирована, поэтому она недоступна при создании моментального снимка). .

  • Затем выберите видимые записи из цепочки версий.Как видно из рисунка, содержимое столбца имени последней версии — «маленькая F». Значение trx_id этой версии равно 200, что находится в списке m_ids, поэтому он не соответствует требованиям видимости. Перейти к следующей версии на основе roll_pointer.

  • Содержимое имени столбца следующей версии — «маленькая D», а значение trx_id этой версии — 200, что тоже есть в списке m_ids, поэтому не соответствует требованиям, и продолжает переходить к следующей версии .

  • Содержимое имени столбца следующей версии — «маленький C», а значение trx_id этой версии — 100, что меньше наименьшего идентификатора транзакции id200 в списке m_ids, поэтому эта версия соответствует требованиям, а окончательная версия пользователю возвращается эта запись A, имя столбца которой — «маленькая C».

По аналогии, если запись с идентификатором транзакции 200 также отправляется позже, когда запись со значением идентификатора 1 в таблице t запрашивается в транзакции с использованием уровня изоляции READ COMMITTED, полученный результат будет «маленький F». Мы не будем анализировать процесс. Подведем итог: транзакция, использующая уровень изоляции READ COMMITTED, генерирует отдельный ReadView в начале каждого запроса.После того, как я закончил говорить, уровень изоляции [Прочитано и отправлено]. Я не знаю, понимаете ли вы? Если вы не понимаете, пожалуйста, свяжитесь со мной, и мы можем обсудить это вместе. Далее давайте посмотрим, как MVCC управляет видимостью данных, когда уровень изоляции транзакций [повторяемое чтение]. --[2]--****[ПОВТОРЯЕМОЕ ЧТЕНИЕ ---Создать ReadView при первом чтении данных]Для транзакций, использующих уровень изоляции REPEATABLE READ, при первом выполнении инструкции запроса создается только ReadView, и последующие запросы не будут генерироваться повторно. Давайте используем пример, чтобы увидеть, каков эффект. Например, сейчас в системе выполняются две транзакции с id 100 и 200:

# Transaction 100
BEGIN;
 
UPDATE t SET name = '小B' WHERE id = 1;
 
UPDATE t SET name = '小C' WHERE id = 1;
# Transaction 200
BEGIN;
 
# 更新了一些别的表的记录
...

На данный момент список версий, полученный записью с id 1 в таблице t, выглядит следующим образом:image.pngПредположим теперь, что транзакция с уровнем изоляции REPEATABLE READ начинает выполняться:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
 
# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'

Процесс выполнения этого SELECT1 выглядит следующим образом:

  • При выполнении оператора SELECT сначала будет создан ReadView, а содержимое списка m_ids ReadView будет [100, 200].

  • Затем выберите видимые записи из цепочки версий.Как видно из рисунка, содержимое столбца имени последней версии — «маленькая С». Значение trx_id этой версии равно 100, что находится в списке m_ids, поэтому он не соответствует требованиям видимости. Перейти к следующей версии на основе roll_pointer.

  • Содержимое имени столбца следующей версии — 'маленькая Б', и значение trx_id этой версии тоже 100, что тоже есть в списке m_ids, поэтому не соответствует требованиям, и продолжает переходить к следующему версия.

  • Содержимое имени столбца следующей версии — «маленькая А», значение trx_id этой версии — 80, что меньше наименьшего идентификатора транзакции 100 в списке m_ids, поэтому эта версия соответствует требованиям, и возвращена окончательная версия. пользователю это имя столбца Records для «Small A».

После этого мы фиксируем транзакцию с идентификатором транзакции 100, вот так:

# Transaction 100
BEGIN;
 
UPDATE t SET name = '小B' WHERE id = 1;
 
UPDATE t SET name = '小C' WHERE id = 1;
 
COMMIT;

Затем обновите запись с идентификатором 1 в таблице t в транзакции с идентификатором транзакции 200:

# Transaction 200
BEGIN;
 
# 更新了一些别的表的记录
...
 
UPDATE t SET name = '小D' WHERE id = 1;
 
UPDATE t SET name = '小F' WHERE id = 1;

На данный момент цепочка версий записи с id 1 в таблице t выглядит так:image.pngЗатем перейдите к транзакции, которая только что использовала уровень изоляции REPEATABLE READ, чтобы продолжить поиск записи с идентификатором 1 следующим образом:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
 
# SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 得到的列name的值为'小A'
 
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 得到的列name的值仍为'小A'

Процесс выполнения этого SELECT2 выглядит следующим образом:

  • Поскольку ReadView был сгенерирован ранее, предыдущий ReadView непосредственно повторно используется в это время Список m_ids в предыдущем ReadView [100, 200].

  • Затем выберите видимые записи из цепочки версий.Как видно из рисунка, содержимое столбца имени последней версии — «маленькая F». Значение trx_id этой версии равно 200, что находится в списке m_ids, поэтому он не соответствует требованиям видимости. Перейти к следующей версии на основе roll_pointer.

  • Содержимое имени столбца следующей версии — «маленькая D», а значение trx_id этой версии — 200, что тоже есть в списке m_ids, поэтому не соответствует требованиям, и продолжает переходить к следующей версии .

  • Содержимое имени столбца следующей версии — «маленький C», значение trx_id этой версии — 100, а список m_ids содержит значение идентификатора транзакции 100, поэтому эта версия не соответствует требованиям, то же самое верно для имени следующего столбца Версия, содержимое которой «маленькая B», также не соответствует требованиям. Продолжайте переходить к следующей версии.

  • Содержимое имени столбца следующей версии — «маленькая буква А», значение trx_id этой версии — 80, а 80 меньше наименьшего идентификатора транзакции 100 в списке m_ids, поэтому эта версия соответствует требованиям, а окончательная версия пользователю возвращается эта запись столбца A с именем «маленький A».

То есть результаты данных, полученные нашими двумя запросами SELECT, одинаковы (повторяются), а значения имени столбца все «маленькие A», что означает [повторяемое чтение]. Если мы отправим запись с идентификатором транзакции 200 позже, а затем продолжим поиск записи с идентификатором 1 в транзакции, которая только что использовала уровень изоляции REPEATABLE READ, результатом будет по-прежнему «Small A».

Резюме MVCC

Из вышеприведенного описания видно, что так называемый MVCC (Multi-Version Concurrency Control, многоверсионный контроль параллелизма) относится к использованию READ COMMITTD, REPEATABLE READ этих двух транзакций уровня изоляции для доступа при выполнении обычных операций SEELCT. процесс записи цепочки версий, чтобы операции чтения-записи и записи-чтения разных транзакций могли выполняться одновременно, тем самым повышая производительность системы. Большая разница между двумя уровнями изоляции READ COMMITTD и REPEATABLE READ заключается в том, что время создания ReadView различно.READ COMMITTD создает ReadView перед каждой обычной операцией SELECT, а REPEATABLE READ — только перед первой обычной операцией SELECT. и повторите ReadView для последующих операций запроса.

Вернемся к нашему названию: Может ли MySQL разрешить фантомное чтение? Или как MySQL решает фантомные чтения? Вы понимаете теперь? добро пожаловать на обсуждение