Говоря об управлении параллелизмом в MySQL: уровень изоляции, блокировка и MVCC

MySQL
Говоря об управлении параллелизмом в MySQL: уровень изоляции, блокировка и MVCC

предисловие

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

Проблемы с одновременными транзакциями

Во-первых, давайте взглянем на проблемы, которые могут быть связаны с одновременными транзакциями. Параллельные транзакции, обращающиеся к одной и той же записи, можно условно разделить на следующие три ситуации:

  • читать-читать: то есть параллельные транзакции последовательно считывают одну и ту же запись;
  • пиши-пиши: то есть параллельные транзакции последовательно изменяют одну и ту же запись;
  • пиши-читайиличитай пиши: две параллельные транзакции выполняют операции чтения и записи для одной и той же записи соответственно.
читать-читать

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

пиши-пиши

Если параллельным транзакциям разрешено читать одну и ту же запись и последовательно изменять запись на основе старого значения, то модификация, сделанная предыдущей транзакцией, будет перезаписана модификацией более поздней транзакции, то есть возникновениеЗафиксировать покрытиеПроблема.

В другом случае параллельные транзакции последовательно вносят изменения в одну и ту же запись, и после того, как одна транзакция зафиксирована, другая транзакция откатывается, поэтому будет проблема, что зафиксированные изменения теряются из-за отката, т.е.наложение откатапроблема.

Обе эти проблемы приводят к потере обновлений, когда перезаписывается откат.Потерянные обновления категории 1проблема, покрытие фиксации называетсяВторая категория потерянных обновленийпроблема.

пиши-читайиличитай пиши

Эта ситуация более сложная и наиболее подвержена проблемам.

Возникает, если одна транзакция читает незафиксированные записи модификации другой транзакции.грязное чтениеПроблема;

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

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

уровень изоляции транзакций

Для проблем, которые могут возникнуть при выполнении параллельных транзакций, упомянутых выше, их серьезность также различна, мы можем отсортировать их по серьезности проблем:

丢失更新 > 脏读 > 不可重复读 > 幻读

Таким образом, если мы сможем допустить некоторые менее серьезные проблемы, мы сможем получить некоторый прирост производительности. Таким образом, существует четыре уровня изоляции транзакций:

  • Читайте незарегистрированные (Read Uncommitted): разрешить незафиксированную запись чтения, происходит грязное чтение, неповторяемое чтение, фантомное чтение;
  • читать зафиксировано (Read Committed): Разрешены только отправленные записи, грязные чтения не будут происходить, но будут происходить повторные чтения и фантомные чтения;
  • повторяющееся чтение (Repeatable Read): Грязные чтения и неповторяющиеся чтения не будут происходить, но будут происходить фантомные чтения; ноMySQLЭксплойт на этом уровне изоляцииMVCCилигэп замокМожет предотвратить возникновение проблем с фантомным чтением;
  • Сериализуемый (Serializable): То есть транзакция выполняется последовательно, и вышеперечисленных проблем естественно не возникнет.

Стоит отметить, что вышеописанные четыре уровня изоляции не имеют проблемы покрытия откатами, но проблема покрытия фиксации не является проблемой.MySQLНапример, вRead Uncommitted,Read Committedа такжеRepeatable ReadВстречается на всех трех уровнях изоляции (стандартныйRepeatable ReadУровень изоляции не позволяет решить проблему покрытия фиксации), и для решения этой проблемы требуются дополнительные блокировки.

Реализация уровней изоляции

SQLСпецификация определяет вышеупомянутые четыре уровня изоляции, но не дает, как реализовать четыре уровня изоляции, поэтому реализация и использование разных баз данных также различны. иSQLСтандарт уровня изоляции основан на реализации на основе блокировки, потому что необходимо сначала понять, как реализуется традиционный уровень изоляции на основе блокировки.

Реализация традиционных уровней изоляции

Поскольку традиционный уровень изоляции основан на блокировках, давайте сначала разберемся с блокировками.

Замок

Традиционные замки бывают двух типов:

  • общий замок (Shared Locks): короткое имяS锁, когда транзакция читает запись, она должна сначала получить общую блокировку записи.
  • эксклюзивный замок (Exclusive Locks): короткое имяX锁Когда транзакция записывает запись, она должна сначала получить эксклюзивный замок на записи.

Следует отметить, что другие транзакции также могут получить совместную блокировку записи с общей блокировкой, но не могут получить монопольную блокировку записи, т.е.S锁иS锁совместим,S锁иX锁несовместимы; и для записи с эксклюзивной блокировкой другие транзакции не могут получить ни общую блокировку записи, ни эксклюзивную блокировку, т.X锁иX锁тоже несовместимо.

Кроме того, только что упоминалось, что когда транзакция читает запись, ей необходимо получитьS锁, но иногда транзакции необходимо предотвратить доступ других транзакций к записи при чтении записи, тогда ей необходимо получитьX锁. отMySQLНапример, есть два способа блокировки чтения:

  • Добавлять в запись при чтенииS锁:
SELECT ... LOCK IN SHARE MODE;

Если транзакция выполняет инструкцию, она добавит к прочитанной записиS锁Это позволяет другим транзакциям получить записьS锁; и если другим транзакциям нужно получить записьX锁, то вам нужно дождаться фиксации текущей транзакции и освободить ееS锁.

  • Добавлять в запись при чтенииX锁:
SELECT ... FOR UPDATE;

Если транзакция выполняет инструкцию, она добавит к прочитанной записиX锁Эта другая транзакция хочет сказать записиS锁илиX锁, то вам нужно дождаться фиксации текущей транзакции и освободить ееX锁.

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

  • Блокировка строки: блокируется только одна строка записей, записи в других строках не затрагиваются.
  • Блокировка таблицы: заблокируйте всю таблицу, это повлияет на все операции в таблице.
Уровень изоляции на основе замков

В реализации на основе блокировки разница между четырьмя уровнями изоляции заключается в разнице в методах блокировки:

  • читать незафиксированные: Операция чтения не заблокирована, чтение-чтение, чтение-запись и запись-чтение параллельны, добавлена ​​операция записиX锁И он не освобождается, пока транзакция не будет зафиксирована.
  • чтение зафиксировано: операция чтения плюсS锁, операции записи плюсX锁И он не будет освобожден до тех пор, пока транзакция не будет зафиксирована; операции чтения не будут блокировать другие транзакции от чтения или записи, а операции записи будут блокировать другие транзакции от записи и чтения, тем самым предотвращая проблему грязного чтения.
  • повторяемое чтение: операция чтения плюсS锁И не освобождается, пока транзакция не будет зафиксирована, а операция записи добавляетX锁И он не будет выпущен до тех пор, пока транзакция не будет зафиксирована; операция чтения не будет блокировать чтение других транзакций, но заблокирует запись других транзакций, а операция записи заблокирует чтение и запись других транзакций, поэтому она может предотвратить грязные чтения и неповторяющиеся чтения.
  • сериализовать: добавлены операции чтения и записи.X锁И он не освобождается до тех пор, пока транзакция не будет зафиксирована, а гранулярность — это блокировка таблицы, которая является строго последовательной.

Вот некоторые детали, на которые стоит обратить внимание:

  • Если блокировка получена и не снимается до тех пор, пока транзакция не будет зафиксирована, блокировка вызываетсядлинный замок; если блокировка снимается после завершения операции, вызывается блокировкакороткий замок. Например, на уровне изоляции read commited операции чтения добавляютS锁Для короткой блокировки добавлена ​​операция записиX锁для длинных замков.
  • Для повторяемых уровней изоляции чтения и сериализуемых операций чтения добавляютсяS锁и добавлены операции записиX锁Оба являются длинными блокировками, то есть после того, как транзакция получает блокировку, она не может быть освобождена до тех пор, пока транзакция не будет зафиксирована.Этот протокол, который делит получение блокировок и освобождение блокировок на два разных этапа, называетсядвухфазный замокпротокол(2-phase locking). Протокол двухфазной блокировки предусматривает, что на этапе блокировки транзакция может получить блокировку, но не может снять блокировку, а на этапе разблокировки транзакция может только снять блокировку и не может получить новые блокировки. Протокол двухфазной блокировки может обеспечить сериализацию транзакций и решить проблему параллелизма транзакций, но он также значительно увеличивает вероятность взаимоблокировок.

Реализация уровня изоляции MySQL

различные базы данных дляSQLПоддержка уровня изоляции, указанная в стандарте, отличается.Хотя ядро ​​базы данных реализует уровень изоляции, максимально приближенный к стандартной спецификации уровня изоляции, он все же несколько отличается от стандартного ожидания.

MySQL(InnoDB) поддерживает четыре уровня изоляции, которые несколько отличаются от проблем, допускаемых стандартными уровнями изоляции на всех уровнях, таких какMySQLПри уровне изоляции повторяющегося чтения проблема фантомного чтения может быть предотвращена, но также может возникнуть проблема покрытия фиксации.

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

Принцип реализации MVCC

Для удобства описания сначала составим таблицуbook, всего три поля, которые являются первичным ключомbook_id, названиеbook_name, в наличииstock. Затем вставьте некоторые данные в таблицу:

INSERT INTO book VALUES(1, '数据结构', 100);
INSERT INTO book VALUES(2, 'C++指南', 100);
INSERT INTO book VALUES(3, '精通Java', 100);
цепочка версий

Для использованияInnoDBТаблица механизма хранения содержит два важных скрытых столбца в своей записи кластеризованного индекса:

  • Номер транзакции (DB_TRX_ID): Всякий раз, когда транзакция изменяет запись в кластеризованном индексе, идентификатор текущей транзакции записывается вDB_TRX_IDсередина.
  • указатель отката (DB_ROLL_PTR): Всякий раз, когда транзакция изменяет запись в кластеризованном индексе, она записывает старую версию записи вundoвойти, черезDB_ROLL_PTRЭтот указатель можно использовать для получения информации о старой версии записи.

Если запись изменяется несколько раз в рамках транзакции, каждая модификация генерируетundoжурнал, и этиundoпроход журналаDB_ROLL_PTRУказатели объединяются в цепочку версий, головной узел цепочки версий — это последнее значение записи, а хвостовой узел — начальное значение в начале транзакции.

Например, мы в таблицеbookСделайте следующие модификации:

BEGIN;

UPDATE book SET stock = 200 WHERE id = 1;

UPDATE book SET stock = 300 WHERE id = 1;

Такid=1Цепочка версий записи в это время показана на следующем рисунке:

ReadView

Для использованияRead UncommittedДля транзакций на уровне изоляции вам нужно только прочитать последнюю версию записи в цепочке версий; для транзакций, использующихSerializableДля транзакций уровня изоляцииInnoDBИспользуйте блокировки для доступа к записям. иRead CommittedиRepeatable ReadС точки зрения уровня изоляции необходимо прочитать записи, измененные зафиксированной транзакцией, то есть, если модификация версии в цепочке версий не отправлена, записи этой версии не могут быть прочитаны. Следовательно, необходимо определитьRead CommittedиRepeatable ReadНа уровне изоляции какая версия в цепочке версий может быть прочитана текущей транзакцией. тогдаReadViewДля решения этой проблемы предлагается концепция.

ReadViewЭквивалент снимка записи расписания, в этом снимке мы можем получить транзакции, связанные с текущей записью, какие транзакции были зафиксированы.стабильные дела, которыеАктивная транзакция, которыеТранзакции, которые запускаются после создания моментального снимка. Из этого мы можемАлгоритм сравнения видимостиОпределите последнюю запись версии, которую можно прочитать в цепочке версий.

Алгоритм сравнения видимостиАлгоритм сравнения основан на идентификаторе транзакции. Во-первых, нам нужно знать следующее: Транзакцияidраспределяется постепенно. отReadViewМы можем получить наименьшую и наибольшую транзакцию среди активных транзакций в системе на момент создания снимкаid(крупнейшая сделкаidна самом деле тот, который будет назначен следующей транзакции в системеidзначение), поэтому мы получаем активную транзакциюidДиапазон, мы можем назвать этоACTIVE_TRX_ID_RANGE. Тогда транзакции, соответствующие идентификаторам транзакций меньше этого диапазона, являются зафиксированными стабильными транзакциями, а транзакции, превышающие этот диапазон, являются транзакциями, которые открываются после создания моментального снимка.ACTIVE_TRX_ID_RANGEВ дополнение к активной транзакции транзакции в области также являются зафиксированными стабильными транзакциями.

С приведенной выше информацией мы следуем по цепочке версий от головного узла, чтобы найти последнюю запись версии, которую можно прочитать:

1. Сначала определите запись версииDB_TRX_IDПоля и генерацияReadViewСовпадают ли идентификаторы транзакций, соответствующие транзакциям. Если они равны, то это означает, что данная версия записи генерируется в текущей транзакции и естественно может быть прочитана текущей транзакцией, иначе переходим к шагу 2.

2. Если версия записанаDB_TRX_IDПоле меньше диапазонаACTIVE_TRX_ID_RANGE, указывающее, что запись версии — это запись, измененная зафиксированной транзакцией, то есть видимая для текущей транзакции, иначе переходим к следующему шагу.

3. Если версия записанаDB_TRX_IDПоле находится в пределах досягаемостиACTIVE_TRX_ID_RANGEЕсли идентификатор транзакции не соответствует активной транзакции, это указывает на то, что запись версии является записью, измененной зафиксированной транзакцией, то есть она видна текущей транзакции; если идентификатор транзакции соответствует активной транзакции, он не видна для текущей транзакции Чтение Для записи следующей версии в цепочке версий повторяйте описанные выше шаги, пока не будет найдена версия, видимая для текущей транзакции.

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

существуетMySQLсередина,Read CommittedиRepeatable ReadРазница на уровне изоляции заключается в том, что они генерируютReadViewсроки разные.

MVCC обеспечивает различные уровни изоляции

сказал раньшеReadViewМеханизм только вRead CommittedиRepeatable ReadЭто вступает в силу на уровне изоляции, поэтому только эти два уровня изоляции имеютMVCC.
существуетRead CommittedНа уровне изоляции он будет генерироваться каждый раз при чтении данных.ReadView; пока вRepeatable ReadУровень изоляции будет создан только тогда, когда транзакция считывает данные в первый раз.ReadView, последующие операции чтения будут использовать этоReadView.

Давайте посмотрим на примерRead CommittedиRepeatable Readниже уровня изоляцииMVCCразная производительность. Мы продолжаем показыватьbookПокажите как пример.

Читать Анализ уровня изоляции

предположим вRead CommittedНа уровне изоляции выполняются следующие транзакции: транзакцияidна 10:

BEGIN; // 开启Transaction 10

UPDATE book SET stock = 200 WHERE id = 2;

UPDATE book SET stock = 300 WHERE id = 2;

На данный момент транзакция не была зафиксирована,idЦепочка версий записи для 2 показана ниже:

Затем мы запускаем пару транзакцийidЗапрос для 2 записей:

BEGIN;

SELECT * FROM book WHERE id = 2;

при исполненииSELECTоператор будет генерироватьReadView,ДолженReadViewсерединаACTIVE_TRX_ID_RANGEза[10, 11), идентификатор текущей транзакцииcreator_trx_idза0(Поскольку при выполнении операции записи внутри транзакции выделяется отдельная транзакция.id, иначе транзакцияidза0). как мы уже говорилиReadViewПринцип работы запрошенной записи версии:

+----------+-----------+-------+
| book_id  | book_name | stock |
+----------+-----------+-------+
| 2        | C++指南    |  100  |
+----------+-----------+-------+

Тогда мы будем делоidСовершить 10 транзакций:

BEGIN; // 开启Transaction 10

UPDATE book SET stock = 200 WHERE id = 2;

UPDATE book SET stock = 300 WHERE id = 2;

COMMIT;

Начать выполнение другой транзакции в то же времяidза11транзакции, но не совершать:

BEGIN; // 开启Transaction 11

UPDATE book SET stock = 400 WHERE id = 2;

В настоящее времяidЦепочка версий записи для 2 показана ниже:

Затем мы возвращаемся к транзакции запроса только сейчас, чтобы снова запроситьidза 2 записи:

BEGIN;

SELECT * FROM book WHERE id = 2; // 此时Transaction 10 未提交

SELECT * FROM book WHERE id = 2; // 此时Transaction 10 已提交

при выполнении во второй разSELECTоператор снова сгенерируетReadView,ДолженReadViewсерединаACTIVE_TRX_ID_RANGEза[11, 12), идентификатор текущей транзакцииcreator_trx_idвсе еще0. в соответствии сReadViewЧтобы проанализировать принцип работы, запрошенная нами запись версии выглядит следующим образом:

+----------+-----------+-------+
| book_id  | book_name | stock |
+----------+-----------+-------+
| 2        | C++指南    | 300   |
+----------+-----------+-------+

Его можно найти из приведенного выше анализа, потому что каждый раз, когда выполняется оператор запроса, создается новыйReadView, так что вRead CommittedТранзакция на уровне изоляции считывает данные, измененные зафиксированной транзакцией, в расписании запроса.

Анализ уровня изоляции Repeatable Read

мы вRepeatable ReadПовторите описанную выше транзакционную операцию на уровне изоляции:

BEGIN; // 开启Transaction 20

UPDATE book SET stock = 200 WHERE id = 2;

UPDATE book SET stock = 300 WHERE id = 2;

На данный момент транзакция еще не зафиксирована, и тогда мы открываем пару транзакцийidЗапрос для 2 записей:

BEGIN;

SELECT * FROM book WHERE id = 2;

Когда транзакция выполняется впервыеSELECTоператор будет генерироватьReadView,ДолженReadViewсерединаACTIVE_TRX_ID_RANGEза[10, 11), идентификатор текущей транзакцииcreator_trx_idза0. в соответствии сReadViewПринцип работы запрошенной записи версии:

+----------+-----------+-------+
| book_id  | book_name | stock |
+----------+-----------+-------+
| 2        | C++指南    |  100  |
+----------+-----------+-------+

Тогда мы будем делоidСовершить 20 транзакций:

BEGIN; // 开启Transaction 20

UPDATE book SET stock = 200 WHERE id = 2;

UPDATE book SET stock = 300 WHERE id = 2;

COMMIT;

Начать выполнение другой транзакции в то же времяidТранзакция за 21, но не совершена:

BEGIN; // 开启Transaction 21

UPDATE book SET stock = 400 WHERE id = 2;

Затем мы возвращаемся к транзакции запроса только сейчас, чтобы снова запроситьidза 2 записи:

BEGIN;

SELECT * FROM book WHERE id = 2; // 此时Transaction 10 未提交

SELECT * FROM book WHERE id = 2; // 此时Transaction 10 已提交

при выполнении во второй разSELECTоператор не создает новыйReadView, будет по-прежнему генерироваться при использовании первого запросаReadView. Следовательно, запрашиваемая нами запись версии совпадает с результатом первого запроса:

+----------+-----------+-------+
| book_id  | book_name | stock |
+----------+-----------+-------+
| 2        | C++指南    |  100  |
+----------+-----------+-------+

Из вышеприведенного анализа видно, что, посколькуRepeatable ReadТранзакции на уровне изоляции будут генерироваться только при первом выполнении запроса.ReadView, последующие операции запроса в транзакции будут использовать этоReadView, поэтому один и тот же запрос выполняется несколько раз в следующей транзакции на этом уровне изоляции, и результат остается тем же, что обеспечивает повторяемость чтения.

Чтение моментального снимка и текущее чтение
Снимок прочитан

существуетRead CommittedиRepeatable ReadПод уровнем изоляции обычныйSELECTзапрос прочитанMVCCВерсия в цепочке версий эквивалентна чтению моментального снимка, поэтому она называетсяСнимок прочитан. Этот способ чтения не блокируется, поэтому операция чтения является неблокирующей, поэтому ее также называютнеблокирующее чтение.

в стандартеRepeatable ReadОперации чтения на уровне изоляции увеличатсяS锁, до конца транзакции, тем самым блокируя запись другими транзакциями; но вMySQLизRepeatable ReadОперация чтения под уровнем изоляции не заблокирована и не помешает другим транзакциям записать ту же запись, поэтому в последующей операции записи можно записать результат, рассчитанный на основе старых данных в цепочке версий, что приводит к проблеме покрытия фиксации. Чтобы избежать этой проблемы, вам нужно добавить дополнительные блокировки для достижения.

текущее чтение

упоминалось ранееMySQLЕсть два способа блокировки чтения:

SELECT ... LOCK IN SHARE MODE; // 读取时对记录加S锁,直到事务结束

SELECT ... FOR UPDATE; // 读取时对记录加X锁,直到事务结束

Этот метод чтения считывает текущую последнюю версию записи, называемуютекущее чтение. Также дляINSERT,DELETE,UPDATEоперации, также необходимо сначала прочитать запись и получить записьX锁, этот процесс также является текущим чтением. Поскольку запись должна быть заблокирована, она будет блокировать операцию записи других транзакций, поэтому ее также называютблокировка чтенияилиблокировка чтения.

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

Суммировать

В данной статье представлены различные проблемы параллелизма транзакций, а также уровни изоляции, используемые для избежания проблем разной степени, и более подробно описана реализация традиционных уровней изоляции иMySQLКак реализованы уровни изоляции. Однако механизм параллелизма базы данных относительно сложен. В этой статье представлено только общее описание и введение. Многие детали по-прежнему требуют от читателей запроса соответствующей информации для более подробного понимания.

использованная литература

1,Многоверсионный контроль MySQL-InnoDB-MVCC

2,Как работает MySQL: понимание MySQL в корне