У меня есть следующий метод контроллера, который создает новый ордер, если и только если нет открытых ордеров (открытый ордер имеет статус = 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, вставленных в База данных, когда ожидается, что только одна будет вставлена, а другие не должны. ИМО, что происходит:
- Многие запросы касаются веб-сервера (nginx в моем случае)
- Веб-сервер создает множество PHP процессов (через php -fpm в моем случае) )
- Несколько PHP процессов запускают метод одновременно, передавая if ($ last_active) {...} одновременно, до того, как какая-то запись будет вставлена в другой процесс, что приведет к вставлено несколько записей.
Что я пытался это исправить:
- На nginx стороне, я ограничиваю частоту запросов (10 r / s). Это не очень помогает, потому что все же позволяет очень быстро отправить 10 запросов с очень небольшой задержкой между ними, прежде чем отклонить их. Я не могу установить предельное значение скорости ниже 10 об / с, потому что это повредит обычным пользователям
- На стороне 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.