MySQL InnoDB блокує первинний ключ при видаленні, навіть якщо він читається


11

Передмова

У нашому додатку працює кілька потоків, які виконують DELETEзапити паралельно. Запити впливають на поодинокі дані, тобто не повинно бути ймовірності того, що паралельний запис DELETEвідбувається в одних і тих же рядках з окремих потоків. Однак, відповідно до документації, MySQL використовує так званий замок "next-key" для DELETEоператорів, який блокує як відповідність ключа, так і деякий пробіл. Ця річ призводить до глухих замків, і єдине знайдене нами рішення - використовувати READ COMMITTEDрівень ізоляції.

Проблема

Проблема виникає при виконанні складних DELETEоператорів з JOINвеличезними таблицями. У конкретному випадку у нас є таблиця з попередженнями, яка містить лише два рядки, але запит повинен видалити всі попередження, які належать певним особам, з двох окремих INNER JOINтаблиць редакції. Запит такий:

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).

EDIT: Зв'язані зразки даних також містять схему та індекси для таблиць запитів, відтворені тут для зручності:

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

Звичайно. Проблема полягає в тому, що всі інші транзакції повинні чекати, поки одна з них не вчинить змін. Простий тестовий випадок не містить заяву про введення комісій, щоб показати, як відтворити проблему. Якщо ви виконуєте фіксацію чи відкат у транзакції, що не чекає, вона одночасно звільняє блокування і транзакція очікування завершує роботу.
vitalidze

Коли ви говорите, що MySQL не використовує індекси в будь-якому випадку, це тому, що їх немає в реальному сценарії? Якщо є індекси, ви могли б надати код для них? Чи можна спробувати будь-яку із наведених нижче пропозицій щодо індексу? Якщо немає індексів і неможливо спробувати додати будь-який, то MySQL не може обмежити набір даних, оброблений кожним потоком. Якщо це так, то N потоків просто помножить навантаження сервера на N разів, і було б ефективніше просто дозволити одному потоку працювати зі списком параметрів, наприклад {WHERE vd.ivehicle_id IN (2, 13) AND dp.dirty_data = 1;}.
Дж. М. Хікс

Гаразд, знайдені індекси заховані у зв'язаному файлі зразків даних.
Дж. М. Хікс

ще кілька питань: 1) скільки рядків day_positionзазвичай містить таблиця, коли вона починає працювати так повільно, що вам доведеться збільшити обмеження на час очікування до 500 сек? 2) Скільки часу потрібно запустити, коли у вас є лише вибіркові дані?
Дж. М. Хікс

Відповіді:


3

НОВИЙ ВІДПОВІДЬ (Динамічний SQL у стилі MySQL): Добре, цей вирішує проблему в описі одного з інших плакатів - змінює порядок, коли отримуються взаємно несумісні виключні замки, так що незалежно від того, скільки їх відбувається, вони трапляються лише для найменша кількість часу в кінці виконання транзакції.

Це досягається шляхом відокремлення прочитаної частини оператора на власний оператор select та динамічного генерування оператора видалення, який буде змушений запускатися останнім через порядок появи оператора і який вплине лише на таблицю proc_warnings.

Демонстраційна версія доступна на скрипті sql:

Це посилання показує схему w / sample data та простий запит для рядків, які відповідають 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`)
);

ПЕРЕГЛЯДУТИ ТУТ ТАКОЖ: Оскільки це запускається стільки часу, скільки часу він запускається, я залишив "брудні_дані" в індексі, і я зрозумів, що я помилявся напевно, коли розмістив їх після 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;

Якщо, звичайно, є щось про письмовий порядок приєднання, що впливає на те, як відбувається оптимізатор запитів, у такому випадку ви можете спробувати деякі пропозиції щодо перезапису, надані іншими, включаючи, можливо, цей підказки w / index (необов’язково):

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/uk/innodb-locks-set.html

... DELETE зазвичай встановлює блокування записів на кожен запис індексу, який сканується при обробці оператора SQL. Не має значення, чи є у твердженні умови WHERE, які б виключали рядок. InnoDB не пам'ятає точну умову WHERE, але знає лише, які діапазони індексів були відскановані.

Ви також згадали вище:

... як на мене, головною особливістю ПРОЧИТАНОГО ВЗАЄМОГО є те, як це стосується замків. Він повинен випустити блоки покажчиків невідповідних рядків, але це не так.

і надав наступне посилання на це:
http://dev.mysql.com/doc/refman/5.1/uk/set-transaction.html#isolevel_read-committed

У якому зазначено те саме, що і ви, за винятком того, що відповідно до цієї ж посилання існує умова, згідно з якою замок буде звільнено:

Також блокування записів для не збігаються рядків звільняється після того, як MySQL оцінив умову WHERE.

Що також підтверджено на цій сторінці посібника http://dev.mysql.com/doc/refman/5.1/uk/innodb-record-level-locks.html

Існують також інші ефекти використання рівня ізоляції READ COMMITTED або включення innodb_locks_unsafe_for_binlog: Блоки записів для не збігаються рядків звільняються після того, як MySQL оцінив умову WHERE.

Отже, нам кажуть, що стан WHERE повинен бути оцінений, перш ніж блокування можна буде відновити. На жаль, нам не кажуть, коли оцінюється стан WHERE, і, ймовірно, щось може змінитися з одного плану на інший, створеного оптимізатором. Але це говорить нам про те, що випуск блокування певним чином залежить від виконання запиту, оптимізація якого, як ми обговорюємо вище, залежить від ретельного написання висловлювання та розумного використання індексів. Це також можна покращити за допомогою кращого дизайну столів, але це, мабуть, найкраще залишиться окремим питанням.

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

База даних не може блокувати записи в індексі, якщо таких немає.

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

Це може означати численні речі, такі як, але, ймовірно, не обмежуються: інший план виконання через зміну статистики, занадто короткий для спостереження замок через набагато швидше виконання через набагато менший набір даних / приєднатися до операції.


WHEREСтан оцінюється , коли запит завершується. Чи не так? Я думав, що блокування випускається відразу після виконання деяких одночасних запитів. Це природна поведінка. Однак цього не відбувається. Жоден із запропонованих запитів у цьому потоці не допомагає уникнути блокування кластеризованого індексу в proc_warningsтаблиці. Думаю, я подам помилку на MySQL. Спасибі за вашу допомогу.
vitalidze

Я також не очікував, що вони також уникатимуть поведінки блокування. Я б очікував, що він заблокується, тому що я думаю, що в документації сказано, що це очікується, незалежно від того, чи ми хотіли б, щоб він обробив запит. Я просто очікував, що позбавлення від продуктивності не дозволить блокувати паралельний запит настільки очевидно (500+ секунд таймаут) довгий час.
Дж. М. Хікс

Хоча ваш {WHERE} здається, що він може бути використаний під час обробки приєднання для обмеження, які рядки включені в обчислення з'єднання, я не бачу, як ваш {WHERE} пункт може бути оцінений за заблокованим рядком, поки весь набір об'єднань не буде обчислюється також. Це означає, що, під час нашого аналізу, я підозрюю, що ви праві, що ми маємо підозрювати "УСІСТЬ, де оцінюється, коли запит завершується". І все ж це призводить мене до такого ж загального висновку, що результативність потрібно вирішити, і тоді очевидна ступінь одночасності зросте пропорційно.
Дж. М. Хікс

Пам’ятайте, що належні індекси потенційно можуть усунути будь-яке сканування повної таблиці, що відбувається в таблиці proc_warnings. Для того, щоб це сталося, нам потрібен оптимізатор запитів, щоб добре працювати над нами, і нам потрібні наші індекси, запити та дані, щоб добре працювати з ним. Значення параметрів повинні в кінцевому підсумку оцінювати рядки в цільовій таблиці, які не перетинаються між двома запитами. Індекси повинні забезпечити оптимізатору запитів засіб для ефективного пошуку цих рядків. Нам потрібен оптимізатор, щоб зрозуміти, що потенційна ефективність пошуку, і вибрати такий план.
Дж. М. Хікс

Якщо між параметрами, індексами, результатами неперекривання в таблиці proc_warnings та вибором плану оптимізатора все добре входить, навіть якщо блокування може генеруватися протягом тривалості часу, необхідного для виконання запиту для кожного потоку, ці блокування, якщо ні перекриття, не буде суперечити запитам блокування інших потоків.
Дж. М. Хікс

3

Я бачу, як READ_COMMITTED може спричинити цю ситуацію.

READ_COMMITTED дозволяє виконувати три речі:

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

Це створює внутрішню парадигму для самої транзакції, оскільки транзакція повинна підтримувати контакт з:

  • Буферний пул InnoDB (поки фіксація ще не використовується)
  • Первинний ключ таблиці
  • Можливо
    • буфер подвійного запису
    • Скасувати простір таблиць
  • Образотворче зображення

Якщо дві різні транзакції READ_COMMITTED отримують доступ до одних і тих же таблиць / рядків, які оновлюються однаково, будьте готові очікувати не блокування таблиці, а ексклюзивного блокування в межах gen_clust_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. Але, як на мене, головна особливість ПРОЧИТАНОГО ВЗАЄМОГО - це те, як він працює з замками . Він повинен випустити блоки покажчиків невідповідних рядків, але це не так.
vitalidze

Якщо в результаті помилки повертається лише один оператор SQL, деякі з блоків, встановлених оператором, можуть бути збережені. Це трапляється тому, що InnoDB зберігає блокування рядків у такому форматі, що потім не може знати, який замок був встановлений, за допомогою якого оператора: dev.mysql.com/doc/refman/5.5/uk/innodb-deadlock-detection.html
RolandoMySQLDBA

Зверніть увагу, що я згадав про можливість двох Look back at information_schema.innodb_locks you supplied in the Question
блоків, що

Про відкат одного твердження - я розумію це так, що якщо одне твердження виходить з ладу в межах однієї транзакції, воно все ще може містити блокування. Це добре. Моє велике питання, чому він не випускає невідповідні блокування рядків після успішної обробки DELETEоператора.
vitalidze

З двома комплектуючими замками, один повинен бути відкатаний. Цілком можливо, що замки можуть затриматися. ТЕОРІЯ РОБОТИ: транзакція, яка повернулася назад, може повторитись і може зустріти старий замок з попередньої транзакції, яка її проводила.
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все ще заблокуються. Все одно, дякую.
vitalidze

2

Коли ви встановлюєте рівень транзакції без того, як ви це робите, застосовується «Прочитання, здійснене» лише до наступної транзакції, таким чином (встановіть автоматичну фіксацію). Це означає, що після автокомісії = 0, ви більше не перебуваєте в читанні "Здійснено". Я б написав це так:

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заяви.
vitalidze

1
@SqlKiwi це був спосіб редагування цієї публікації, і це було те, що коментувати ;-)
jcolebrand
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.