Предисловие
Наше приложение запускает несколько потоков, которые выполняют DELETE
запросы параллельно. Запросы влияют на изолированные данные, т. Е. Не должно быть вероятности того, что одновременное выполнение будет DELETE
происходить в одних и тех же строках из отдельных потоков. Однако согласно документации MySQL использует так называемую блокировку следующего ключа для DELETE
операторов, которая блокирует как соответствующий ключ, так и некоторый пробел. Это приводит к тупикам, и единственное решение, которое мы нашли, это использовать READ COMMITTED
уровень изоляции.
Проблема
Проблема возникает при выполнении сложных DELETE
операторов с JOIN
огромными таблицами. В конкретном случае у нас есть таблица с предупреждениями, которая имеет только две строки, но запрос должен удалить все предупреждения, которые принадлежат некоторым конкретным объектам, из двух отдельных INNER JOIN
таблиц ed. Запрос выглядит следующим образом:
DELETE pw
FROM proc_warnings pw
INNER JOIN day_position dp
ON dp.transaction_id = pw.transaction_id
INNER JOIN ivehicle_days vd
ON vd.id = dp.ivehicle_day_id
WHERE vd.ivehicle_id=? AND dp.dirty_data=1
Когда таблица day_position достаточно велика (в моем тестовом примере 1448 строк), любая транзакция, даже в READ COMMITTED
режиме изоляции, блокирует всю proc_warnings
таблицу.
Проблема всегда воспроизводится на этом примере данных - http://yadi.sk/d/QDuwBtpW1BxB9 как в MySQL 5.1 (проверено на 5.1.59), так и MySQL 5.5 (проверено на MySQL 5.5.24).
РЕДАКТИРОВАТЬ: Связанные примеры данных также содержат схему и индексы для таблиц запросов, воспроизведенные здесь для удобства:
CREATE TABLE `proc_warnings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`warning` varchar(2048) NOT NULL,
PRIMARY KEY (`id`),
KEY `proc_warnings__transaction` (`transaction_id`)
);
CREATE TABLE `day_position` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_day_id` int(10) unsigned DEFAULT NULL,
`dirty_data` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `day_position__trans` (`transaction_id`),
KEY `day_position__is` (`ivehicle_day_id`,`sort_index`),
KEY `day_position__id` (`ivehicle_day_id`,`dirty_data`)
) ;
CREATE TABLE `ivehicle_days` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`d` date DEFAULT NULL,
`sort_index` int(11) DEFAULT NULL,
`ivehicle_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ivehicle_days__is` (`ivehicle_id`,`sort_index`),
KEY `ivehicle_days__d` (`d`)
);
Запросы на транзакции следующие:
Транзакция 1
set transaction isolation level read committed; set autocommit=0; begin; DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=2 AND dp.dirty_data=1;
Транзакция 2
set transaction isolation level read committed; set autocommit=0; begin; DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1;
Один из них всегда завершается ошибкой «Ошибка ожидания ожидания превышена ...». information_schema.innodb_trx
Содержит следующие строки:
| trx_id | trx_state | trx_started | trx_requested_lock_id | trx_wait_started | trx_wait | trx_mysql_thread_id | trx_query |
| '1A2973A4' | 'LOCK WAIT' | '2012-12-12 20:03:25' | '1A2973A4:0:3172298:2' | '2012-12-12 20:03:25' | '2' | '3089' | 'DELETE pw FROM proc_warnings pw INNER JOIN day_position dp ON dp.transaction_id = pw.transaction_id INNER JOIN ivehicle_days vd ON vd.id = dp.ivehicle_day_id WHERE vd.ivehicle_id=13 AND dp.dirty_data=1' |
| '1A296F67' | 'RUNNING' | '2012-12-12 19:58:02' | NULL | NULL | '7' | '3087' | NULL |
information_schema.innodb_locks
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
| '1A2973A4:0:3172298:2' | '1A2973A4' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
| '1A296F67:0:3172298:2' | '1A296F67' | 'X' | 'RECORD' | '`deadlock_test`.`proc_warnings`' | '`PRIMARY`' | '0' | '3172298' | '2' | '53' |
Как я вижу, оба запроса хотят монопольную X
блокировку строки с первичным ключом = 53. Однако ни один из них не должен удалять строки из proc_warnings
таблицы. Я просто не понимаю, почему индекс заблокирован. Кроме того, индекс не блокируется, когда proc_warnings
таблица пуста или day_position
таблица содержит меньшее количество строк (то есть сто строк).
Дальнейшее расследование должно было пройти EXPLAIN
по аналогичному SELECT
запросу. Это показывает, что оптимизатор запросов не использует индекс для запроса proc_warnings
таблицы, и это единственная причина, по которой я могу себе представить, почему он блокирует весь индекс первичного ключа.
Упрощенный случай
Проблема также может быть воспроизведена в более простом случае, когда есть только две таблицы с несколькими записями, но у дочерней таблицы нет индекса в столбце ссылки родительской таблицы.
Создать parent
таблицу
CREATE TABLE `parent` (
`id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Создать child
таблицу
CREATE TABLE `child` (
`id` int(10) unsigned NOT NULL,
`parent_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
Заполнить таблицы
INSERT INTO `parent` (id) VALUES (1), (2);
INSERT INTO `child` (id, parent_id) VALUES (1, NULL), (2, NULL);
Тест в двух параллельных транзакциях:
Транзакция 1
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET AUTOCOMMIT=0; BEGIN; DELETE c FROM child c INNER JOIN parent p ON p.id = c.parent_id WHERE p.id = 1;
Транзакция 2
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SET AUTOCOMMIT=0; BEGIN; DELETE c FROM child c INNER JOIN parent p ON p.id = c.parent_id WHERE p.id = 2;
Общей чертой в обоих случаях является то, что MySQL не использует индексы. Я считаю, что это является причиной блокировки всей таблицы.
Наше решение
Единственное решение, которое мы видим на данный момент, - это увеличить время ожидания блокировки по умолчанию с 50 до 500 секунд, чтобы позволить завершить очистку потока. Затем держите пальцы скрещенными.
Любая помощь приветствуется.
day_position
обычно содержит таблица, когда она начинает работать так медленно, что вам нужно увеличить ограничение по времени до 500 секунд? 2) Сколько времени требуется для запуска, когда у вас есть только данные образца?Ответы:
НОВЫЙ ОТВЕТ (динамический SQL в стиле MySQL). Хорошо, этот вопрос решает проблему так, как описал один из других авторов, - в обратном порядке, в котором приобретаются взаимно несовместимые эксклюзивные блокировки, так что независимо от того, сколько их происходит, они происходят только для наименьшее количество времени в конце выполнения транзакции.
Это достигается путем разделения части read оператора в его собственный оператор select и динамической генерации оператора delete, который будет принудительно выполняться последним из-за порядка появления оператора и который повлияет только на таблицу proc_warnings.
Демо доступно на sql fiddle:
Эта ссылка показывает схему с образцами данных и простой запрос для соответствующих строк
ivehicle_id=2
. В результате получаются 2 строки, поскольку ни одна из них не была удалена.Эта ссылка показывает ту же схему, примеры данных, но передает значение 2 хранимой программе DeleteEntries, сообщая SP об удалении
proc_warnings
записей дляivehicle_id=2
. Простой запрос строк не возвращает результатов, так как все они были успешно удалены. Демонстрационные ссылки только демонстрируют, что код работает так, как предназначено для удаления. Пользователь с соответствующей тестовой средой может прокомментировать, решает ли это проблему заблокированного потока.Вот код для удобства:
Это синтаксис для вызова программы из транзакции:
ОРИГИНАЛЬНЫЙ ОТВЕТ (все еще думаю, что это не так уж плохо) Похоже, 2 проблемы: 1) медленный запрос 2) неожиданное поведение блокировки
Что касается проблемы № 1, медленные запросы часто разрешаются с помощью тех же двух методов упрощения операторов тандемного запроса и полезных дополнений или модификаций индексов. Вы сами уже установили связь с индексами - без них оптимизатор не сможет найти ограниченный набор строк для обработки, и каждая строка в каждой таблице, умноженная на дополнительную строку, сканирует объем дополнительной работы, которую необходимо выполнить.
ПЕРЕСМОТРЕНО ПОСЛЕ ОТЧЕТА О СХЕМЕ И ИНДЕКСАХ: Но я полагаю, что вы получите наибольшее повышение производительности для своего запроса, убедившись, что у вас хорошая конфигурация индекса. Для этого вы можете повысить производительность удаления и, возможно, повысить производительность удаления, компенсировав более крупные индексы и, возможно, заметно снизив производительность вставки в тех же таблицах, к которым добавлена дополнительная структура индекса.
ЧТО-ЛУЧШЕ
ПЕРЕСМОТРЕН ЗДЕСЬ СЛИШКОМ: так как это занимает столько времени, сколько нужно для запуска, я оставляю dirty_data в индексе, и я тоже ошибался, когда помещал его после ivehicle_day_id в порядке индекса - он должен быть первым.
Но если бы у меня были на этом руки, на данный момент, так как должно быть достаточно большого количества данных, чтобы это заняло так много времени, я бы просто пошел на все охватывающие индексы, просто чтобы убедиться, что я получаю наилучшую индексацию, которая мое время на устранение неполадок может выиграть, если не исключать, что эта часть проблемы исключена.
ЛУЧШИЕ / ИНДЕКСЫ ПОКРЫТИЯ:
В последних двух предложениях по изменению преследуются две цели оптимизации производительности:
1) Если ключи поиска для таблиц с последовательным доступом не совпадают с результатами кластеризованного ключа, возвращенными для текущей таблицы, мы исключаем то, что было бы необходимо сделать второй набор операций поиска по индексу со сканированием в кластеризованном индексе.
2) Если последний случай не так, все еще существует, по крайней мере, вероятность того, что оптимизатор может выбрать более эффективный алгоритм объединения, поскольку индексы будут сохранять необходимые ключи объединения в отсортированном порядке.
Ваш запрос выглядит настолько упрощенным, насколько это возможно (его можно скопировать на случай, если он будет отредактирован позже):
Если, конечно, есть что-то в письменном порядке соединения, которое влияет на работу оптимизатора запросов, в этом случае вы можете попробовать некоторые предложения по перезаписи, которые предоставили другие, включая, возможно, этот с указаниями индекса (необязательно):
Что касается № 2, неожиданное поведение блокировки.
Я предполагаю, что это будет заблокированный индекс, потому что строка данных, которые должны быть заблокированы, находится в кластерном индексе, то есть сама строка данных находится в индексе.
Это будет заблокировано, потому что:
1) в соответствии с http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html
Вы также упомянули выше:
и предоставил следующую ссылку для этого:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed
Который заявляет то же самое, что и вы, за исключением того, что согласно той же ссылке существует условие, при котором блокировка должна быть снята:
Который также повторяется на этой странице руководства http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html
Итак, нам сказали, что условие WHERE должно быть оценено до того, как блокировка может быть восстановлена. К сожалению, нам не сообщают, когда оценивается условие WHERE, и, вероятно, что-то может измениться из одного плана в другой, созданный оптимизатором. Но это говорит нам о том, что освобождение блокировки как-то зависит от производительности выполнения запроса, оптимизация которого, как мы обсуждали выше, зависит от тщательного написания заявления и разумного использования индексов. Это также может быть улучшено за счет лучшего дизайна стола, но это, вероятно, лучше оставить отдельным вопросом.
База данных не может блокировать записи в индексе, если их нет.
Это может означать множество вещей, таких как, но, возможно, не ограничиваясь: другой план выполнения из-за изменения статистики, слишком короткая, чтобы быть наблюдаемой блокировка из-за гораздо более быстрого выполнения из-за намного меньшего набора данных / присоединиться к операции.
источник
WHERE
Состояние оценивается , когда запрос завершается. Не так ли? Я думал, что блокировка освобождается сразу после выполнения нескольких параллельных запросов. Это естественное поведение. Однако этого не происходит. Ни один из предложенных запросов в этой теме не помогает избежать блокировки кластерного индекса вproc_warnings
таблице. Я думаю, я сообщу об ошибке в MySQL. Спасибо за вашу помощь.Я вижу, как READ_COMMITTED может вызвать эту ситуацию.
READ_COMMITTED допускает три вещи:
Это создает внутреннюю парадигму для самой транзакции, потому что транзакция должна поддерживать связь с:
Если две разные транзакции READ_COMMITTED обращаются к одним и тем же таблицам / строкам, которые обновляются одинаковым образом, будьте готовы ожидать не блокировки таблицы, а исключительной блокировки в пределах gen_clust_index (он же Clustered Index) . Учитывая запросы из вашего упрощенного случая:
Транзакция 1
Транзакция 2
Вы блокируете то же место в gen_clust_index. Кто-то может сказать: «Но каждая транзакция имеет свой первичный ключ». К сожалению, это не так в глазах InnoDB. Так получилось, что id 1 и id 2 находятся на одной странице.
Оглянись на
information_schema.innodb_locks
тебя поставленный в ВопросеЗа исключением
lock_id
,lock_trx_id
остальная часть описания блокировки идентична. Поскольку транзакции находятся на одном игровом поле (одинаковая изоляция транзакций), это действительно должно произойти .Поверьте мне, я обращался к такой ситуации раньше. Вот мои прошлые сообщения об этом:
Nov 05, 2012
: Как проанализировать состояние innodb в тупик во вставке Query?Aug 08, 2011
: Блокировки InnoDB являются исключительными для INSERT / UPDATE / DELETE?Jun 14, 2011
: Причины для иногда медленных запросов?Jun 08, 2011
: Приведут ли эти два запроса к тупику, если будут выполняться последовательно?Jun 06, 2011
: Проблема с расшифровкой тупика в журнале состояния innodbисточник
Look back at information_schema.innodb_locks you supplied in the Question
)DELETE
оператора.Я посмотрел на запрос и объяснение. Я не уверен, но у меня есть ощущение, что проблема заключается в следующем. Давайте посмотрим на запрос:
Эквивалент SELECT:
Если вы посмотрите на его объяснение, вы увидите, что план выполнения начинается с
proc_warnings
таблицы. Это означает, что MySQL сканирует первичный ключ в таблице и для каждой строки проверяет, выполнено ли условие, а если оно есть - строка удаляется. То есть MySQL должен заблокировать весь первичный ключ.Вам нужно инвертировать ордер JOIN, то есть найти все идентификаторы транзакций
vd.ivehicle_id=16 AND dp.dirty_data=1
и соединить их вproc_warnings
таблице.То есть вам нужно будет исправить один из индексов:
и переписать запрос на удаление:
источник
proc_warnings
прежнему блокируются. Спасибо, в любом случае.Когда вы устанавливаете уровень транзакции без того, как вы это делаете, он применяет Read Committed только к следующей транзакции, таким образом (установите auto commit). Это означает, что после autocommit = 0 вы больше не находитесь в Read Committed. Я бы написал так:
Вы можете проверить, на каком уровне изоляции вы находитесь, запросив
источник
SET AUTOCOMMIT=0
следует сбросить уровень изоляции для следующей транзакции? Я полагаю, что начинается новая транзакция, если ни одна из них не была запущена ранее (как в моем случае). Таким образом, чтобы быть более точным, следующееSTART TRANSACTION
илиBEGIN
утверждение не является необходимым. Моя цель отключения автокоммитов - оставить транзакцию открытой после выполненияDELETE
оператора.