Как реализовать многопоточное обновление данных без конфликтов?

Пере до мной встала интересная задача, с которой я не могу справиться.
Условие:
Есть таблица с данными. Нужно в многопоточном режиме выбирать из таблицы по одной записи и обновлять количество выборов для нее. Каждая запись должна быть выбрана не больше двух раз. Так-же между выбором и обновлением есть микрозадержка.

На практике есть скрипт, который выполняется в многопоточном режиме и выбирает запись из mysql таблицы innodb, которая была выбрана не более 2-х раз до этого, основываясь на поле count. После выбора записи для нёё, поле count увеличивается на 1.

$connect = mysqli_connect($host, $dbuser, $dbpass);
	if($connect){
		mysqli_select_db($connect, $dbname);
		for ($i = 1; $i <= 10; $i++) {
			//Выбираем подряд записи, у которых count < 2
			$sql = mysqli_query($connect, "SELECT id FROM threadsTest WHERE id >= {$i} AND count < 2 LIMIT 1;");
			if (mysqli_num_rows($sql) > 0){
				$row = mysqli_fetch_assoc($sql);
				//Задержка после выбора 0.1 сек
				usleep(100000);
				//Обновление count
				mysqli_query($connect, "UPDATE threadsTest SET count = count + 1 WHERE id = {$row['id']}");
			}
			
		}
		mysqli_close($connect);
	}


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

Моё решение - после выборки создавать дополнительный файл для записи, если его нет и инкрементить в него с блокировкой счетчик выборок. Сразу после выборки проверять этот файл и если счетчик в нём 2, пробовать выбрать следующую запись. Однако у этого способа есть недостаток. Проверка происходит уже после выборки и приходится перебирать намного большее количество записей. В моем тесте это увеличило время работы в 100 раз и это очень зависимо от количества потоков и количества данных в таблице.

Что можно предпринять в такой ситуации, сохранив логику работы и приемлемую скорость выполнения?
Буду рад любым свежим идеям и предложениям :)

Кому интересно попробовать разобраться в этом, вот тестовый скрипт:
https://yadi.sk/d/mnHA4grn39xBaT

create_table.php - создаёт тестовую таблицу с полями id и count, заполняет её данными
clear_table.php - обнуляет count в тестовой таблице
thread.php - сам скрипт, который делает выборку и инкрементит count
run_threads.sh - bash скрипт, который запускает thread.php в 300 потоков

UPD:
Удалось решить задачу с помощью блокировки строк FOR UPDATE и добавления 2х полей.
Добавил поле lock - tinyint
Добавил поле type - tinyint, добавил type в индекс
Код выше приобрел такой вид:
$connect = mysqli_connect($host, $dbuser, $dbpass);
	if($connect){
		mysqli_select_db($connect, $dbname);
		mysqli_query($connect, "SET AUTOCOMMIT = 0;"); //Отключаем автокоммит
		for ($i = 1; $i <= 10; $i++) {
			$rand = rand(1,10); //Это сделал для ускорения обработки, что бы каждый запущенный скрипт выбирал более-менее разные строки
			mysqli_query($connect, "BEGIN;");//Стартуем транзакцию
			//Выбираем подряд записи, у которых count < 2, type = от 0 до 10, и поле lock = 0, блокируем запись с помощью FOR UPDATE
			$sql = mysqli_query($connect, "SELECT id FROM threadsTest WHERE id >= {$i} AND type = {$rand} AND `lock` = 0 LIMIT 1 FOR UPDATE;");
			if (mysqli_num_rows($sql) > 0){
				//Ставим указатель блокировки вручную, чтобы, пока эта запись обрабатывалась, другие скрипты ее не извлекали
				mysqli_query($connect, "UPDATE threadsTest SET `lock` = 1 WHERE id = {$row['id']};");
				//Коммитим апдейт снимая блокировку с записи
				mysqli_query($connect, "COMMIT;");
				$row = mysqli_fetch_assoc($sql);
				//Задержка после выбора 0.1 сек
				usleep(100000);
				//Обновляем count, снимаем указатель блокировки
				mysqli_query($connect, "BEGIN;");
				mysqli_query($connect, "UPDATE threadsTest SET count = count + 1, `lock` = 0 WHERE id = {$row['id']}");
				mysqli_query($connect, "COMMIT;");
			}
			
		}
		mysqli_close($connect);
	}


Таким макаром удалось добиться выполнения всех скриптов, без превышения count, и с приемлемым временем выполнения.
Если начальный вариант 10 итераций в 300 потоков выполнялся ~1-2 сек., то этот вариант в том же количестве выполняется за ~2-4 сек. Без type = rand ~8-10 сек.
  • Вопрос задан
  • 628 просмотров
Решения вопроса 1
Sanasol
@Sanasol Куратор тега PHP
нельзя просто так взять и загуглить ошибку
Пригласить эксперта
Ответы на вопрос 1
gobananas
@gobananas
finishhim.ru
Что вы понимаете под потоками в данном случае? В PHP же нет тредов в классическом их виде. Просто несколько скриптов одновременно обрабатывают одну таблицу бд?

UPD:
Тогда всё не просто а очень просто. Пишете скрипт в нём запрос который выбирает например 100 записей и обрабатывает их за минуту. И скриптов таких много тогда первый скрипт выбирает:
SELECT * FROM `table` WHERE id='N' LIMIT 0,100
Т.е. первые 100 записей.
Второй скрипт должен выбирать LIMIT 100,200, но вдруг будет какая-то задержка, поэтому для уверенности мы сделаем второму LIMIT 200,300 и всё дальше третий скрипт LIMIT 400,500 и таких скриптов можете сделать сколько угодно пока система под завязку загружена не будет. Они могут запускаться по крону и одновременно выбирать данные не пересекаясь друг с другом.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы