Избежать конкуренции при многопоточной обработке данных в БД

Фанат

oncle terrible
Команда форума
А как оно поможет в данном случае?
Я так понимаю, что блокировка на запись не выполнит задачу не дать другому воркеру прочитать строку. То есть он точно так же прочитает ту же самую строку и точно так же начнет её обарабывать.
Но при этом если он вдруг закончит работать раньше, чем первый, то вот для записи результата будет как раз ждать.
В итоге мы не имеем ускорения, а имеем потенциальное замедление.
Где я неправ?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
выполнит, это advisory write lock, он эксклюзивный, если используется всеми участниками
без лока прочитать можно, но в 2 потоках захватить лок нельзя
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
блокировка на запись выполнит задачу не дать другому воркеру прочитать строку, и не начать её обарабывать, что бы это ни значило
 

Фанат

oncle terrible
Команда форума
блокировка на запись выполнит задачу не дать другому воркеру прочитать строку, и не начать её обарабывать, что бы это ни значило
Ну так задача изначально - избежать конкуренции, а не поставить всё колом :)
Гриш, я тут конечно самый дурак из вас - второй день подряд в поте лица вытыкаю в пост фиксера и сопуствтующие материалы.
Но некий здравый смысл "от сохи" во мне есть.
И всякую задачу я пытаюсь решать целиком, а не по отдельным ключевым словам.

В данном случае задача - не "не заблокировать чтение любой ценой", а "повысить эффективность многопоточной обработки" :)
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
не прибедняйся, более сложной темы, чем конкурентная обработка, еще поискать
вопрос настолько глубокий, что тут можно писать докторскую и посвятить этому жизнь
 

WMix

герр M:)ller
Партнер клуба
я так понял, что после commit `data` будет равна 2 а значит SELECT id FROM `post` WHERE `data` = '1' LIMIT 1; вернет другую строку
 

WMix

герр M:)ller
Партнер клуба
Код:
CREATE TABLE `test1` (
  `id` int(11) NOT NULL,
  `data` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `test1` (`id`, `data`) VALUES
(1, 1),
(2, 1);

ALTER TABLE `test1`
  ADD PRIMARY KEY (`id`);
Код:
#1 thread
mysql> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0,00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0,00 sec)

mysql> select * from test1 where data=1 limit 1 for update;
+----+------+
| id | data |
+----+------+
|  1 |    1 |
+----+------+
1 row in set (0,00 sec)
#
# выполняем #2thread
#
mysql> update test1 set data=2 where id=1;
Query OK, 1 row affected (0,00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0,03 sec)

mysql>
Код:
#2 thread
mysql> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0,00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0,00 sec)

mysql> select * from test1 where data=1 limit 1 for update;
#
# ОЖИДАЕТ
# вывод после commit из #1thread
+----+------+
| id | data |
+----+------+
|  2 |    1 |
+----+------+
1 row in set (34,30 sec)

mysql>
REPEATABLE READ кстати достаточно
 
Последнее редактирование:

Фанат

oncle terrible
Команда форума
Ну вот в этом "ОЖИДАЕТ" ведь всё и дело!
Нам как раз не нужно чтобы ожидало! Нам надо чтобы летало! Чтобы разные воркеры не цепляли работу друг друга.

В идеале, нам надо чтобы select * from test1 where data=1 вообще не показывала те строки, которые уже кем-то залочены. Это делает SKIP LOCKED в 8-ке.
Но при этом все равно каждый селект будет лочить пол-таблицы. И нафига это надо?

Поэтому тупое шардирование мне кажется будет решать проблему куда лучше.
 

Фанат

oncle terrible
Команда форума
вопрос настолько глубокий, что тут можно писать докторскую и посвятить этому жизнь
Гриш, ну в данном случае вопрос не в диссертации а в небольшом усилии чтобы вникнуть в задачу :)
Которая состоит не в том чтобы прочитать определенную строку, а в том что мы читаем в несколько потоков случайные строки, и чтобы потоки не делали работу друг друга. То есть мы при чтении заранее не знаем, какую строку мы хотм залочить.
 

WMix

герр M:)ller
Партнер клуба
вообще яб посоветовал читать queue одним потоком а после когда уже знаешь id создавать параллельные задачи и передавать туда id
 

fixxxer

К.О.
Партнер клуба
Скажите, я тут в более-менее правильном направлении разглагольствовал?
https://qna.habr.com/q/700342
Почти.

В варианте со статусом надо либо транзакцию с SELECT FOR UPDATE, либо UPDATE ... SET status = 'processing' WHERE status = 'queued', проверять affected rows и пробовать заново, если 0.

Впрочем, учитывая, что в случае с транзакцией, строго говоря, тоже надо делать catch на дедлок и, если что, повторять - то второй вариант проще и эффективнее.

Еще остается вопрос, что делать, если воркер почему-то упал, нажали кнопку reset, пропало питание и так далее. Чтобы ничего не "залипло" в processing, имеет смысл вместо статуса processing делать timestamp, и сбрасывать его, если уж явно не может быть так долго. Ну или и статус и timestamp, если база не умеет индекс на timestamp is null (современные версии mysql умеют через virtual column).

Кстати, да, индексы! тут надо аккуратно. Я вот, помню, такую очередь ускорил почти в 100 раз, просто создав правильные индексы и удалив неправильные.
 

fixxxer

К.О.
Партнер клуба
Поэтому тупое шардирование мне кажется будет решать проблему куда лучше.
У тупого шардирования есть недостаток: не получится просто так добавить пачку воркеров. Хорошо, конечно, если количество воркеров заранее планируется в соответствии с ростом нагрузки, но обычно воркеры добавляются, когда "блин, а че у нас очереди растут и растут?".

Можно сделать чуть более умное шардирование - циферок с запасом и какой-нибудь диапазон каждому. Но мне обычно варианта со статусом хватало.
 

WMix

герр M:)ller
Партнер клуба
либо UPDATE ... SET status = 'processing' WHERE status = 'queued',
тут кстати не хватает информации worker_id=42 или чтото в этом роде, иначе не понятно какой из 2х "processing" принадлежит конкретному worker. при этом worker_id=42 - уж очень странная информация
 

fixxxer

К.О.
Партнер клуба
а не пофиг, какой воркер?

(ну понятное дело что where id=$id я опустил, это на одну операцию в очереди)
 

S.Chushkin

Пофигист
Скажите, я тут в более-менее правильном направлении разглагольствовал?
https://qna.habr.com/q/700342
Нет.
Точнее, если просто потренировать мозги, то возможно. Если про решение задачи, то нет.
Задача проще пареной репы. И сводится к получению изменённой записи (её ИД).
Как известно, с прошлого века, - железобетонный вариант это использовать GUID как идентификатор изменённой записи. Далее делай с ней что хочется.
Что-то вроде:
PHP:
update table set
guid = {$GUID}
where ... limit 1;
... много кода ...
update table set
....
guid = null
where guid = {$GUID};
 
Сверху