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

286

У меня есть таблица 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

Крон каждые 15 минут:

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

Затем он делает некоторые подсчеты, чтобы регистрировать некоторые статистические данные (то есть: пользователи онлайн, посетители онлайн).

Дэвид
источник
Можете ли вы предоставить более подробную информацию о структуре таблицы? Существуют ли какие-либо кластерные или некластерные индексы?
Андерс Абель
13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - Запуск «show innodb status» предоставит полезную диагностику.
Мартин
Не рекомендуется делать синхронную запись в базу данных, когда пользователи переходят на страницу. правильный способ сделать это - сохранить его в памяти, такой как memcache или некоторую быструю очередь, и записать в db с помощью cron.
Nir

Ответы:

292

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

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

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

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

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

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

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

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

  1. Убедитесь, что у вас нет других запросов, которые блокируют доступ более чем к одному ключу одновременно, кроме оператора delete. если вы делаете (и я подозреваю, что вы делаете), закажите их ГДЕ в (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), генерируется исключение взаимоблокировки. Достаточно ли просто повторить тот же оператор Statement.executeUpdate () или вся транзакция теперь отображается и должна быть откатана + перезапустить все, что в ней выполнялось?
Whome
5
если у вас включены транзакции, это все или ничего. если у вас есть какое-либо исключение, это гарантирует, что вся транзакция не имела никакого эффекта. в этом случае вы захотите перезапустить все это.
Омри Ядан
4
Удаление на основе выбора на огромной таблице происходит намного медленнее, чем простое удаление
Thermech
3
Спасибо тебе большое, чувак. Подсказка «операторы сортировки» исправила мои проблемы с блокировками.
Miere
4
@OmryYadan Насколько я знаю, в MySQL вы не можете выбрать подзапрос из той же таблицы, в которой вы делаете ОБНОВЛЕНИЕ. dev.mysql.com/doc/refman/5.7/ru/update.html
artaxerxe
73

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

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

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

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

ewernli
источник
3
Так что, возможно, моя проблема в том, что Пользователь обновил страницу и, таким образом, вызвал ОБНОВЛЕНИЕ записи, в то время как cron пытается выполнить УДАЛЕНИЕ для записи. Тем не менее, я получаю сообщение об ошибке на INSERTS, поэтому cron не будет УДАЛИТЬ записи, которые только что были созданы. Так как же может возникнуть тупик на записи, которую еще предстоит вставить?
Дэвид
Можете ли вы предоставить немного больше информации о таблицах и что именно делают транзакции?
ewernli
Я не понимаю, как может возникнуть тупик, если в транзакции используется только один оператор. Нет других операций над другими таблицами? Нет специальных внешних ключей или уникальных ограничений? Нет ограничений каскадного удаления?
ewernli
Нет, больше ничего особенного ... Я полагаю, это связано с характером использования таблицы. строка вставляется / обновляется при каждом обновлении страницы от посетителя. Около 1000+ посетителей включены одновременно.
Дэвид
12

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

Для начала стоит попробовать явно получить блокировку таблицы сразу для оператора delete. См. LOCK TABLES и Проблемы с блокировкой таблиц .

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

http://dev.mysql.com/doc/refman/5.1/en/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-я параллельная транзакция уже удерживала блокировку SHARED, она вызывала тупик.

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

chatsap
источник
В моем случае, похоже, проблема была в связи с внешним ключом. Thanks1
Chris Prince
3

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

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

Арчи
источник
0

У меня есть метод, внутренности которого обернуты в MySqlTransaction.

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

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

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

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

CINCHAPPS
источник
0

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

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

Также INDEX(datetime)очень важно избегать тупиков.

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

Другая причина для перехода с меньшими кусками - это блокировка меньшего количества строк.

Нижняя граница:

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

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

Рик Джеймс
источник