我有以下控制器方法,当且仅当没有打开的订单时创建新订单(打开的订单具有状态=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请求。这背后的逻辑非常简单:我希望用户一次只有一个活动订单,所以用户在创建新订单之前必须执行一些操作来关闭以前的订单。以下代码在普通用户的情况下非常有效,但在用户想要损害我的应用程序的情况下则不然。所以问题来了。当用户每秒发送大量这样的请求时(我只是在谷歌Chrome开发控制台中使用以下脚本(
for (var i = 0; i < 20; i++)
setTimeout(function(){
$.ajax({
url : '/create',
type : 'post',
success: function(d){
console.log(d)
}
})
}, 1);
它会导致多个状态=0的记录插入数据库,而预期只插入一个,而其他记录则不应插入。IMO,发生的是:
- 许多请求都涉及Web服务器(在我的例子中是nginx(
- Webserver创建了许多PHP进程(在我的例子中是通过PHP-fpm(
- 多个PHP进程同时运行方法,在将某条记录插入另一个进程之前,同时通过if($last_active({…}检查,从而导致插入多条记录
我试图解决的问题:
- 在nginx方面,i限制请求速率(10 r/s(。这并没有多大帮助,因为它仍然允许非常快地发送10个请求,在拒绝它们之前,它们之间的延迟非常小。我不能将速率限制值设置为低于10 r/s,因为这会伤害正常用户
- 在拉拉韦方面,我试图进行交易
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'];
}
使用事务显著减少了插入的行数(通常甚至可以按预期工作——只允许插入一行,但并不总是这样(。
- 我创建了一个中间件,它可以跟踪上次用户请求的时间,并将这些信息存储在会话中
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行(在30种情况中有1-2种(。而且这对我来说确实很奇怪。我考虑过队列,但正如laravel文档所说,它们是为耗时的任务而设计的。我还考虑过表锁定,但对于普通用户来说,这似乎也很奇怪,影响了性能。我相信这个问题有干净简单的解决方案,但我在谷歌上找不到任何合理的东西,也许我错过了一些非常明显的东西?你能帮忙吗?此外,在我的应用程序中有很多类似的情况,我真的想找到一些通用的解决方案,以解决并发执行不仅会导致数据库错误,还会导致会话、缓存、redis等错误的情况。
您应该能够在user
模型上使用lockForUpdate()
,以防止同一用户插入并发订单:
DB::beginTransaction();
User::where('id', $this->user->id)->lockForUpdate()->first();
// Create order if not exists etc...
DB::commit();