PHP: как предотвратить множественное выполнение кода (если он уже выполняется)

Объяснение

Запрос API (на другой сервис), который обычно отвечает за 10-20 секунд, сохраняется в базе данных,

После того, как он будет сохранен, система попытается использовать API мгновенно, чтобы показать результат пользователю, но он может потерпеть неудачу (и покажет, что он потерпел неудачу, но мы попробуем снова автоматически), поэтому существует также набор Cron Job для запуска каждые 30 секунд и повторения запросов (неудачных).

Если API возвращает успех (будь то в режиме мгновенного использования или использования задания Cron), флаг будет изменен на успех в базе данных, и он не будет запущен снова.

Вопрос

Моя проблема заключается в том, что процесс Instant Call to API находится в процессе, Cron Job может также попробовать другой вызов, поскольку он еще не отмечен как успешный,

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

Что я уже пытался предотвратить проблему

Я попытался сохранить вызовы API In Process в таблице базы данных с помощью Status=1 и удалить их, когда вызов API был успешным, или установить статус на 0, если он не удалось,

 if ($status === 0)
 {

     // Set Status to 1 in Database First (or die() if database update failed)

     // Then Call The API

     // If Failed Set Status to 0 so Cron Job can try again

     // If Successful Change Flag to success and remove from queue

 }

Но что делать, если Instant Call и Cron Job Call происходят в одно и то же время? они оба проверяют, есть ли статус 0, который он есть, затем оба устанавливают статус 1 и выполняют вызов API...

Вопросы

  • Я пытаюсь найти правильный способ справиться с этим?

  • Должны ли я беспокоиться о том, что они происходят в точное время (проблема, которую я объяснил в "Желтой цитате" выше), если есть много вызовов (иногда + 500/сек)

Обновить до баунти

Нет ли на самом деле простого способа обработки таких случаев на стороне PHP? если нет, то каким образом лучше мнение экспертов? ниже приведены некоторые методы, но ни один из них не является достаточно подробным, и ни у одного из них нет ни одного ниспадающего/приоритетного.

P.S. В базу данных много обновлений/вставок, я не думаю, что блокировка является эффективной идеей, и я не уверен в остальном.

Ответ 1

Именно поэтому Семафор был создан для.

В php его можно использовать следующим образом: Использование семафоров в PHP на самом деле очень прямолинейно. Есть только 4 функции семафора:

sem_acquire() – Attempt to acquire control of a semaphore.
sem_get() – Creates (or gets if already present) a semaphore.
sem_release() – Releases the a semaphore if it is already acquired.
sem_remove() – Removes (deletes) a semaphore.

Итак, как они все работают вместе? Во-первых, вы вызываете sem_get() для получения идентификатора для семафора. После этого один из ваших процессов вызовет sem_acquire(), чтобы попытаться получить семафор. Если его в настоящее время недоступно, sem_acquire() будет блокироваться до тех пор, пока семафор не будет освобожден другим процессом. После получения семафора вы можете получить доступ к ресурсу, с которым вы контролируете его. После того, как вы закончите работу с ресурсом, вызовите sem_release(), чтобы другой процесс мог получить семафор. Когда все сказано и сделано, и вы убедились, что ни один из ваших процессов больше не требует семафора, вы можете вызвать sem_remove(), чтобы полностью удалить семафор.

Более подробную информацию и пример можно найти в этой статье.

Ответ 2

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

Итак, вы можете выбирать задачи из таблицы очередей следующим образом:

LOCK TABLES table WRITE;
SELECT * FORM table WHERE status = 0 LIMIT 1;
set status = 1 for the selected row
UNLOCK TABLES;

Блокировка таблицы гарантирует, что другие процессы не будут выполнять SELECT и не будут получать одну и ту же строку из таблицы.

Вставка задания в очередь так же просто:

INSERT INTO table (job_id, status) VALUES(NULL, status);

Удаление задания после завершения обработки:

DELETE FROM table WHERE job_id = 12345;

Ответ 3

что я делаю в скриптах (Псевдокод)

SCRIPT START
LOCK FILE 'MYPROCESSFILE.LOCK'
DO SOMETHING I WANT
UNLOCK FILE 'MYPROCESSFILE.LOCK'
SCRIPT END

Итак, если файл заблокирован, второй (дублированный) процесс не будет запускаться (будет заблокирован/остановлен/ждать). UNTIL файл НЕ РАЗБЛОКИРОВАН исходным процессом.

EDIT, обновленный с помощью кода WORKING PHP

<?php

    class Locker {

        public $filename;
        private $_lock;

        public function __construct($filename) {
            $this->filename = $filename;
        }

        /**
         * locks relevant file
         */
        public function lock() {
                touch($this->filename);
                $this->_lock = fopen($this->filename, 'r');
                flock($this->_lock, LOCK_EX);
        }

        /**
         * unlock above file
         */
        public function unlock() {
                flock($this->_lock, LOCK_UN);
        }

    }

    $locker = new Locker('locker.lock');
    echo "Waiting\n";
    $locker->lock();
    echo "Sleeping\n";
    sleep(30);
    echo "Done\n";
    $locker->unlock();

?>

Ответ 4

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

Ответ 5

Поскольку вы должны знать времена, в которые будет выполняться cron (скажем, каждые 5 минут), то для запрошенной пользователем функции вы можете проверить, действительно ли системное время, когда должен работать cron? Это не помешало бы им работать в одно и то же время.

Ответ 6

Я использую это в Linux, чтобы увидеть, работает ли script, когда нужно избегать нескольких действий:

$output = array();
exec('pgrep -fl the_script.php', $output);

Затем сканируйте $output и определите, выполняется ли она.

Например, вот копия/вставка существующего кода:

$exec_output = array();
exec('pgrep -fl archiver.php', $exec_output);
$pid_count = 0;
foreach ($exec_output as $line) {
    $parts = explode(' ', $line);
    if (basename($parts[2]) == 'archiver.php') $pid_count++;
}

Затем выполните действия, основанные на $pid_count. Проверка basename() заключается в том, чтобы убедиться, что я не поймаю какую-либо другую вещь, например special_archiver.php или что-то еще. Вы также можете проверить полный путь.

Ответ 7

Семафоры могут быть установлены в php, а для управления сигналами на уровне ядра он будет управлять процессом блокировки атомарно. Unix был разработан для использования этого механизма наряду с другими методами, такими как сигналы для межпроцессного общения. Не уверен, что вам нужно получить это сложное.

Он может работать, глядя на выход ps -ef, но может быть подвержен загрузке системы и приоритету процесса. Вы можете обнаружить, что он работает с использованием флага базы данных, но зачем добавлять накладные расходы? Базы данных могут быть заняты.

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

например. если cron script начинается с

if ( ! -f otherprocessisrunning)
then
   // create/open the file
   > cronprocessisrunning

   // when cron process finishes
   // it removes the cronprocessisrunning file
   rm -f cronprocessisrunning
else 
   sleep for 2 minutes
   call this function
fi

а другой script имеет такое же поведение в php, что и это

if (! file_exist(cronprocessisrunning))
    > otherprocessisrunning
    start the other process
    when it is finished, remove otherprocessisrunning
endif

Он должен быть достаточно быстрым, поскольку создание дескрипторов файлов (без содержимого) преобразуется в простой системный вызов. Если это не так, попробуйте в оболочке bash.

Ответ 8

Я не знаю, может ли это быть хорошим способом:

temp_queue Table
-----------------------
id --> Int, Index, Autoincrement
query_id --> Int (your query ID or something to identificate a specific query)
in_use_by --> varchar (cron or api)

Задача Cron:

Script запускается

SELECT in_use_by FROM temp_queue ORDER_BY id ASC LIMIT 1;

if results != 0 return;

INSERT INTO temp_queue SET query_id=SOME_ID, in_use_by = 'cron';
SELECT in_use_by FROM temp_queue ORDER_BY id ASC LIMIT 1;

Затем проверьте последние результаты SELECT

if in_use_by == 'cron' continue
else return

Когда выполнение заканчивается:

DELETE FROM temp_queue WHERE query_id=SOME_ID

Работа с API:

Script запускается

SELECT in_use_by FROM temp_queue ORDER_BY id ASC LIMIT 1;

if results != 0 return;

INSERT INTO temp_queue SET query_id=SOME_ID, in_use_by = 'api';
SELECT in_use_by FROM temp_queue ORDER_BY id ASC LIMIT 1;

Затем проверьте последние результаты SELECT

if in_use_by == 'api' continue
else return

Когда выполнение заканчивается:

DELETE FROM temp_queue WHERE query_id=SOME_ID

Что произойдет, если Cron Job и API попытаются вызвать запрос в одно и то же время? Они оба проведут проверку 1-й строки с запросом_ID = SOME_ID, поэтому только 1 из них с продолжением.

Да, много вариантов, вставляет и удаляет. Но он работает.

Что вы, ребята, думаете об этом?