Мне нужно выбрать, сделать манипуляции и обновить большое количество данных менее 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)
Каждый процесс выполняет следующие шаги:
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
select * from offers where worker_id =: process_id order by offer_id asc limit 1000
offer_id
переменной и готовые данные к другой переменной для дальнейшего обновленияand offer_id > :last_selected_id
чтобы выбрать следующие 1000 строкupdate offers set worker_id = null where worker_id =: process_id
блокировок update offers set worker_id = null where worker_id =: process_id
и те же шаги для других 4 процессов
Проблема здесь в том, что я зашел в тупик, когда все 5 процессов одновременно запускают запрос с шага 1 для блокировки строк (set worker_id
), но каждый процесс делает блокировку для собственных строк, которые в зависимости от формулы. Я попытался установить уровень изоляции транзакций READ COMMITED
но все тот же вопрос.
Я новичок в механизме блокировки, и мне нужна помощь для предотвращения взаимоблокировок здесь или для создания лучшего механизма
Выражение 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
за раз из очереди сообщений. Рабочий выполняет следующие шаги с каждым идентификатором:
UPDATE offers SET work_state = 'in progress' WHERE id = :id
Работник выполняет работу по одному предложению.
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
Выберите длину интервала, чтобы было уверенно, что любой рабочий закончил бы это к тому времени, если что-то не пошло не так. Вы, вероятно, сделаете это "перезагрузку" до того, как диспетчер запросит текущие предложения, поэтому заявки, которые были забыты, будут переупознаны.
Я нашел проблему. Это связано с тем, что мой ORM по умолчанию обновляет поля метки времени (чтобы упростить пример выше, я удалил их из структуры таблицы) во время выполнения операции обновления, и после того, как я отключил его, тупик исчез. Но все же я не понимаю, как поле updated_at
может вызвать проблему, поскольку я все еще обновляю другие поля
offer_sid
- это varchar, потому что я получаю строку из API, аworker_id
- это varchar, потому что process_id - это строка