Як уникнути mysql 'Тупик, виявлений при спробі заблокувати; спробуйте перезапустити транзакцію "


286

У мене є таблиця innoDB, яка записує користувачів в Інтернеті. Користувач оновлює його при оновленні кожної сторінки, щоб відстежувати, на яких сторінках вони перебувають, та останню дату доступу до сайту. Потім у мене є крон, який працює кожні 15 хвилин для ВИДАЛЕННЯ старих записів.

У мене виявлено "Тупик, знайдений при спробі заблокувати замок; спробуйте перезапустити транзакцію протягом приблизно 5 хвилин минулої ночі, і, здається, це відбувається під час запуску INSERT в цю таблицю. Хтось може підказати, як уникнути цієї помилки?

=== EDIT ===

Ось такі запити, які виконуються:

Перший візит на сайт:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

На кожній сторінці оновіть:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron кожні 15 хвилин:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Тоді деякі підрахунки реєструють статистику (тобто: учасників в Інтернеті, відвідувачів в Інтернеті).


Чи можете ви надати трохи детальніше про структуру таблиці? Чи є кластерні чи некластеризовані індекси?
Андерс Абель

13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - Запуск "показувати статус двигуна innodb" надасть корисну діагностику.
Мартін

Недоцільно робити синхронну запис у базу даних, коли користувачі переходять на сторінку. правильний спосіб зробити це - зберегти його в пам'яті, такі як memcache або якась швидка черга і записувати на db cron.
Нір

Відповіді:


292

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

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

  • з'єднання 1: ключ блокування (1), ключ блокування (2);
  • з'єднання 2: ключ блокування (2), ключ блокування (1);

Якщо обидва працюють одночасно, з'єднання 1 заблокує ключ (1), з'єднання 2 заблокує ключ (2), і кожне з'єднання буде чекати, коли інший відпустить ключ -> тупик.

Тепер, якщо ви змінили запити таким чином, щоб з'єднання блокували ключі в тому самому порядку, тобто:

  • з'єднання 1: ключ блокування (1), ключ блокування (2);
  • з'єднання 2: ключ блокування ( 1 ), ключ блокування ( 2 );

неможливо отримати тупик.

Ось що я пропоную:

  1. Переконайтеся, що у вас немає інших запитів, які блокують доступ одночасно до декількох клавіш, крім оператора delete. якщо ви це робите (і я підозрюю, що ви це робите), замовте їх WHERE у (k1, k2, .. kn) у порядку зростання.

  2. Виправте заяву видалення, щоб вона працювала у порядку зростання:

Зміна

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

До

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Інша річ, яку слід пам’ятати, - це те, що документація на mysql передбачає, що у випадку тупикової ситуації клієнт повинен повторно спробувати. Ви можете додати цю логіку до коду клієнта. (Скажіть, 3 спроби щодо цієї конкретної помилки перед відмовою).


2
Якщо у мене Transaction (autocommit = false), викид з тупикового зв'язку викинутий. Чи достатньо лише повторити той же оператор.executeUpdate () або ціла транзакція зараз зв'язана, і її слід відкатати + повторити все, що в ній працює?
Кому

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

4
Видалення на основі вибору з величезної таблиці відбувається дуже повільніше, ніж просте видалення
Thermech

3
Дуже дякую, чувак. Порада "сортування висловлювань" вирішила мої проблеми із загиблим блокуванням
Miere

4
@OmryYadan Як я знаю, у MySQL ви не можете вибрати підзапит із тієї ж таблиці, в якій ви робите ОНОВЛЕННЯ. dev.mysql.com/doc/refman/5.7/uk/update.html
artaxerxe

72

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

  • Tx 1: замок A, потім B
  • Tx 2: замок B, потім A

Існують численні запитання та відповіді щодо тупиків. Кожен раз, коли ви вставляєте / оновлюєте / чи видаляєте рядок, замок набувається. Щоб уникнути тупикової ситуації, потрібно переконатися, що паралельні транзакції не оновлюють рядок у порядку, який може призвести до тупикової ситуації. Взагалі кажучи, намагайтеся придбати блокування завжди в одному порядку навіть у різних транзакціях (наприклад, завжди спочатку таблиця A, потім таблиця B).

Ще однією причиною тупика в базі даних можуть бути відсутні індекси . Коли рядок вставляється / оновляється / видаляється, в базі даних потрібно перевірити обмеження реляції, тобто переконатися, що відносини є послідовними. Для цього в базі даних потрібно перевірити зовнішні ключі у відповідних таблицях. Це може призвести до придбання іншого блокування, ніж модифікована рядок. Тоді не забудьте завжди мати індекс на зовнішніх ключах (і звичайно первинних ключів), інакше це може призвести до блокування таблиці замість блокування рядків . Якщо відбудеться блокування таблиці, конфлікт блокування вищий, а ймовірність тупикового зв'язку зростає.


3
Тому, можливо, моя проблема полягає в тому, що Користувач оновив сторінку і, тим самим, запустив ОНОВЛЕННЯ запису одночасно, крон намагається запустити DELETE на записі. Однак я отримую помилку на INSERTS, тож у cron не буде ВИДАЛЕННЯ записів, які були щойно створені. Тож як може статися тупик у записі, який ще потрібно вставити?
Девід

Чи можете ви надати трохи більше інформації про таблицю (я) та про те, що саме роблять транзакції?
ewernli

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

ні, нічого іншого особливого ... Я припускаю, що це до природи використання таблиці. рядок вставляється / оновлюється при оновленні кожної сторінки від відвідувача. У будь-який час відвідують близько 1000+ відвідувачів.
Девід

12

Цілком імовірно, що оператор видалення вплине на велику частку від загальної кількості рядків у таблиці. Врешті-решт це може призвести до придбання блокування таблиці при видаленні. Затримка блокування (у цьому випадку блокування рядків чи сторінок) та придбання більшої кількості блокувань - це завжди ризик глухого кута. Однак я не можу пояснити, чому оператор insert призводить до ескалації блокування - це може бути пов'язано з розділенням / додаванням сторінок, але хтось, хто краще знає MySQL, повинен буде там заповнити.

Для початку варто спробувати явно придбати блокування таблиці відразу для оператора delete. Див. Розділи " Блокування записів" та " Блокування блоків таблиці" .


6

Ви можете спробувати виконати цю deleteроботу, спочатку вставивши ключ кожного рядка, який потрібно видалити, у таблицю темп, як цей псевдокод

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

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

Крім того, змініть свої selectзапити, щоб додати whereпункт, що виключає рядки старші 900 секунд. Це дозволяє уникнути залежності від роботи з cron і дозволяє перенести її на більш рідку роботу.

Теорія про тупикові місця: у MySQL у мене не дуже багато, але тут ідеться ... Це deleteбуде тримати блокування блоку ключів для дат, щоб запобігти доданню рядків, що відповідають його whereпункту в середині транзакції , і коли він знайде рядки для видалення, він спробує придбати блокування на кожній зміненій сторінці. Він insertпридбає блокування на сторінці, в яку він вставляється, а потім спробує придбати блокування ключа. Зазвичай цей insertтерпіння буде терпляче чекати, коли цей ключ блокування відкриється, але це буде тупиком, якщо deleteнамагається заблокувати ту саму сторінку, яку insertвикористовує, тому що deleteпотребує блокування сторінки та insertпотреби блокування ключа. Це не здається правильним для вставок, але deleteіinsert використовують діапазони дат, які не перетинаються, тому, можливо, відбувається щось інше.

http://dev.mysql.com/doc/refman/5.1/uk/innodb-next-key-locking.html


4

У випадку, якщо хтось все ще бореться з цим питанням:

Я зіткнувся з подібною проблемою, коли 2 запити одночасно потрапляли на сервер. Не було такої ситуації, як нижче:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Тож мене спантеличило, чому відбувається тупик.

Тоді я виявив, що між двома столами перебуває шлюбний зв’язок для батьків - через зовнішній ключ. Коли я вставляв запис у дочірню таблицю, транзакція набуває блокування в рядку батьківської таблиці. Відразу після цього я намагався оновити батьківський рядок, який викликав підняття блокування до ЕКСКЛЮЗИВНОГО. Оскільки 2-а паралельна транзакція вже тримала БЕЗПЕЧНИЙ замок, це спричинило тупик.

Зверніться до: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html


І в моєму випадку, схоже, проблема була в зовнішньому ключі. Thanks1
Chris Prince

3

Для Java-програмістів, які використовують Spring, я уникнув цієї проблеми, використовуючи аспект AOP, який автоматично повторює транзакції, що стикаються з тимчасовими тупиками.

Для отримання додаткової інформації див. @RetryTransaction Javadoc.


0

У мене є метод, внутрішність якого загорнута в MySqlTransaction.

Проблема з тупиком виявилася для мене, коли я застосував той самий метод паралельно з собою.

Не було проблеми із запуском жодного примірника методу.

Коли я видалив MySqlTransaction, я зміг запустити метод паралельно сам із собою без проблем.

Просто ділюсь своїм досвідом, я нічого не пропагую.


0

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

Було б краще мати постійно працюючу роботу, яка видалила б рядки, спала деякі, а потім повторила.

Також INDEX(datetime)дуже важливо уникати тупиків.

Але, якщо тест на дату включає більше, ніж, скажімо, 20% таблиці, то DELETEбуде проведено сканування таблиці. Менші шматки, видалені частіше, є вирішенням проблеми.

Ще одна причина, коли ви ходите з меншими шматками - це заблокувати менше рядів.

Нижня лінія:

  • INDEX(datetime)
  • Постійно виконується завдання - видалити, хвилину поспати, повторити.
  • Щоб переконатися, що вищезазначене завдання не загинуло, створіть роботу cron, єдиною метою якої є перезапуск її після відмови.

Інші методи видалення: http://mysql.rjweb.org/doc.php/deletebig

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