Как избежать обнаружения тупика MySQL при попытке получить блокировку; попробуйте перезапустить транзакцию '

У меня есть таблица innoDB, в которой записываются онлайн-пользователи. Он обновляется при каждом обновлении страницы пользователем, чтобы отслеживать, на каких страницах они находятся, и дату их последнего доступа к сайту. Затем у меня есть cron, который запускается каждые 15 минут для УДАЛЕНИЯ старых записей.

Я обнаружил тупик при попытке получить блокировку; попробуйте перезапустить транзакцию примерно на 5 минут прошлой ночью, и это похоже на запуск INSERT в эту таблицу. Может кто-нибудь подсказать, как избежать этой ошибки?

=== РЕДАКТИРОВАТЬ ===

Вот выполняемые запросы:

Первое посещение сайта:

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

Затем он делает некоторые подсчеты для регистрации некоторой статистики (например: участники онлайн, посетители онлайн).


person David    schedule 25.02.2010    source источник
comment
Не могли бы вы подробнее рассказать о структуре таблицы? Есть ли кластерные или некластеризованные индексы?   -  person Anders Abel    schedule 06.03.2010
comment
dev.mysql.com/doc/refman/5.1/en/ innodb-deadlocks.html - Запуск демонстрационного движка innodb status предоставит полезную диагностику.   -  person Martin    schedule 07.03.2010


Ответы (8)


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

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

  • соединение 1: ключ для замков (1), ключ для замков (2);
  • соединение 2: ключ для замков (2), ключ для замков (1);

Если оба работают одновременно, соединение 1 заблокирует ключ (1), соединение 2 заблокирует ключ (2), и каждое соединение будет ждать, пока другое соединение отпустит ключ - ›тупик.

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

  • соединение 1: ключ для замков (1), ключ для замков (2);
  • соединение 2: блокирует ключ (1), блокирует ключ (2);

зайти в тупик будет невозможно.

Вот что я предлагаю:

  1. Убедитесь, что у вас нет других запросов, которые блокируют доступ к более чем одному ключу одновременно, за исключением оператора удаления. если вы это сделаете (и я подозреваю, что вы это сделаете), закажите их WHERE in (k1, k2, .. kn) в порядке возрастания.

  2. Исправьте оператор удаления, чтобы он работал в порядке возрастания:

Изменять

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

To

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

Также следует иметь в виду, что документация MySQL предполагает, что в случае тупиковой ситуации клиент должен автоматически повторить попытку. вы можете добавить эту логику в свой клиентский код. (Скажем, 3 попытки исправить эту конкретную ошибку, прежде чем отказаться).

person Omry Yadan    schedule 11.03.2010
comment
Если у меня есть транзакция (autocommit = false), возникает исключение взаимоблокировки. Достаточно ли просто повторить тот же оператор ..executeUpdate () или вся транзакция теперь заблокирована и ее нужно откатить + перезапустить все, что в ней выполнялось? - person Whome; 16.09.2014
comment
если у вас включены транзакции, все или ничего. если у вас было какое-либо исключение, гарантируется, что вся транзакция не возымела никакого эффекта. в этом случае вы захотите перезапустить все это. - person Omry Yadan; 16.09.2014
comment
Удаление на основе выбора в огромной таблице происходит очень медленнее, чем простое удаление. - person Thermech; 24.12.2014
comment
Спасибо тебе большое, чувак. Совет «Сортировка операторов» устранил мои проблемы с мертвой блокировкой. - person Miere; 26.06.2015
comment
@OmryYadan Насколько я знаю, в MySQL вы не можете выбирать в подзапросе из той же таблицы, в которой вы делаете ОБНОВЛЕНИЕ. dev.mysql.com/doc/refman/5.7/en/update.html - person artaxerxe; 09.12.2015
comment
Не удалось использовать этот ответ bc того, что упомянул @artaxerxe, но dasunhegoda.com/ исправил это. - person dragonflyesque; 16.02.2016
comment
@artaxerxe При выполнении запроса на обновление вы можете inner join подзапрос с таблицей using(id), затем вы выбираете по подзапросу, но обновляете фактическую таблицу UPDATE mytable t INNER JOIN (SELECT id FROM mytable WHERE datetime <= NOW() ORDER BY id) t2 USING(id) SET t.datetime = NOW(). Думаю, тот же трюк можно применить и к удалению DELETE t FROM mytable t INNER JOIN (SELECT id FROM mytable WHERE datetime <= NOW() ORDER BY id) t2 USING(id) - person Timo Huovinen; 06.04.2016
comment
как сортировка элементов в запросе на удаление устраняет тупик? - person a.valchev; 16.05.2018
comment
В MySQL невозможно удалить и выбрать в той же таблице. - person diegodsp; 25.10.2019
comment
DELETE FROM table WHERE id IN (SELECT id FROM table ...) завершается ошибкой с OperationalError: (1093, "You can't specify target table 'table' for update in FROM clause") - person CordiS; 15.02.2021
comment
Объяснение вводит в заблуждение, лучше прочтите dev.mysql.com/doc /refman/8.0/en/innodb-deadlocks.html - person es cologne; 16.03.2021

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

  • Tx 1: заблокировать A, затем B
  • Tx 2: заблокировать B, затем A

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

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

person ewernli    schedule 25.02.2010
comment
Так что, возможно, моя проблема в том, что пользователь обновил страницу и, таким образом, запустил ОБНОВЛЕНИЕ записи в то же время, когда cron пытается запустить DELETE для записи. Однако я получаю сообщение об ошибке INSERTS, поэтому cron не будет УДАЛЯТЬ только что созданные записи. Так как же может произойти тупиковая ситуация с записью, которую еще предстоит вставить? - person David; 25.02.2010
comment
Не могли бы вы предоставить немного больше информации о таблице (таблицах) и о том, что именно делают транзакции? - person ewernli; 25.02.2010
comment
Я не понимаю, как может произойти тупик, если для каждой транзакции используется только один оператор. Никаких других операций с другими таблицами? Никаких специальных внешних ключей или уникальных ограничений? Нет ограничений на каскадное удаление? - person ewernli; 25.02.2010
comment
нет, ничего особенного ... Полагаю, дело в характере использования таблицы. строка вставляется / обновляется при каждом обновлении страницы от посетителя. Одновременно подключено около 1000+ посетителей. - person David; 04.03.2010

Вероятно, что оператор удаления повлияет на большую часть всех строк в таблице. В конечном итоге это может привести к блокировке таблицы при удалении. Удержание блокировки (в данном случае блокировки строк или страниц) и получение дополнительных блокировок всегда сопряжены с риском взаимоблокировки. Однако я не могу объяснить, почему оператор вставки приводит к эскалации блокировки - это может быть связано с разделением / добавлением страницы, но кто-то, лучше знающий MySQL, должен будет это заполнить.

Для начала, возможно, стоит попытаться сразу же явно получить блокировку таблицы для оператора удаления. См. ТАБЛИЦЫ БЛОКИРОВКИ и Проблемы с блокировкой таблиц.

person Anders Abel    schedule 06.03.2010

Если кто-то все еще борется с этой проблемой:

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

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

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

Итак, я был озадачен, почему происходит тупик.

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

См .: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html

person chatsap    schedule 26.02.2019
comment
В моем случае тоже похоже, что проблема связана с внешним ключом. Спасибо1 - person Chris Prince; 27.05.2019
comment
Для меня то же самое: одна таблица обновлена, при этом внешний ключ в другой таблице обновлен. Я удаляю свой контрастный ключ без действия. И тут, чудо, больше никаких тупиков! Конечно, он больше не проверяет контраст, но его нельзя было использовать в больших обновленных таблицах. Большое спасибо ! - person Patrice G; 15.12.2020

Вы можете попробовать запустить это задание 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 будет удерживать блокировку диапазона ключей для datetime, чтобы строки, соответствующие его предложению where, не добавлялись в середине транзакции, и по мере того, как она находит строки для удаления, она будет пытаться получить блокировку на каждой изменяемой странице. insert собирается получить блокировку страницы, в которую он вставляется, а затем затем попытается получить блокировку ключа. Обычно insert будет терпеливо ждать, пока откроется блокировка этой клавиши, но это зайдет в тупик, если delete попытается заблокировать ту же страницу, которую использует insert, потому что delete нужна эта блокировка страницы, а insert нужна эта блокировка. Это кажется неправильным для вставок, delete и insert используют диапазоны даты и времени, которые не перекрываются, поэтому, возможно, происходит что-то еще.

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

person Brian Sandlin    schedule 11.03.2010

Для Java-программистов, использующих Spring, я избежал этой проблемы, используя аспект АОП, который автоматически повторяет транзакции, которые приводят к временным тупикам.

См. @RetryTransaction Javadoc для получения дополнительной информации.

person Archie    schedule 29.06.2013

У меня есть метод, внутренняя часть которого заключена в MySqlTransaction.

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

Не было проблем с запуском одного экземпляра метода.

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

Просто делюсь своим опытом, я ничего не защищаю.

person BitsAndBytes    schedule 04.08.2018

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

Было бы лучше иметь постоянно работающее задание, которое удаляло бы некоторые строки, некоторые засыпало, а затем повторяло.

Кроме того, INDEX(datetime) очень важен для предотвращения тупиковых ситуаций.

Но если тест datetime включает, скажем, более 20% таблицы, DELETE выполнит сканирование таблицы. Чанки меньшего размера удаляются чаще - это временное решение.

Еще одна причина использования меньших блоков - блокировка меньшего количества строк.

Нижняя линия:

  • INDEX(datetime)
  • Постоянно запущенная задача - удалить, поспать минутку, повторить.
  • Чтобы убедиться, что вышеуказанная задача не умерла, создайте задание cron, единственная цель которого - перезапустить его в случае сбоя.

Другие методы удаления: http://mysql.rjweb.org/doc.php/deletebig

person Rick James    schedule 05.12.2019