MySQL InnoDB блокирует первичный ключ при удалении даже в READ COMMITTED


11

Предисловие

Наше приложение запускает несколько потоков, которые выполняют 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 секунд, чтобы позволить завершить очистку потока. Затем держите пальцы скрещенными.

Любая помощь приветствуется.


У меня есть вопрос: вы выполняли COMMIT в любой из транзакций?
RolandoMySQLDBA

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

Когда вы говорите, что MySQL не использует индексы в любом случае, это потому, что в реальном сценарии их нет? Если есть индексы, можете ли вы предоставить код для них? Можно ли попробовать какие-либо из представленных ниже предложений по индексам? Если индексов нет, и попытаться добавить их невозможно, MySQL не может ограничить набор данных, обрабатываемый каждым потоком. Если это так, то N потоков просто умножат нагрузку на сервер в N раз, и было бы более эффективно просто запустить один поток со списком параметров, например {WHERE vd.ivehicle_id IN (2, 13) И dp.dirty_data = 1;}.
JM Hicks

Хорошо, нашли индексы, спрятанные в связанном примере файла данных.
JM Hicks

еще пара вопросов: 1) сколько строк day_positionобычно содержит таблица, когда она начинает работать так медленно, что вам нужно увеличить ограничение по времени до 500 секунд? 2) Сколько времени требуется для запуска, когда у вас есть только данные образца?
JM Hicks

Ответы:


3

НОВЫЙ ОТВЕТ (динамический SQL в стиле MySQL). Хорошо, этот вопрос решает проблему так, как описал один из других авторов, - в обратном порядке, в котором приобретаются взаимно несовместимые эксклюзивные блокировки, так что независимо от того, сколько их происходит, они происходят только для наименьшее количество времени в конце выполнения транзакции.

Это достигается путем разделения части read оператора в его собственный оператор select и динамической генерации оператора delete, который будет принудительно выполняться последним из-за порядка появления оператора и который повлияет только на таблицу proc_warnings.

Демо доступно на sql fiddle:

Эта ссылка показывает схему с образцами данных и простой запрос для соответствующих строк ivehicle_id=2. В результате получаются 2 строки, поскольку ни одна из них не была удалена.

Эта ссылка показывает ту же схему, примеры данных, но передает значение 2 хранимой программе DeleteEntries, сообщая SP об удалении proc_warningsзаписей для ivehicle_id=2. Простой запрос строк не возвращает результатов, так как все они были успешно удалены. Демонстрационные ссылки только демонстрируют, что код работает так, как предназначено для удаления. Пользователь с соответствующей тестовой средой может прокомментировать, решает ли это проблему заблокированного потока.

Вот код для удобства:

CREATE PROCEDURE DeleteEntries (input_vid INT)
BEGIN

    SELECT @idstring:= '';
    SELECT @idnum:= 0;
    SELECT @del_stmt:= '';

    SELECT @idnum:= @idnum+1 idnum_col, @idstring:= CONCAT(@idstring, CASE WHEN CHARACTER_LENGTH(@idstring) > 0 THEN ',' ELSE '' END, CAST(id AS CHAR(10))) idstring_col
    FROM proc_warnings
    WHERE EXISTS (
        SELECT 0
        FROM day_position
        WHERE day_position.transaction_id = proc_warnings.transaction_id
        AND day_position.dirty_data = 1
        AND EXISTS (
            SELECT 0
            FROM ivehicle_days
            WHERE ivehicle_days.id = day_position.ivehicle_day_id
            AND ivehicle_days.ivehicle_id = input_vid
        )
    )
    ORDER BY idnum_col DESC
    LIMIT 1;

    IF (@idnum > 0) THEN
        SELECT @del_stmt:= CONCAT('DELETE FROM proc_warnings WHERE id IN (', @idstring, ');');

        PREPARE del_stmt_hndl FROM @del_stmt;
        EXECUTE del_stmt_hndl;
        DEALLOCATE PREPARE del_stmt_hndl;
    END IF;
END;

Это синтаксис для вызова программы из транзакции:

CALL DeleteEntries(2);

ОРИГИНАЛЬНЫЙ ОТВЕТ (все еще думаю, что это не так уж плохо) Похоже, 2 проблемы: 1) медленный запрос 2) неожиданное поведение блокировки

Что касается проблемы № 1, медленные запросы часто разрешаются с помощью тех же двух методов упрощения операторов тандемного запроса и полезных дополнений или модификаций индексов. Вы сами уже установили связь с индексами - без них оптимизатор не сможет найти ограниченный набор строк для обработки, и каждая строка в каждой таблице, умноженная на дополнительную строку, сканирует объем дополнительной работы, которую необходимо выполнить.

ПЕРЕСМОТРЕНО ПОСЛЕ ОТЧЕТА О СХЕМЕ И ИНДЕКСАХ: Но я полагаю, что вы получите наибольшее повышение производительности для своего запроса, убедившись, что у вас хорошая конфигурация индекса. Для этого вы можете повысить производительность удаления и, возможно, повысить производительность удаления, компенсировав более крупные индексы и, возможно, заметно снизив производительность вставки в тех же таблицах, к которым добавлена ​​дополнительная структура индекса.

ЧТО-ЛУЧШЕ

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd` (`dirty_data`, `ivehicle_day_id`)

) ;


CREATE TABLE  `ivehicle_days` (
    ...,
    KEY `ivehicle_days__vid_no_sort_index` (`ivehicle_id`)
);

ПЕРЕСМОТРЕН ЗДЕСЬ СЛИШКОМ: так как это занимает столько времени, сколько нужно для запуска, я оставляю dirty_data в индексе, и я тоже ошибался, когда помещал его после ivehicle_day_id в порядке индекса - он должен быть первым.

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

ЛУЧШИЕ / ИНДЕКСЫ ПОКРЫТИЯ:

CREATE TABLE  `day_position` (
    ...,
    KEY `day_position__id_rvrsd_trnsid_cvrng` (`dirty_data`, `ivehicle_day_id`, `transaction_id`)
) ;

CREATE TABLE  `ivehicle_days` (
    ...,
    UNIQUE KEY `ivehicle_days__vid_id_cvrng` (ivehicle_id, id)
);

CREATE TABLE  `proc_warnings` (

    .., /*rename primary key*/
    CONSTRAINT pk_proc_warnings PRIMARY KEY (id),
    UNIQUE KEY `proc_warnings__transaction_id_id_cvrng` (`transaction_id`, `id`)
);

В последних двух предложениях по изменению преследуются две цели оптимизации производительности:
1) Если ключи поиска для таблиц с последовательным доступом не совпадают с результатами кластеризованного ключа, возвращенными для текущей таблицы, мы исключаем то, что было бы необходимо сделать второй набор операций поиска по индексу со сканированием в кластеризованном индексе.
2) Если последний случай не так, все еще существует, по крайней мере, вероятность того, что оптимизатор может выбрать более эффективный алгоритм объединения, поскольку индексы будут сохранять необходимые ключи объединения в отсортированном порядке.

Ваш запрос выглядит настолько упрощенным, насколько это возможно (его можно скопировать на случай, если он будет отредактирован позже):

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;

Если, конечно, есть что-то в письменном порядке соединения, которое влияет на работу оптимизатора запросов, в этом случае вы можете попробовать некоторые предложения по перезаписи, которые предоставили другие, включая, возможно, этот с указаниями индекса (необязательно):

DELETE FROM proc_warnings
FORCE INDEX (`proc_warnings__transaction_id_id_cvrng`, `pk_proc_warnings`)
WHERE EXISTS (
    SELECT 0
    FROM day_position
    FORCE INDEX (`day_position__id_rvrsd_trnsid_cvrng`)  
    WHERE day_position.transaction_id = proc_warnings.transaction_id
    AND day_position.dirty_data = 1
    AND EXISTS (
        SELECT 0
        FROM ivehicle_days
        FORCE INDEX (`ivehicle_days__vid_id_cvrng`)  
        WHERE ivehicle_days.id = day_position.ivehicle_day_id
        AND ivehicle_days.ivehicle_id = ?
    )
);

Что касается № 2, неожиданное поведение блокировки.

Как я вижу, оба запроса хотят исключительную блокировку X для строки с первичным ключом = 53. Однако ни один из них не должен удалять строки из таблицы proc_warnings. Я просто не понимаю, почему индекс заблокирован.

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

Это будет заблокировано, потому что:
1) в соответствии с http://dev.mysql.com/doc/refman/5.1/en/innodb-locks-set.html

... УДАЛЕНИЕ обычно устанавливает блокировки записей для каждой индексной записи, которая сканируется при обработке оператора SQL. Неважно, есть ли в операторе условия WHERE, исключающие строку. InnoDB не помнит точное условие WHERE, а только знает, какие диапазоны индекса были отсканированы.

Вы также упомянули выше:

... для меня главная особенность READ COMMITTED - это то, как он работает с блокировками. Он должен освободить блокировки индекса несовпадающих строк, но это не так.

и предоставил следующую ссылку для этого:
http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html#isolevel_read-committed

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

Кроме того, блокировки записей для несовпадающих строк освобождаются после того, как MySQL оценил условие WHERE.

Который также повторяется на этой странице руководства http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html

Существуют также другие эффекты использования уровня изоляции READ COMMITTED или включения innodb_locks_unsafe_for_binlog: блокировки записи для несовпадающих строк снимаются после того, как MySQL оценил условие WHERE.

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

Более того, индекс не блокируется, когда таблица proc_warnings пуста

База данных не может блокировать записи в индексе, если их нет.

Более того, индекс не блокируется, когда ... таблица day_position содержит меньшее количество строк (то есть сто строк).

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


WHEREСостояние оценивается , когда запрос завершается. Не так ли? Я думал, что блокировка освобождается сразу после выполнения нескольких параллельных запросов. Это естественное поведение. Однако этого не происходит. Ни один из предложенных запросов в этой теме не помогает избежать блокировки кластерного индекса в proc_warningsтаблице. Я думаю, я сообщу об ошибке в MySQL. Спасибо за вашу помощь.
Виталидзе

Я бы не ожидал, что они избежат блокировки. Я ожидаю, что он заблокируется, потому что я думаю, что документация говорит, что это то, что ожидается, независимо от того, хотим ли мы это обрабатывать запрос. Я бы просто ожидал, что избавление от проблемы с производительностью не позволит параллельному запросу блокироваться в течение такого явно (500+ секундного времени ожидания) времени.
JM Hicks

Хотя ваш {WHERE} выглядит так, как будто его можно использовать во время обработки объединения, чтобы ограничить количество строк, включаемых в вычисление объединения, я не вижу, как ваше предложение {WHERE} может быть оценено для каждой заблокированной строки, пока весь набор объединений не будет вычислил также. Тем не менее, для нашего анализа, я подозреваю, что вы правы, что мы должны заподозрить «условие WHERE оценивается, когда запрос завершается». Тем не менее, это приводит меня к тому же общему выводу, что производительность должна быть решена, и тогда очевидная степень параллелизма будет пропорционально возрастать.
JM Hicks

Помните, что правильные индексы могут потенциально исключить любое полное сканирование таблицы, которое происходит в таблице proc_warnings. Для того, чтобы это произошло, нам нужен оптимизатор запросов, чтобы он хорошо работал для нас, и нам нужны наши индексы, запросы и данные, чтобы с ним приятно работать. Значения параметров должны в конце вычисляться для строк в целевой таблице, которые не перекрываются между двумя запросами. Индексы должны предоставлять оптимизатору запросов средства для эффективного поиска этих строк. Нам нужен оптимизатор, чтобы понять эту потенциальную эффективность поиска и выбрать такой план.
JM Hicks

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

3

Я вижу, как READ_COMMITTED может вызвать эту ситуацию.

READ_COMMITTED допускает три вещи:

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

Это создает внутреннюю парадигму для самой транзакции, потому что транзакция должна поддерживать связь с:

  • InnoDB Buffer Pool (пока коммит еще не очищен)
  • Первичный ключ таблицы
  • возможно
    • двойной буфер записи
    • Отменить табличное пространство
  • Наглядное представление

Если две разные транзакции READ_COMMITTED обращаются к одним и тем же таблицам / строкам, которые обновляются одинаковым образом, будьте готовы ожидать не блокировки таблицы, а исключительной блокировки в пределах gen_clust_index (он же Clustered Index) . Учитывая запросы из вашего упрощенного случая:

  • Транзакция 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;
    

Вы блокируете то же место в gen_clust_index. Кто-то может сказать: «Но каждая транзакция имеет свой первичный ключ». К сожалению, это не так в глазах InnoDB. Так получилось, что id 1 и id 2 находятся на одной странице.

Оглянись на 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' |

За исключением lock_id, lock_trx_idостальная часть описания блокировки идентична. Поскольку транзакции находятся на одном игровом поле (одинаковая изоляция транзакций), это действительно должно произойти .

Поверьте мне, я обращался к такой ситуации раньше. Вот мои прошлые сообщения об этом:


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

Если в результате ошибки откатывается только один оператор SQL, некоторые блокировки, установленные этим оператором, могут быть сохранены. Это происходит потому, что InnoDB хранит блокировки строк в таком формате, что после этого он не может знать, какая блокировка была установлена ​​каким оператором: dev.mysql.com/doc/refman/5.5/en/innodb-deadlock-detection.html
RolandoMySQLDBA

Обратите внимание, что я упомянул возможность блокировки двух строк на одной странице (см. Look back at information_schema.innodb_locks you supplied in the Question)
RolandoMySQLDBA,

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

С двумя завершающими замками, один должен быть откат. Возможно, что замки могут задержаться. РАБОЧАЯ ТЕОРИЯ: откат транзакции может повториться и может встретить старую блокировку предыдущей транзакции, которая его удерживала.
RolandoMySQLDBA

2

Я посмотрел на запрос и объяснение. Я не уверен, но у меня есть ощущение, что проблема заключается в следующем. Давайте посмотрим на запрос:

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;

Эквивалент SELECT:

SELECT pw.id
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=16 AND dp.dirty_data=1;

Если вы посмотрите на его объяснение, вы увидите, что план выполнения начинается с proc_warningsтаблицы. Это означает, что MySQL сканирует первичный ключ в таблице и для каждой строки проверяет, выполнено ли условие, а если оно есть - строка удаляется. То есть MySQL должен заблокировать весь первичный ключ.

Вам нужно инвертировать ордер JOIN, то есть найти все идентификаторы транзакций vd.ivehicle_id=16 AND dp.dirty_data=1и соединить их в proc_warningsтаблице.

То есть вам нужно будет исправить один из индексов:

ALTER TABLE `day_position`
 DROP INDEX `day_position__id`,
 ADD INDEX `day_position__id`
   USING BTREE (`ivehicle_day_id`, `dirty_data`, `transaction_id`);

и переписать запрос на удаление:

DELETE pw
FROM (
  SELECT DISTINCT dp.transaction_id
  FROM ivehicle_days vd
  JOIN day_position dp ON dp.ivehicle_day_id = vd.id
  WHERE vd.ivehicle_id=? AND dp.dirty_data=1
) as tr_id
JOIN proc_warnings pw ON pw.transaction_id = tr_id.transaction_id;

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

2

Когда вы устанавливаете уровень транзакции без того, как вы это делаете, он применяет Read Committed только к следующей транзакции, таким образом (установите auto commit). Это означает, что после autocommit = 0 вы больше не находитесь в Read Committed. Я бы написал так:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
DELETE c FROM child c
INNER JOIN parent p ON
    p.id = c.parent_id
WHERE p.id = 1;

Вы можете проверить, на каком уровне изоляции вы находитесь, запросив

SELECT @@tx_isolation;

Это не правда. Почему SET AUTOCOMMIT=0следует сбросить уровень изоляции для следующей транзакции? Я полагаю, что начинается новая транзакция, если ни одна из них не была запущена ранее (как в моем случае). Таким образом, чтобы быть более точным, следующее START TRANSACTIONили BEGINутверждение не является необходимым. Моя цель отключения автокоммитов - оставить транзакцию открытой после выполнения DELETEоператора.
Виталидзе

1
@SqlKiwi это был способ отредактировать этот пост, и это был тот, чтобы прокомментировать ;-)
jcolebrand
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.