Laravel как предотвратить одновременную обработку запросов, отправленных одним и тем же пользователем - PullRequest
2 голосов
/ 18 апреля 2020

У меня есть следующий метод контроллера, который создает новый ордер, если и только если нет открытых ордеров (открытый ордер имеет статус = 0, закрытый ордер имеет статус = 1) ,

public function createOrder(Request $req){
    // some validation stuff
    $last_active = Orders::where('user_id', $this->user->id)->where('status', 0)->orderBy('id', 'desc')->first();
    if ($last_active){
        return ['status' => 'error'];
    }
    $order= Orders::create([
        'status' => 0
                // some details
        ]);
    return ['status' => 'success'];
}

Этот метод привязан к определенному маршруту

Route::post('/create', 'OrderController@create');

Клиент отправляет ajax запросов на этот маршрут. Логика c очень проста: я хочу, чтобы у пользователя был только один активный ордер за раз, поэтому пользователь должен выполнить некоторые действия, чтобы закрыть предыдущий ордер, прежде чем создавать новый. Следующий код прекрасно работает в случае обычного пользователя, но не в случае пользователя, который хочет навредить моему приложению. Так вот в чем проблема. Когда пользователь отправляет тонны таких запросов в секунду (я делаю это только в консоли Google Chrome dev с помощью следующего скрипта)

for (var i = 0; i < 20; i++) 
    setTimeout(function(){
        $.ajax({
                url : '/create',
                type : 'post', 
            success: function(d){
                console.log(d)
            }
        })
}, 1);

Это вызывает несколько записей с status = 0, вставленных в База данных, когда ожидается, что только одна будет вставлена, а другие не должны. ИМО, что происходит:

  1. Многие запросы касаются веб-сервера (nginx в моем случае)
  2. Веб-сервер создает множество PHP процессов (через php -fpm в моем случае) )
  3. Несколько PHP процессов запускают метод одновременно, передавая if ($ last_active) {...} одновременно, до того, как какая-то запись будет вставлена ​​в другой процесс, что приведет к вставлено несколько записей.

Что я пытался это исправить:

  1. На nginx стороне, я ограничиваю частоту запросов (10 r / s). Это не очень помогает, потому что все же позволяет очень быстро отправить 10 запросов с очень небольшой задержкой между ними, прежде чем отклонить их. Я не могу установить предельное значение скорости ниже 10 об / с, потому что это повредит обычным пользователям
  2. На стороне laravel, я попытался совершить транзакцию
public function createOrder(Request $req){
    // some validation stuff
    DB::beginTransaction();
    try{
        $last_active = Orders::where('user_id', $this->user->id)->where('status', 0)->orderBy('id', 'desc')->first();
        if ($last_active){
            DB::rollBack();  // i dont think i even need this
            return ['status' => 'error'];
        }
        $order= Orders::create([
            'status' => 0
                    // some details
            ]);
        DB::commit();
    }
    catch (\Exception $e){
        DB::rollBack();
        return ['status' => 'error'];
    }
    return ['status' => 'success'];
}

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

Я создал промежуточное ПО, которое отслеживает, когда последний пользовательский запрос был сделан, и сохраняет эту информацию в сеансе
   public function handle($request, Closure $next)
    {
        if ((session()->has('last_request_time') && (microtime(true) - session()->get('last_request_time')) > 1)
            || !session()->has('last_request_time')){
            session()->put('last_request_time', microtime(true));
            return $next($request);
        }
       return abort(429);
    }

Это не помогло вообще, потому что оно просто перемещает проблему на промежуточном ПО. уровень

Я также попробовал кое-что странное:
public function createOrder(Request $req){
    if (Cache::has('action.' . $this->user->id)) return ['status' => 'error'];
        Cache::put('action.' . $this->user->id, '', 0.5);
    // some validation stuff
    $last_active = Orders::where('user_id', $this->user->id)->where('status', 0)->orderBy('id', 'desc')->first();
    if ($last_active){
        Cache::forget('action.' . $this->user->id);
        return ['status' => 'error'];
    }
    $order= Orders::create([
        'status' => 0
                // some details
        ]);
    Cache::forget('action.' . $this->user->id);
    return ['status' => 'success'];
}

Этот вид работает во многих случаях, особенно в сочетании с транзакцией, но иногда он позволяет вставлять до 2 строк (в 1- 2 случая из 30). А также это выглядит странно для меня. Я думал об очередях, но поскольку laravel do c заявляет, что они предназначены для трудоемких задач. Также я подумал о блокировке таблицы, но это также кажется странным и влияет на производительность для обычных пользователей. Я считаю, что существует простое и простое решение этой проблемы, но я не могу найти ничего разумного в Google, может быть, я упускаю что-то супер очевидное? Можете ли вы помочь, пожалуйста? Кроме того, в моем приложении так много подобных случаев, что я действительно хочу найти какое-то общее решение для ситуаций, когда параллельное выполнение вызывает такие ошибки не только с базой данных, но также сессиями, кешем, redis и т. Д. c.

1 Ответ

4 голосов
/ 18 апреля 2020

Вы должны иметь возможность использовать lockForUpdate() на модели user, чтобы предотвратить одновременное добавление заказов одним и тем же пользователем:

DB::beginTransaction();
User::where('id', $this->user->id)->lockForUpdate()->first();
// Create order if not exists etc...
DB::commit();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...