一、背景
众所周知的,在循环语句中执行数据库操作,对数据库来说负担很大。
举个例子:
for(int I=0;I<500;I++){查询语句}
这样你的这个查询语句会执行500次,稍极端一点,忽略数据库内部执行语句的时间,服务器到数据库服务器之间网络不畅,光是服务器发送查询请求到数据库就需要1秒的时间,返回又1秒。这样你光是网络原因的等待时间就1000秒了。而如果你是在for语句外一次性取500条数据,这个网络请求等待时间就2秒。
当循环量特别大的时候,一方面会造成严重的网络延迟,一方面可能会连接数据库失败,因此,避免循环中执行sql也算是业界的一个准则。
二、如何避免
大前提:博主一直用的laravel框架,因此会用到一些laravel特有的方法,殊途同归,各位道友心里明白就好。
1、循环内拼接sql语句,循环外执行
例如:
//假设这里的$users是一个很大的数组,我们要循环取出其中的值
foreach($users as $user){
//这部分的sql是我从项目中随便摘抄出来的。主要就是实现拼接sql语句
$sql .= " ('{$order_sn}', '{$this->user_id}', '{$deliver_fee}', 0, "
. " '', '', '', "
. " '{$add_time}','{$order_status}', '{$order_amount}', '{$remark}', "
. " '{$pickup_code}', '{$self_pickup}', '{$collect_order_sn}', '{$key}', {$reservation_time}),";
}
//循环中拼接好sql之后,我们在循环外执行
$sql = substr($sql, 0, -1);
DB::statement($sql);
2、where条件换为where in()
比如我们的where()条件中要使用大规模的数组,那么在语句应该是:
foreach($arr as $v){
DB::table('')->where('id',$v->id);
}
此时我们可以转变一下:
//先取出条件数组$arr,在直接执行whereIn即可
DB::table('')->whereIn('id',$arr);
注意:如果数据量很大,比如几十万条数据,那么whereIn也会成为代码的瓶颈,这个函数适用于数据量不是很大的情况。
3、laravel的chunk方法
官方文档:https://laravel-china.org/docs/laravel/5.5/eloquent/1332
如果你需要处理数千个 Eloquent 记录,可以使用 chunk 命令。chunk 方法会检索 Eloquent 模型的「分块」,将它们提供给指定的 Closure 进行处理。在处理大型结果集时,使用 chunk 方法可节省内存:
Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
//
}
});
传递到方法的第一个参数是希望每个「分块」接收的数据量。闭包则被作为第二个参数传递,它会在每次执行数据库查询传递每个块时被调用。
上面这段代码的意思是,先执行200条,执行完之后,再继续执行后200条。用一种类似于分页的方法来小规模的调用数据库操作,如此便可避免大数据量的操作。
4、laravel的chunk()方法的漏洞
注意:laravel的chunk方法在筛选条件并且更新数据的时候,会出现漏掉数据的bug,具体的可参考博客:https://www.jianshu.com/p/5dafd0d6e69a
例如:
User::where('approved', 0)->chunk(100, function ($users) {
foreach ($users as $user) {
$user->update(['approved' => 1]);
}
});
如果要运行上面的代码,并不会有报错,但是where条件是筛选approved为0的user然后将approved的值跟新为1。
在这个过程中,档第一数据库的数据被修改后,下一个数据块的数据将是在被修改后的数据中选出来的,这个时候数据变了,而page也加了1。所以执行结束后,只对数据中一半的数据进行了更新操作。
如果没有明白的话,我们来看一下chunk的底层实现。还以上面的代码为例,假如一共有400条数据,数据被按照100条进行分块处理。
page = 1: 最开始的时候page为1,选取1-100条数据进行处理;
page = 2: 这时候前一百数据的approved值全部为1,那么在次筛选的时候,此时数据库中符合条件的数据只有300条,而且数据将从第101条开始,而这个时候的page=2,那么处理的数据将是第200-300之前的数据。之后依旧。
public function chunk($count, callable $callback)
{
$results = $this->forPage($page = 1, $count)->get();
while (count($results) > 0) {
// On each chunk result set, we will pass them to the callback and then let the
// developer take care of everything within the callback, which allows us to
// keep the memory low for spinning through large result sets for working.
if (call_user_func($callback, $results) === false) {
return false;
}
$page++;
$results = $this->forPage($page, $count)->get();
}
return true;
}
当然,关于chunk()的这个漏洞,我没找到chunk()的源码,但是我用laravel5.5没有出现这个问题。