MySQL тупик при обновлении

0

Мне нужно выбрать, сделать манипуляции и обновить большое количество данных менее 3 минут. И было решено создать какой-то механизм блокировки, чтобы сделать возможность запуска отдельных процессов (параллельно), и каждый процесс должен блокировать, выбирать и обновлять собственные строки.

Чтобы было возможно, было решено добавить столбец worker_id в таблицу.

Структура таблицы:

CREATE TABLE offers
(
    id int(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    offer_id int(11) NOT NULL,
    offer_sid varchar(255) NOT NULL,
    offer_name varchar(255),
    account_name varchar(255),
    worker_id varchar(255),
);
CREATE UNIQUE INDEX offers_offer_id_offer_sid_unique ON offers (offer_id, offer_sid);
CREATE INDEX offers_offer_id_index ON offers (offer_id);
CREATE INDEX offers_offer_sid_index ON offers (offer_sid);

Кроме того, мы решили начать с 5 параллельных процессов и не допускать выбор одной и той же строки различными процессами, мы используем формулу: offer_id % max_amount_of_processes = process_number (process_number, начиная с 0, поэтому сначала 0, а последний - 4)

Каждый процесс выполняет следующие шаги:

  1. set worker_id с текущим идентификатором процесса для первых 1000 строк с использованием запроса: update offers set worker_id =: process_id where worker_id is null and offer_id%5 =: process_number order by offer_id asc limit 1000
  2. выберите эти строки: select * from offers where worker_id =: process_id order by offer_id asc limit 1000
  3. делать манипуляции с данными, хранить последние offer_id переменной и готовые данные к другой переменной для дальнейшего обновления
  4. запустите тот же запрос с шага 1, чтобы заблокировать следующие 1000 строк
  5. запустите тот же запрос, что и на шаге 2, с дополнительным предложением and offer_id > :last_selected_id чтобы выбрать следующие 1000 строк
  6. выполните те же шаги в цикле, пока мы не заблокируем все строки
  7. удалить все update offers set worker_id = null where worker_id =: process_id блокировок update offers set worker_id = null where worker_id =: process_id
  8. выполнить запрос для обновления всех собранных данных

и те же шаги для других 4 процессов

Проблема здесь в том, что я зашел в тупик, когда все 5 процессов одновременно запускают запрос с шага 1 для блокировки строк (set worker_id), но каждый процесс делает блокировку для собственных строк, которые в зависимости от формулы. Я попытался установить уровень изоляции транзакций READ COMMITED но все тот же вопрос.

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

  • 0
    MySQL позволяет использовать транзакции для предотвращения одновременных обновлений. вставки и т. д. Кроме того, я бы предложил не использовать varchars для ID, а использовать целые числа, такие как внешний ключ для таблицы users.
  • 0
    так вы предлагаете обернуть шаг 1 и 2 транзакцией? о каком id ты говоришь? offer_sid - это varchar, потому что я получаю строку из API, а worker_id - это varchar, потому что process_id - это строка
Показать ещё 3 комментария
Теги:
deadlock

2 ответа

1

Выражение offer_id%5 = :process_number не может использовать индекс, поэтому он может проверять только все строки, совпадающие с первым условием, worker_id is null.

Вы можете доказать это двумя окнами:

mysql1> begin;
mysql1> set @p=1;
mysql1> update offers set worker_id = @p where worker_id is null and offer_id%5 = @p;

Не совершайте транзакцию в окне 1.

mysql2> set @p=2;
mysql2> update offers set worker_id = @p where worker_id is null and offer_id%5 = @p;
...waits for about 50 seconds, or value of innodb_lock_wait_timeout, then...
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

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

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


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

Я не уверен, потому что вы не опубликовали диагностику взаимоблокировки InnoDB от SHOW ENGINE INNODB STATUS.

Я замечаю, что в вашей таблице есть дополнительный UNIQUE KEY, который также потребует блокировок. Есть некоторые случаи взаимоблокировок, которые происходят из-за неатомичности назначения блокировки.

Worker 1                               Worker 2            

UPDATE SET worker_id = 1
(acquires locks on PK)

                                        UPDATE SET worker_id = 2
                                        (waits for PK locks held by worker 1)

(waits for locks on UNIQUE KEY)

Поэтому рабочий 1 и рабочий 2 могут ждать друг друга и вступать в тупик.

Это просто догадка. Другая возможность заключается в том, что ORM делает второй UPDATE для столбца updated_at, и это открывает еще одну возможность для состояния гонки. Я не совсем отработал это умственно, но я думаю, что это возможно.

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


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

Вы можете решить обе проблемы, блокировку и балансировку нагрузки:

Измените столбцы таблицы следующим образом:

ALTER TABLE offers
  CHANGE worker_id work_state ENUM('todo', 'in progress', 'done') NOT NULL DEFAULT 'todo',
  ADD INDEX (work_state),
  ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  ADD INDEX (updated_at);

Создайте ОДИН процесс, который периодически читает из таблицы, и добавляет значения id первичного ключа в состоянии "todo" в очередь сообщений. Все предложения, независимо от их значения offer_id, попадают в очередь одинаково.

SELECT id FROM offers WHERE work_state = 'todo'
/* push each id onto the queue */

Затем каждый из рабочих может вывести один id за раз из очереди сообщений. Рабочий выполняет следующие шаги с каждым идентификатором:

  1. UPDATE offers SET work_state = 'in progress' WHERE id = :id

  2. Работник выполняет работу по одному предложению.

  3. UPDATE offers SET work_state = 'done' WHERE id = :id

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

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

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

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

Там шанс, что работник потерпит неудачу во время своей работы и никогда не будет отмечать предложение "сделано". Вы должны периодически проверять наличие сиротских предложений. Предположите, что они не будут завершены, и отметьте их состояние "todo".

UPDATE offers SET work_state = 'todo' 
WHERE work_state = 'in progress' AND updated_at < NOW() - INTERVAL 5 MINUTE

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

  • 0
    Спасибо за отличное объяснение! 1. Как я уже говорил, проблема исчезла, когда я прекратил обновлять поля меток времени, поэтому тупиков больше нет.
  • 0
    2. Я не думаю, что это хорошая идея использовать MQ здесь, потому что предложения выбираются из внешнего API (другим процессом) и сохраняются в БД. И у API есть несколько конечных точек для обновления различных полей, а документация по API рекомендует обновлять поля предложений каждые 3 минуты. Но API позволяет совершать вызов API только для одного предложения одновременно. Первоначальная проблема заключалась в том, что синхронные вызовы API выполнялись более чем за 3 минуты, поэтому мне нужно отправлять несколько асинхронных вызовов API одновременно, и это невозможно с MQ, где каждое предложение обслуживается индивидуально.
Показать ещё 3 комментария
0

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню