最近对老项目进行了重构,需要将老版数据库里的数据迁移到新版的数据库,由于新旧两版的数据库表结构存在较大差异,所以写了php脚本来迁移数据。在Laravel项目下的app\Console\Commands目录下创建命令行类,之后在linux命令行执行 php artisan data:v2 运行
迁移代码很简单,只有几行,用到了laravle查询构建器的chunk方法:
public function familyUploadCount()
{
$service = app(DataService::class);
Scheduling::where('schedule_type', 4)
->where('id', '>', 363100)
->where('id', '<=', 477478)
->orderBy('id')
->select('id', 'student_ids')
->chunk(500,function($data)use($service){
foreach ($data as $value) {
if(isset($value->student_ids[0])){
$service->updateStudentScheduleData($value->student_ids[0], [
'family_upload_count' => 1,
]);
}
echo $value->id."\n";
}
unset($data);
});
}
chunk方法的源代码是根据id来排序,然后每次从数据库中取出500条数据,再传递给开发者定义的闭包函数处理。
/**
* Chunk the results of the query.
*
* @param int $count
* @param callable $callback
* @return bool
*/
public function chunk($count, callable $callback)
{
$this->enforceOrderBy();
$page = 1;
do {
// We'll execute the query for the given page and get the results. If there are
// no results we can just break and return from here. When there are results
// we will call the callback with the current chunk of these results here.
$results = $this->forPage($page, $count)->get();
$countResults = $results->count();
if ($countResults == 0) {
break;
}
// 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 ($callback($results, $page) === false) {
return false;
}
unset($results);
$page++;
} while ($countResults == $count);
return true;
}
但是在命令行运行一段时间后,却出现了内存耗尽的情况。抛出异常:
PHP Fatal error: Allowed memory size of 268 435 456 bytes exhausted
修改php.ini文件,将memory_limit设置为-1,一段时间后发现系统内存被占满,用top命令观察到此时脚本吃掉了系统所有内存,系统不得不启用swap分区,导致系统十分卡顿。
内存耗尽的可能原因:
1. 程序读取了大文件,超出了内存限制;
2. 在程序内分配了大的变量没有及时unset;
3. 循环引用的问题,特别是当执行loop的时候,最容易产生这样的问题;
由于这里没有对大文件进行大写,只有一个循环,并且在循环里执行了数据库查询、更新操作,再结合laravel的单例模式,因此分析可能的原因是对象的某个属性一直在写入数据,常见的如数组 的 push操作,因此导致了属性不断变大,占用的内存越来越多。这里操作数据库用到了laravel的Model类,查看Model源码,发现每次查询和更新都会记录 日志 和发布事件。在运行脚本之前手动关闭:
\DB::disableQueryLog();
\DB::unsetEventDispatcher();
Model::unsetEventDispatcher();